@haaaiawd/second-nature 0.2.6 → 0.2.7

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.
@@ -1,16 +1,17 @@
1
1
  /**
2
2
  * QuietDailyReviewBuilder — Aggregate daily closures, perceptions, and
3
- * memory-review candidates into a source-backed QuietDailyReview.
3
+ * memory-review candidates into a readable, source-backed QuietDailyReview.
4
4
  *
5
5
  * Core logic: Read ActionClosureRecords by day, collect memory-review
6
- * candidates, build summary, and write QuietDailyReview row.
6
+ * candidates and attached PerceptionCard summaries, build a human-readable
7
+ * review, and write QuietDailyReview row.
7
8
  *
8
9
  * Design authority:
9
10
  * - `.anws/v8/04_SYSTEM_DESIGN/dream-quiet-memory-system.detail.md §3.1`
10
11
  * - `.anws/v8/04_SYSTEM_DESIGN/dream-quiet-memory-system.md §4.2`
11
12
  *
12
13
  * Dependencies:
13
- * - `src/storage/v8-state-stores.js` (readActionClosuresByDay, writeQuietDailyReview)
14
+ * - `src/storage/v8-state-stores.js` (readActionClosuresByDay, readPerceptionCardById, writeQuietDailyReview)
14
15
  * - `src/shared/types/v8-contracts.js` (SourceRef, DegradedOperationResult, V8ReasonCode)
15
16
  *
16
17
  * Boundary:
@@ -20,11 +21,12 @@
20
21
  *
21
22
  * Test coverage: tests/unit/quiet/quiet-daily-review-builder.test.ts
22
23
  */
23
- import { readActionClosuresByDay, writeQuietDailyReview, } from "../../../storage/v8-state-stores.js";
24
+ import { readActionClosuresByDay, writeQuietDailyReview, readPerceptionCardById, readEvidenceItemsByDay, readPerceptionCardsByDay, } from "../../../storage/v8-state-stores.js";
24
25
  // ───────────────────────────────────────────────────────────────
25
26
  // Config
26
27
  // ───────────────────────────────────────────────────────────────
27
28
  const QUIET_MAX_CLOSURES_PER_DAY = 200;
29
+ const QUIET_REVIEW_MAX_MEMORY_CANDIDATES = 20;
28
30
  // ───────────────────────────────────────────────────────────────
29
31
  // Helpers
30
32
  // ───────────────────────────────────────────────────────────────
@@ -50,6 +52,71 @@ function buildSourceRefFromClosure(closure) {
50
52
  resolveStatus: "resolvable",
51
53
  };
52
54
  }
55
+ function parseSourceRefs(json) {
56
+ if (!json)
57
+ return [];
58
+ try {
59
+ const parsed = JSON.parse(json);
60
+ return Array.isArray(parsed) ? parsed : [];
61
+ }
62
+ catch {
63
+ return [];
64
+ }
65
+ }
66
+ function buildSourceRefFromEvidence(evidence) {
67
+ const refs = parseSourceRefs(evidence.sourceRefsJson);
68
+ return (refs[0] ?? {
69
+ uri: `sn://evidence/${evidence.id}`,
70
+ family: "evidence",
71
+ id: evidence.id,
72
+ redactionClass: "none",
73
+ resolveStatus: "resolvable",
74
+ });
75
+ }
76
+ function buildSourceRefFromPerception(perception) {
77
+ const refs = parseSourceRefs(perception.sourceRefsJson);
78
+ return (refs[0] ?? {
79
+ uri: `sn://perception/${perception.id}`,
80
+ family: "perception",
81
+ id: perception.id,
82
+ redactionClass: "none",
83
+ resolveStatus: "resolvable",
84
+ });
85
+ }
86
+ function renderActionKind(actionKind) {
87
+ if (!actionKind)
88
+ return "action";
89
+ switch (actionKind) {
90
+ case "notify_owner":
91
+ return "notified you";
92
+ case "draft_reply":
93
+ return "drafted a reply";
94
+ case "remember":
95
+ return "remembered for review";
96
+ case "watch":
97
+ return "watched";
98
+ case "auto_reply":
99
+ return "auto-replied";
100
+ default:
101
+ return actionKind;
102
+ }
103
+ }
104
+ function renderClosureLine(entry) {
105
+ const platform = entry.platformId ?? "system";
106
+ const action = renderActionKind(entry.actionKind);
107
+ const reason = entry.reason ? ` (${entry.reason})` : "";
108
+ const summary = entry.summary ? `: ${entry.summary}` : "";
109
+ return `- ${platform} ${action}${summary}${reason} [${entry.closureId}]`;
110
+ }
111
+ function groupByStatus(entries) {
112
+ const groups = {};
113
+ for (const entry of entries) {
114
+ if (!groups[entry.status])
115
+ groups[entry.status] = [];
116
+ groups[entry.status].push(entry);
117
+ }
118
+ return groups;
119
+ }
53
120
  // ───────────────────────────────────────────────────────────────
54
121
  // Public API
55
122
  // ───────────────────────────────────────────────────────────────
@@ -67,22 +134,127 @@ export async function buildQuietDailyReview(db, options) {
67
134
  reason: "quiet_empty_input",
68
135
  };
69
136
  }
70
- const sourceRefs = closures.map(buildSourceRefFromClosure);
71
- // T-DQ.R.4: first-class closure refs — identical to sourceRefs here, but explicitly typed
72
137
  const closureRefs = closures.map(buildSourceRefFromClosure);
73
- // Collect memory-review candidates from closure payloads
138
+ let sourceRefs = [...closureRefs];
139
+ // Load content-bearing evidence and perception rows for the day
140
+ const evidenceRead = await readEvidenceItemsByDay(db, day);
141
+ if (evidenceRead.degraded) {
142
+ return evidenceRead.degraded;
143
+ }
144
+ const perceptionRead = await readPerceptionCardsByDay(db, day);
145
+ if (perceptionRead.degraded) {
146
+ return perceptionRead.degraded;
147
+ }
148
+ const evidenceRows = evidenceRead.rows.slice(0, 100);
149
+ const perceptionRows = perceptionRead.rows.slice(0, 100);
150
+ sourceRefs.push(...evidenceRows.map(buildSourceRefFromEvidence));
151
+ sourceRefs.push(...perceptionRows.map(buildSourceRefFromPerception));
152
+ sourceRefs = [...new Map(sourceRefs.map((r) => [r.uri, r])).values()];
153
+ // Build readable entries, enriching with perception summary when available
154
+ const entries = [];
74
155
  const memoryCandidates = [];
156
+ const notableSignals = [];
75
157
  for (const closure of closures) {
76
158
  const payload = parsePayloadJson(closure.payloadJson);
159
+ let summary;
160
+ let actionKind;
161
+ const perceptionId = payload.perceptionCardId;
162
+ if (perceptionId) {
163
+ const perceptionRead = await readPerceptionCardById(db, perceptionId);
164
+ if (!perceptionRead.degraded && perceptionRead.row) {
165
+ summary = perceptionRead.row.summary ?? undefined;
166
+ const perceptionPayload = parsePayloadJson(perceptionRead.row.payloadJson);
167
+ if (perceptionPayload.possibleIntents && Array.isArray(perceptionPayload.possibleIntents)) {
168
+ actionKind = perceptionPayload.possibleIntents[0];
169
+ }
170
+ }
171
+ }
172
+ // Action kind fallback from closure payload
173
+ if (!actionKind && payload.actionKind) {
174
+ actionKind = String(payload.actionKind);
175
+ }
176
+ entries.push({
177
+ closureId: closure.id,
178
+ platformId: closure.platformId ?? undefined,
179
+ actionKind,
180
+ status: closure.status,
181
+ summary,
182
+ reason: closure.reason ? String(closure.reason) : undefined,
183
+ });
77
184
  if (payload.memoryReviewCandidate) {
78
185
  memoryCandidates.push(payload.memoryReviewCandidate);
79
186
  }
80
187
  }
81
- // Build summary
82
- const completedCount = closures.filter((c) => c.status === "completed").length;
83
- const deniedCount = closures.filter((c) => c.status === "denied").length;
84
- const failedCount = closures.filter((c) => c.status === "failed").length;
85
- const reviewSummary = `Day ${day}: ${closures.length} closures (${completedCount} completed, ${deniedCount} denied, ${failedCount} failed)`;
188
+ for (const perception of perceptionRows) {
189
+ if (perception.summary) {
190
+ notableSignals.push(`Perception: ${perception.summary}`);
191
+ }
192
+ }
193
+ for (const evidence of evidenceRows) {
194
+ const payload = parsePayloadJson(evidence.payloadJson);
195
+ if (payload.summary) {
196
+ notableSignals.push(`${evidence.platformId}: ${String(payload.summary)}`);
197
+ }
198
+ else if (payload.title) {
199
+ notableSignals.push(`${evidence.platformId}: ${String(payload.title)}`);
200
+ }
201
+ }
202
+ const groups = groupByStatus(entries);
203
+ // Build sections
204
+ const sections = [];
205
+ sections.push({
206
+ kind: "headline",
207
+ title: "Headline",
208
+ lines: [`Today I processed ${closures.length} action closures across ${new Set(entries.map((e) => e.platformId)).size} platforms.`],
209
+ });
210
+ if (groups.completed?.length) {
211
+ sections.push({
212
+ kind: "completed",
213
+ title: "Completed",
214
+ lines: groups.completed.slice(0, 10).map(renderClosureLine),
215
+ });
216
+ }
217
+ if (groups.deferred?.length || groups.denied?.length) {
218
+ const deferred = [...(groups.deferred ?? []), ...(groups.denied ?? [])];
219
+ sections.push({
220
+ kind: "deferred",
221
+ title: "Deferred / Denied",
222
+ lines: deferred.slice(0, 10).map(renderClosureLine),
223
+ });
224
+ }
225
+ if (groups.failed?.length) {
226
+ sections.push({
227
+ kind: "failed",
228
+ title: "Failed / Need Attention",
229
+ lines: groups.failed.slice(0, 10).map(renderClosureLine),
230
+ });
231
+ }
232
+ const displayCandidates = memoryCandidates.slice(0, QUIET_REVIEW_MAX_MEMORY_CANDIDATES);
233
+ if (displayCandidates.length > 0) {
234
+ sections.push({
235
+ kind: "memory_candidates",
236
+ title: "Memory-review candidates",
237
+ lines: displayCandidates.map((c) => `- ${c.topicKey ?? "memory candidate"}${c.memoryIntentReason ? ` (${c.memoryIntentReason})` : ""} [${c.perceptionRef?.id ?? "?"}]`),
238
+ });
239
+ }
240
+ if (notableSignals.length > 0) {
241
+ sections.push({
242
+ kind: "observations",
243
+ title: "Notable signals",
244
+ lines: notableSignals.slice(0, 20).map((s) => `- ${s}`),
245
+ });
246
+ }
247
+ const completedCount = groups.completed?.length ?? 0;
248
+ const deniedCount = (groups.denied?.length ?? 0) + (groups.deferred?.length ?? 0);
249
+ const failedCount = groups.failed?.length ?? 0;
250
+ const firstEvidencePayload = evidenceRows[0] ? parsePayloadJson(evidenceRows[0].payloadJson) : {};
251
+ const firstTopic = perceptionRows[0]?.topic
252
+ ?? (firstEvidencePayload.title ? String(firstEvidencePayload.title) : undefined)
253
+ ?? (firstEvidencePayload.summary ? String(firstEvidencePayload.summary) : undefined)
254
+ ?? evidenceRows[0]?.platformId;
255
+ const reviewSummary = firstTopic
256
+ ? `Day ${day}: ${closures.length} closures around ${firstTopic}${notableSignals.length > 0 ? ` with ${notableSignals.length} notable signals` : ""}.`
257
+ : `Day ${day}: ${closures.length} closures (${completedCount} completed, ${deniedCount} deferred/denied, ${failedCount} failed)`;
86
258
  const importanceSignals = [];
87
259
  if (memoryCandidates.length > 0) {
88
260
  importanceSignals.push(`${memoryCandidates.length} memory-review candidates`);
@@ -90,6 +262,9 @@ export async function buildQuietDailyReview(db, options) {
90
262
  if (failedCount > 0) {
91
263
  importanceSignals.push(`${failedCount} failed actions`);
92
264
  }
265
+ if (notableSignals.length > 0) {
266
+ importanceSignals.push(`${notableSignals.length} notable signals`);
267
+ }
93
268
  const reviewId = `quiet_${day}`;
94
269
  const writeResult = await writeQuietDailyReview(db, {
95
270
  id: reviewId,
@@ -105,6 +280,7 @@ export async function buildQuietDailyReview(db, options) {
105
280
  reviewSummary,
106
281
  importanceSignals,
107
282
  memoryCandidates,
283
+ sections,
108
284
  }),
109
285
  });
110
286
  if ("reason" in writeResult) {
@@ -120,6 +296,7 @@ export async function buildQuietDailyReview(db, options) {
120
296
  sourceRefs,
121
297
  closureRefs,
122
298
  reviewSummary,
299
+ sections,
123
300
  importanceSignals,
124
301
  createdAt: now,
125
302
  },
@@ -82,5 +82,5 @@ export interface DegradedOperationResult {
82
82
  operatorNextAction: string;
83
83
  retryable: boolean;
84
84
  }
85
- export type V8ReasonCode = "quiet_completed" | "quiet_empty_input" | "quiet_state_unreadable" | "quiet_validation_failed" | "dream_scheduled" | "dream_scheduler_unavailable" | "dream_started" | "dream_completed" | "dream_failed" | "dream_blocked_redaction" | "projection_candidate_created" | "projection_accepted" | "projection_rejected" | "projection_superseded" | "projection_topic_matched" | "proposal_created" | "proposal_no_action" | "proposal_missing_source_refs" | "proposal_risk_blocked" | "policy_allowed" | "policy_deferred_owner_confirmation" | "policy_downgraded_to_draft" | "policy_denied_missing_permission" | "policy_denied_high_risk" | "policy_denied_breaker_open" | "guidance_unavailable" | "closure_completed" | "closure_no_action" | "closure_denied" | "closure_deferred" | "closure_downgraded" | "closure_downgraded_without_draft" | "closure_failed" | "perception_rules_only" | "perception_contract_drift" | "evidence_batch_empty" | "evidence_batch_truncated" | "judgment_low_confidence" | "judgment_missing_source_refs" | "source_refs_unresolved" | "state_unreadable" | "stage_event_missing" | "ingestion_no_data" | "ingestion_empty" | "ingestion_state_unreadable" | "ingestion_connector_failed" | "execution_completed" | "execution_failed" | "execution_timeout" | "execution_unavailable";
85
+ export type V8ReasonCode = "quiet_completed" | "quiet_empty_input" | "quiet_state_unreadable" | "quiet_validation_failed" | "quiet_redaction_blocked" | "dream_scheduled" | "dream_scheduled_stalled" | "dream_scheduler_unavailable" | "dream_started" | "dream_completed" | "dream_failed" | "dream_blocked_redaction" | "dream_rules_only" | "dream_model_timeout" | "projection_candidate_created" | "projection_accepted" | "projection_rejected" | "projection_superseded" | "projection_topic_matched" | "proposal_created" | "proposal_no_action" | "proposal_missing_source_refs" | "proposal_risk_blocked" | "policy_allowed" | "policy_deferred_owner_confirmation" | "policy_downgraded_to_draft" | "policy_denied_missing_permission" | "policy_denied_high_risk" | "policy_denied_breaker_open" | "guidance_unavailable" | "closure_completed" | "closure_no_action" | "closure_denied" | "closure_deferred" | "closure_downgraded" | "closure_downgraded_without_draft" | "closure_failed" | "perception_rules_only" | "perception_contract_drift" | "evidence_batch_empty" | "evidence_batch_truncated" | "evidence_content_missing" | "judgment_low_confidence" | "judgment_missing_source_refs" | "source_refs_unresolved" | "state_unreadable" | "stage_event_missing" | "ingestion_no_data" | "ingestion_empty" | "ingestion_state_unreadable" | "ingestion_connector_failed" | "execution_completed" | "execution_failed" | "execution_timeout" | "execution_unavailable";
86
86
  export declare const ACTION_KIND_REGISTRY: Readonly<Record<PlatformNeutralActionKind, ActionKindMetadata>>;
@@ -189,6 +189,7 @@ const STATE_SCHEMA_SQL = `
189
189
  payload_json TEXT,
190
190
  lifecycle_status TEXT NOT NULL DEFAULT 'pending'
191
191
  );
192
+ CREATE UNIQUE INDEX IF NOT EXISTS evidence_item_platform_content_hash_idx ON evidence_item(platform_id, content_hash);
192
193
  CREATE TABLE IF NOT EXISTS perception_card (
193
194
  id TEXT PRIMARY KEY,
194
195
  created_at TEXT NOT NULL,
@@ -243,6 +244,7 @@ const STATE_SCHEMA_SQL = `
243
244
  closure_count INTEGER NOT NULL DEFAULT 0,
244
245
  memory_candidate_count INTEGER NOT NULL DEFAULT 0,
245
246
  source_refs_json TEXT NOT NULL,
247
+ closure_refs_json TEXT,
246
248
  redaction_class TEXT NOT NULL DEFAULT 'none',
247
249
  payload_json TEXT,
248
250
  lifecycle_status TEXT NOT NULL DEFAULT 'pending'
@@ -7,6 +7,7 @@ export const V8_003_QUIET_CLOSURE_REFS = {
7
7
  version: 7,
8
8
  label: "v8-quiet-closure-refs",
9
9
  sql: `
10
- ALTER TABLE quiet_daily_review ADD COLUMN closure_refs_json TEXT;
10
+ -- closure_refs_json is now created by the bootstrap schema.
11
+ -- This migration is intentionally a no-op to preserve version continuity.
11
12
  `,
12
13
  };
@@ -23,6 +23,8 @@ export interface WriteValidationResult {
23
23
  ok: boolean;
24
24
  reason?: WriteValidationFailureReason;
25
25
  details?: string;
26
+ field?: string;
27
+ pattern?: string;
26
28
  }
27
29
  export interface WriteValidationGateOptions {
28
30
  /** If true, fact-claim-like payloads require sourceRefs. Default true. */
@@ -63,7 +63,7 @@ function detectSensitiveFieldKey(obj) {
63
63
  const lower = key.toLowerCase();
64
64
  for (const s of SENSITIVE_FIELD_PATTERNS) {
65
65
  if (s.pattern.test(lower)) {
66
- return s.reason;
66
+ return { reason: s.reason, field: key };
67
67
  }
68
68
  }
69
69
  }
@@ -104,36 +104,77 @@ function validateSourceRefs(sourceRefs) {
104
104
  }
105
105
  return undefined;
106
106
  }
107
+ /**
108
+ * Returns true if text is a UUID (with or without dashes).
109
+ * UUIDs are not considered secrets.
110
+ */
111
+ function isUuid(text) {
112
+ return /^[0-9a-fA-F]{8}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{12}$/.test(text);
113
+ }
114
+ const IDENTIFIER_FIELD_NAMES = new Set([
115
+ "id",
116
+ "runId",
117
+ "run_id",
118
+ "sourceRef",
119
+ "source_ref",
120
+ "sourceRefs",
121
+ "source_refs_json",
122
+ "uri",
123
+ "url",
124
+ "externalId",
125
+ "external_id",
126
+ "platform_id",
127
+ "capability_id",
128
+ "candidate_id",
129
+ ]);
130
+ function looksLikeUriPath(text) {
131
+ return /^[a-z][a-z0-9+.-]*:\/\//i.test(text) || (text.includes("/") && !text.includes(" "));
132
+ }
107
133
  /**
108
134
  * Lightweight sensitivity scan: rejects obvious PII or secret patterns
109
- * in string values.
135
+ * in string values. UUIDs and URI-style identifiers are exempt because
136
+ * they appear in normal sourceRefs and are not secrets by themselves.
110
137
  */
111
- function sensitivityScan(value) {
138
+ function sensitivityScan(value, fieldPath = "payload") {
112
139
  if (typeof value === "string") {
140
+ const isIdentifierField = IDENTIFIER_FIELD_NAMES.has(fieldPath.split(".").pop() ?? "");
113
141
  // Basic secret pattern heuristics
114
142
  const secretPatterns = [
115
- /\b[A-Za-z0-9_\-]{32,}\b/, // potential API keys / tokens
116
- /\b-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/,
117
- /\bpassword\s*[:=]\s*\S+/i,
118
- /\bapi[_\-]?key\s*[:=]\s*\S+/i,
119
- /\bsecret\s*[:=]\s*\S+/i,
143
+ // 32+ alphanum token only if it is not a UUID and not part of a URI path/fragment
144
+ { pattern: /\b[A-Za-z0-9_\-]{32,}\b/, exempt: (m) => isUuid(m) },
145
+ { pattern: /\b-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/ },
146
+ { pattern: /\bpassword\s*[:=]\s*\S+/i },
147
+ { pattern: /\bapi[_\-]?key\s*[:=]\s*\S+/i },
148
+ { pattern: /\bsecret\s*[:=]\s*\S+/i },
149
+ { pattern: /\bBearer\s+[a-zA-Z0-9_\-._~+/]+/i },
120
150
  ];
121
- for (const p of secretPatterns) {
122
- if (p.test(value)) {
123
- return "write_validation_failed:sensitivity_scan_failed";
151
+ for (const { pattern, exempt } of secretPatterns) {
152
+ const match = value.match(pattern);
153
+ if (match) {
154
+ const matched = match[0];
155
+ if (exempt && exempt(matched))
156
+ continue;
157
+ if (isIdentifierField || looksLikeUriPath(value))
158
+ continue;
159
+ return {
160
+ reason: "write_validation_failed:sensitivity_scan_failed",
161
+ field: fieldPath,
162
+ pattern: pattern.source,
163
+ };
124
164
  }
125
165
  }
126
166
  }
127
167
  if (Array.isArray(value)) {
128
- for (const item of value) {
129
- const r = sensitivityScan(item);
168
+ for (let i = 0; i < value.length; i += 1) {
169
+ const r = sensitivityScan(value[i], `${fieldPath}[${i}]`);
130
170
  if (r)
131
171
  return r;
132
172
  }
133
173
  }
134
174
  else if (value !== null && typeof value === "object") {
135
- for (const v of Object.values(value)) {
136
- const r = sensitivityScan(v);
175
+ for (const [key, v] of Object.entries(value)) {
176
+ const childPath = fieldPath === "payload" ? key : `${fieldPath}.${key}`;
177
+ const r = sensitivityScan(v, childPath);
137
178
  if (r)
138
179
  return r;
139
180
  }
@@ -164,7 +205,12 @@ export function validateWritePayload(payload, options = {}) {
164
205
  if (scanFieldKeys) {
165
206
  const fieldReason = deepScanSensitiveFields(obj);
166
207
  if (fieldReason) {
167
- return { ok: false, reason: fieldReason };
208
+ return {
209
+ ok: false,
210
+ reason: fieldReason.reason,
211
+ field: fieldReason.field,
212
+ details: `sensitive field key detected: ${fieldReason.field}`,
213
+ };
168
214
  }
169
215
  }
170
216
  // 3. Source refs non-empty for fact-claim-like payloads (DR-022 category 2)
@@ -182,7 +228,13 @@ export function validateWritePayload(payload, options = {}) {
182
228
  if (runSensitivityScan) {
183
229
  const scanReason = sensitivityScan(obj);
184
230
  if (scanReason) {
185
- return { ok: false, reason: scanReason };
231
+ return {
232
+ ok: false,
233
+ reason: scanReason.reason,
234
+ field: scanReason.field,
235
+ pattern: scanReason.pattern,
236
+ details: `sensitivity scan matched ${scanReason.pattern} in ${scanReason.field}`,
237
+ };
186
238
  }
187
239
  }
188
240
  return { ok: true };
@@ -42,6 +42,16 @@ export declare function readEvidenceItemsByStatus(db: StateDatabase, lifecycleSt
42
42
  rows: EvidenceItemRecord[];
43
43
  degraded?: DegradedOperationResult;
44
44
  }>;
45
+ export declare function readEvidenceItemsByDay(db: StateDatabase, day: string): Promise<{
46
+ rows: EvidenceItemRecord[];
47
+ degraded?: DegradedOperationResult;
48
+ }>;
49
+ export declare function updateEvidenceItemLifecycleStatus(db: StateDatabase, id: string, lifecycleStatus: EvidenceItemRecord["lifecycleStatus"]): Promise<{
50
+ id: string;
51
+ } | DegradedOperationResult>;
52
+ export declare function readEvidenceItemById(db: StateDatabase, id: string): Promise<{
53
+ row: EvidenceItemRecord | undefined;
54
+ } | DegradedOperationResult>;
45
55
  export declare function writePerceptionCard(db: StateDatabase, row: Omit<NewPerceptionCardRecord, "sourceRefsJson"> & {
46
56
  sourceRefs: SourceRef[];
47
57
  }): Promise<{
@@ -51,6 +61,10 @@ export declare function readPerceptionCardsByCycle(db: StateDatabase, cycleId: s
51
61
  rows: PerceptionCardRecord[];
52
62
  degraded?: DegradedOperationResult;
53
63
  }>;
64
+ export declare function readPerceptionCardsByDay(db: StateDatabase, day: string): Promise<{
65
+ rows: PerceptionCardRecord[];
66
+ degraded?: DegradedOperationResult;
67
+ }>;
54
68
  export declare function readPerceptionCardById(db: StateDatabase, id: string): Promise<{
55
69
  row?: PerceptionCardRecord;
56
70
  degraded?: DegradedOperationResult;
@@ -100,6 +114,17 @@ export declare function writeDreamConsolidationRun(db: StateDatabase, row: Omit<
100
114
  }): Promise<{
101
115
  id: string;
102
116
  } | DegradedOperationResult>;
117
+ /**
118
+ * Update an existing DreamConsolidationRun status and payload without
119
+ * primary-key conflict. Used by the scheduler after consolidation completes.
120
+ */
121
+ export declare function updateDreamConsolidationRunStatus(db: StateDatabase, id: string, status: DreamConsolidationRunRecord["status"], options?: {
122
+ reason?: DreamConsolidationRunRecord["reason"];
123
+ lifecycleStatus?: DreamConsolidationRunRecord["lifecycleStatus"];
124
+ payloadJson?: string;
125
+ }): Promise<{
126
+ id: string;
127
+ } | DegradedOperationResult>;
103
128
  export declare function readDreamConsolidationRunById(db: StateDatabase, id: string): Promise<{
104
129
  row?: DreamConsolidationRunRecord;
105
130
  degraded?: DegradedOperationResult;
@@ -73,7 +73,16 @@ export async function writeEvidenceItem(db, row) {
73
73
  ...row,
74
74
  sourceRefsJson: serializeSourceRefs(validated.record),
75
75
  };
76
- await db.db.insert(evidenceItem).values(record);
76
+ await db.db
77
+ .insert(evidenceItem)
78
+ .values(record)
79
+ .onConflictDoUpdate({
80
+ target: [evidenceItem.platformId, evidenceItem.contentHash],
81
+ set: {
82
+ payloadJson: record.payloadJson,
83
+ observedAt: record.observedAt,
84
+ },
85
+ });
77
86
  return { id: row.id };
78
87
  }
79
88
  catch {
@@ -96,6 +105,47 @@ export async function readEvidenceItemsByStatus(db, lifecycleStatus) {
96
105
  };
97
106
  }
98
107
  }
108
+ export async function readEvidenceItemsByDay(db, day) {
109
+ try {
110
+ const rows = await db.db
111
+ .select()
112
+ .from(evidenceItem)
113
+ .where(like(evidenceItem.observedAt, `${day}%`))
114
+ .orderBy(desc(evidenceItem.observedAt));
115
+ return { rows };
116
+ }
117
+ catch {
118
+ return {
119
+ rows: [],
120
+ degraded: makeDegraded("state_unreadable", "ingestion", `Check state database connectivity for evidence day=${day}`),
121
+ };
122
+ }
123
+ }
124
+ export async function updateEvidenceItemLifecycleStatus(db, id, lifecycleStatus) {
125
+ try {
126
+ await db.db
127
+ .update(evidenceItem)
128
+ .set({ lifecycleStatus })
129
+ .where(eq(evidenceItem.id, id));
130
+ return { id };
131
+ }
132
+ catch {
133
+ return makeDegraded("state_unreadable", "ingestion", `Retry evidence lifecycle update for ${id} after DB recovery`);
134
+ }
135
+ }
136
+ export async function readEvidenceItemById(db, id) {
137
+ try {
138
+ const rows = await db.db
139
+ .select()
140
+ .from(evidenceItem)
141
+ .where(eq(evidenceItem.id, id))
142
+ .limit(1);
143
+ return { row: rows[0] };
144
+ }
145
+ catch {
146
+ return makeDegraded("state_unreadable", "ingestion", "Check state database connectivity");
147
+ }
148
+ }
99
149
  // ───────────────────────────────────────────────────────────────
100
150
  // PerceptionCard store
101
151
  // ───────────────────────────────────────────────────────────────
@@ -183,6 +233,22 @@ export async function readPerceptionCardsByCycle(db, cycleId) {
183
233
  };
184
234
  }
185
235
  }
236
+ export async function readPerceptionCardsByDay(db, day) {
237
+ try {
238
+ const rows = await db.db
239
+ .select()
240
+ .from(perceptionCard)
241
+ .where(like(perceptionCard.createdAt, `${day}%`))
242
+ .orderBy(desc(perceptionCard.createdAt));
243
+ return { rows };
244
+ }
245
+ catch {
246
+ return {
247
+ rows: [],
248
+ degraded: makeDegraded("state_unreadable", "perception", `Check state database connectivity for perception day=${day}`),
249
+ };
250
+ }
251
+ }
186
252
  export async function readPerceptionCardById(db, id) {
187
253
  try {
188
254
  const rows = await db.db
@@ -369,6 +435,26 @@ export async function writeDreamConsolidationRun(db, row) {
369
435
  return makeDegraded("state_unreadable", "dream", "Retry Dream write after DB recovery", validated.record);
370
436
  }
371
437
  }
438
+ /**
439
+ * Update an existing DreamConsolidationRun status and payload without
440
+ * primary-key conflict. Used by the scheduler after consolidation completes.
441
+ */
442
+ export async function updateDreamConsolidationRunStatus(db, id, status, options) {
443
+ try {
444
+ const updateData = { status };
445
+ if (options?.reason !== undefined)
446
+ updateData.reason = options.reason;
447
+ if (options?.lifecycleStatus !== undefined)
448
+ updateData.lifecycleStatus = options.lifecycleStatus;
449
+ if (options?.payloadJson !== undefined)
450
+ updateData.payloadJson = options.payloadJson;
451
+ await db.db.update(dreamConsolidationRun).set(updateData).where(eq(dreamConsolidationRun.id, id));
452
+ return { id };
453
+ }
454
+ catch {
455
+ return makeDegraded("state_unreadable", "dream", `Retry Dream status update for ${id} after DB recovery`);
456
+ }
457
+ }
372
458
  export async function readDreamConsolidationRunById(db, id) {
373
459
  try {
374
460
  const rows = await db.db