@haaaiawd/second-nature 0.1.24 → 0.1.26

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 (47) hide show
  1. package/index.js +78 -0
  2. package/openclaw.plugin.json +1 -1
  3. package/package.json +5 -5
  4. package/runtime/cli/commands/goal.d.ts +28 -0
  5. package/runtime/cli/commands/goal.js +163 -0
  6. package/runtime/cli/commands/index.js +38 -3
  7. package/runtime/cli/explain/resolve-subject.js +3 -0
  8. package/runtime/cli/ops/ops-router.d.ts +1 -1
  9. package/runtime/cli/ops/ops-router.js +63 -1
  10. package/runtime/cli/ops/workspace-heartbeat-runner.d.ts +6 -0
  11. package/runtime/cli/ops/workspace-heartbeat-runner.js +35 -1
  12. package/runtime/cli/read-models/index.d.ts +14 -2
  13. package/runtime/cli/read-models/index.js +403 -101
  14. package/runtime/cli/read-models/types.d.ts +90 -3
  15. package/runtime/core/second-nature/feedback/owner-reply-feedback.d.ts +46 -0
  16. package/runtime/core/second-nature/feedback/owner-reply-feedback.js +159 -0
  17. package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +11 -1
  18. package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +78 -10
  19. package/runtime/core/second-nature/heartbeat/runtime-snapshot.d.ts +2 -0
  20. package/runtime/core/second-nature/heartbeat/runtime-snapshot.js +1 -1
  21. package/runtime/core/second-nature/heartbeat/snapshot-builder.d.ts +16 -2
  22. package/runtime/core/second-nature/index.d.ts +1 -0
  23. package/runtime/core/second-nature/index.js +1 -0
  24. package/runtime/core/second-nature/orchestrator/goal-priority.d.ts +16 -3
  25. package/runtime/core/second-nature/orchestrator/goal-priority.js +10 -9
  26. package/runtime/core/second-nature/orchestrator/intent-planner.d.ts +29 -1
  27. package/runtime/core/second-nature/orchestrator/intent-planner.js +154 -79
  28. package/runtime/core/second-nature/orchestrator/narrative-update.js +23 -9
  29. package/runtime/core/second-nature/orchestrator/platform-capability-router.d.ts +34 -0
  30. package/runtime/core/second-nature/orchestrator/platform-capability-router.js +115 -0
  31. package/runtime/core/second-nature/outreach/build-outreach-draft-request.d.ts +3 -1
  32. package/runtime/core/second-nature/outreach/build-outreach-draft-request.js +39 -1
  33. package/runtime/core/second-nature/outreach/dispatch-user-outreach.js +21 -2
  34. package/runtime/guidance/draft-outreach-message.js +14 -1
  35. package/runtime/guidance/outreach-draft-schema.d.ts +104 -0
  36. package/runtime/guidance/outreach-draft-schema.js +14 -0
  37. package/runtime/observability/audit/audit-envelope.d.ts +1 -1
  38. package/runtime/observability/query/explain-query.d.ts +3 -0
  39. package/runtime/observability/query/explain-query.js +9 -0
  40. package/runtime/observability/services/lived-experience-audit.d.ts +22 -0
  41. package/runtime/observability/services/lived-experience-audit.js +30 -0
  42. package/runtime/shared/types/credential.d.ts +1 -1
  43. package/runtime/storage/chronicle/session-chronicle-store.d.ts +1 -1
  44. package/runtime/storage/db/schema/narrative-state.d.ts +1 -1
  45. package/runtime/storage/db/schema/narrative-state.js +2 -2
  46. package/runtime/storage/services/credential-vault.d.ts +18 -0
  47. package/runtime/storage/services/credential-vault.js +73 -3
@@ -11,6 +11,9 @@ import { queryExplain, } from "../../observability/query/explain-query.js";
11
11
  import { mapOperatorExplainToReadModel } from "./operator-explain-map.js";
12
12
  import { loadOperatorFallbackRow, toOperatorFallbackView, } from "../../storage/fallback/load-operator-fallback.js";
13
13
  import { loadRhythmPolicySnapshot, } from "../../storage/rhythm/rhythm-policy-snapshot.js";
14
+ import { createNarrativeStateStore } from "../../storage/narrative/narrative-state-store.js";
15
+ import { createRelationshipMemoryStore } from "../../storage/relationship/relationship-memory-store.js";
16
+ import { probeCredentialHealth } from "../../storage/services/credential-vault.js";
14
17
  const INTERNAL_RUNTIME_PLATFORM_ID = "second-nature-runtime";
15
18
  const INTERNAL_RUNTIME_TRACE_PREFIX = "sn-runtime-";
16
19
  function toExplainQuery(subject) {
@@ -30,6 +33,8 @@ function toExplainQuery(subject) {
30
33
  return { kind: "delivery", auditId: subject.id };
31
34
  case "source_ref":
32
35
  return { kind: "source_ref", sourceRefId: subject.id };
36
+ case "relationship":
37
+ return { kind: "relationship", relationshipId: subject.id };
33
38
  default:
34
39
  return undefined;
35
40
  }
@@ -46,6 +51,8 @@ function buildCredentialNextStep(status) {
46
51
  return "submit_verification_answer";
47
52
  if (status === "expired" || status === "revoked" || status === "failed")
48
53
  return "refresh_credential_context";
54
+ if (status === "decrypt_failed")
55
+ return "verify_or_re_create_credential_then_re_import";
49
56
  return undefined;
50
57
  }
51
58
  /**
@@ -112,6 +119,133 @@ function mapConnectorStatus(attempt) {
112
119
  }
113
120
  return "healthy";
114
121
  }
122
+ /**
123
+ * Derive groundingStatus from confidence and status.
124
+ *
125
+ * Rules (in priority order):
126
+ * 1. blocked: status === "awaiting_sources" OR confidence < 0.4
127
+ * 2. pass: confidence >= 0.7 AND status === "active"
128
+ * 3. degraded: all other cases (0.4 <= confidence < 0.7, or status is insufficient_sources)
129
+ */
130
+ function deriveGroundingStatus(status, confidence) {
131
+ if (status === "awaiting_sources" || confidence < 0.4) {
132
+ return "blocked";
133
+ }
134
+ if (confidence >= 0.7 && status === "active") {
135
+ return "pass";
136
+ }
137
+ return "degraded";
138
+ }
139
+ /**
140
+ * Build the base StatusReadModel that is shared by loadStatus and loadV6Status.
141
+ * Centralising this logic eliminates the DRY violation identified in CR-01.
142
+ */
143
+ async function buildBaseStatus(deps) {
144
+ let recentAttempts = [];
145
+ let recentDecisions = [];
146
+ let credentials = [];
147
+ try {
148
+ recentAttempts = await deps.observabilityDb.db
149
+ .select()
150
+ .from(executionAttempts)
151
+ .orderBy(desc(executionAttempts.startedAt), desc(executionAttempts.finishedAt))
152
+ .limit(50);
153
+ }
154
+ catch {
155
+ recentAttempts = [];
156
+ }
157
+ try {
158
+ recentDecisions = await deps.observabilityDb.db
159
+ .select()
160
+ .from(decisionLedger)
161
+ .orderBy(desc(decisionLedger.createdAt))
162
+ .limit(50);
163
+ }
164
+ catch {
165
+ recentDecisions = [];
166
+ }
167
+ try {
168
+ credentials = await deps.stateDb.db.query.credentialRecords.findMany();
169
+ }
170
+ catch {
171
+ credentials = [];
172
+ }
173
+ const latestRuntimeAttempt = recentAttempts.find((attempt) => attempt.platformId === INTERNAL_RUNTIME_PLATFORM_ID);
174
+ const latestConnectorAttempt = recentAttempts.find((attempt) => attempt.platformId !== INTERNAL_RUNTIME_PLATFORM_ID);
175
+ const latestRuntimeDecision = recentDecisions.find((decision) => decision.traceId.startsWith(INTERNAL_RUNTIME_TRACE_PREFIX));
176
+ const runtimeUpdatedAt = latestRuntimeAttempt?.finishedAt ??
177
+ latestRuntimeAttempt?.startedAt ??
178
+ latestRuntimeDecision?.createdAt ??
179
+ "";
180
+ const quietMode = latestRuntimeDecision?.mode === "quiet" ||
181
+ latestRuntimeDecision?.mode === "maintenance_only" ||
182
+ latestRuntimeDecision?.mode === "paused_for_interrupt"
183
+ ? latestRuntimeDecision.mode
184
+ : "unknown";
185
+ const riskFlags = [
186
+ latestRuntimeAttempt?.failureClass,
187
+ latestConnectorAttempt?.failureClass,
188
+ ].filter((value) => Boolean(value));
189
+ const connectorSummary = latestConnectorAttempt
190
+ ? [
191
+ {
192
+ platformId: latestConnectorAttempt.platformId,
193
+ status: mapConnectorStatus(latestConnectorAttempt),
194
+ channel: latestConnectorAttempt.channel,
195
+ failureClass: latestConnectorAttempt.failureClass ?? undefined,
196
+ },
197
+ ]
198
+ : [];
199
+ return {
200
+ runtime: {
201
+ host: "openclaw-plugin",
202
+ serviceStatus: mapRuntimeStatus(latestRuntimeAttempt),
203
+ updatedAt: runtimeUpdatedAt,
204
+ },
205
+ rhythm: {
206
+ mode: latestRuntimeDecision?.mode ?? "unknown",
207
+ },
208
+ quiet: {
209
+ mode: quietMode,
210
+ lastEvent: latestRuntimeDecision?.traceId,
211
+ interrupted: latestRuntimeDecision?.mode === "paused_for_interrupt"
212
+ ? true
213
+ : undefined,
214
+ },
215
+ connectors: connectorSummary,
216
+ credentials: credentials.map((item) => {
217
+ const platformId = item.platformId ??
218
+ item.platform_id;
219
+ const encryptedValue = item.encryptedValue ??
220
+ item.encrypted_value;
221
+ const baseUrl = item.baseUrl ??
222
+ item.base_url;
223
+ const health = probeCredentialHealth(platformId, encryptedValue, baseUrl);
224
+ const effectiveStatus = health.state === "decrypt_failed"
225
+ ? "decrypt_failed"
226
+ : item.status;
227
+ return {
228
+ platformId,
229
+ status: effectiveStatus,
230
+ nextStep: health.diagnosticCode === "missing_runtime_secret"
231
+ ? "set_SECOND_NATURE_ENCRYPTION_KEY_then_re_probe"
232
+ : health.diagnosticCode === "credential_recovery_required"
233
+ ? "verify_or_re_create_credential_then_re_import"
234
+ : buildCredentialNextStep(effectiveStatus),
235
+ keyHealth: health.keyHealth,
236
+ };
237
+ }),
238
+ risk: {
239
+ level: riskFlags.length > 0 ? "medium" : "low",
240
+ flags: riskFlags,
241
+ },
242
+ deliveryPosture: {
243
+ verdict: "none",
244
+ source: "workspace_default_none",
245
+ reasonCode: "delivery_target_none",
246
+ },
247
+ };
248
+ }
115
249
  export function createCliReadModels(deps) {
116
250
  const assetRepository = new AssetRepository(deps.stateDb);
117
251
  const credentialRepository = new CredentialRepository(deps.stateDb);
@@ -124,103 +258,7 @@ export function createCliReadModels(deps) {
124
258
  const auditStore = deps.livedExperienceAuditStore ?? new AppendOnlyAuditStore();
125
259
  return {
126
260
  async loadStatus(_scope) {
127
- let recentAttempts = [];
128
- let recentDecisions = [];
129
- let credentials = [];
130
- try {
131
- recentAttempts = await deps.observabilityDb.db
132
- .select()
133
- .from(executionAttempts)
134
- .orderBy(desc(executionAttempts.startedAt), desc(executionAttempts.finishedAt))
135
- .limit(50);
136
- }
137
- catch {
138
- recentAttempts = [];
139
- }
140
- try {
141
- recentDecisions = await deps.observabilityDb.db
142
- .select()
143
- .from(decisionLedger)
144
- .orderBy(desc(decisionLedger.createdAt))
145
- .limit(50);
146
- }
147
- catch {
148
- recentDecisions = [];
149
- }
150
- try {
151
- credentials = await deps.stateDb.db.query.credentialRecords.findMany();
152
- }
153
- catch {
154
- credentials = [];
155
- }
156
- const latestRuntimeAttempt = recentAttempts.find((attempt) => attempt.platformId === INTERNAL_RUNTIME_PLATFORM_ID);
157
- // CH-15-04 (CH-14-03): latestConnectorAttempt is the most recent execution attempt whose
158
- // platformId is NOT the internal sn-runtime sentinel — i.e. a real connector platform
159
- // (Moltbook, EvoMap, etc.). The `connectors` array in StatusReadModel reflects this single
160
- // most-recent non-runtime attempt, NOT the full connector manifest. An empty array means
161
- // no connector attempt has been recorded yet, not that connectors are misconfigured.
162
- const latestConnectorAttempt = recentAttempts.find((attempt) => attempt.platformId !== INTERNAL_RUNTIME_PLATFORM_ID);
163
- const latestRuntimeDecision = recentDecisions.find((decision) => decision.traceId.startsWith(INTERNAL_RUNTIME_TRACE_PREFIX));
164
- const runtimeUpdatedAt = latestRuntimeAttempt?.finishedAt ??
165
- latestRuntimeAttempt?.startedAt ??
166
- latestRuntimeDecision?.createdAt ??
167
- "";
168
- const quietMode = latestRuntimeDecision?.mode === "quiet" ||
169
- latestRuntimeDecision?.mode === "maintenance_only" ||
170
- latestRuntimeDecision?.mode === "paused_for_interrupt"
171
- ? latestRuntimeDecision.mode
172
- : "unknown";
173
- const riskFlags = [
174
- latestRuntimeAttempt?.failureClass,
175
- latestConnectorAttempt?.failureClass,
176
- ].filter((value) => Boolean(value));
177
- const connectorSummary = latestConnectorAttempt
178
- ? [
179
- {
180
- platformId: latestConnectorAttempt.platformId,
181
- status: mapConnectorStatus(latestConnectorAttempt),
182
- channel: latestConnectorAttempt.channel,
183
- failureClass: latestConnectorAttempt.failureClass ?? undefined,
184
- },
185
- ]
186
- : [];
187
- return {
188
- runtime: {
189
- host: "openclaw-plugin",
190
- serviceStatus: mapRuntimeStatus(latestRuntimeAttempt),
191
- updatedAt: runtimeUpdatedAt,
192
- },
193
- rhythm: {
194
- mode: latestRuntimeDecision?.mode ?? "unknown",
195
- windowId: undefined,
196
- },
197
- quiet: {
198
- mode: quietMode,
199
- lastEvent: latestRuntimeDecision?.traceId,
200
- interrupted: latestRuntimeDecision?.mode === "paused_for_interrupt"
201
- ? true
202
- : undefined,
203
- },
204
- connectors: connectorSummary,
205
- credentials: credentials.map((item) => ({
206
- platformId: item.platformId ??
207
- item.platform_id,
208
- status: item.status,
209
- nextStep: buildCredentialNextStep(item.status),
210
- })),
211
- risk: {
212
- level: riskFlags.length > 0 ? "medium" : "low",
213
- flags: riskFlags,
214
- },
215
- // T1.2.5 (CH-14-04): default delivery posture is workspace_default_none because the
216
- // workspace heartbeat hardcodes `deliveryCapability: { target: "none" }` until a host
217
- // capability probe explicitly sets a valid target.
218
- deliveryPosture: {
219
- verdict: "none",
220
- source: "workspace_default_none",
221
- reasonCode: "delivery_target_none",
222
- },
223
- };
261
+ return buildBaseStatus(deps);
224
262
  },
225
263
  async loadDailyReport(day) {
226
264
  let bundle;
@@ -309,19 +347,40 @@ export function createCliReadModels(deps) {
309
347
  record = undefined;
310
348
  }
311
349
  if (!record) {
350
+ // T1.4.1: even when no row exists, probe key health so status can surface
351
+ // missing_runtime_secret rather than a generic "missing".
352
+ const health = probeCredentialHealth(platformId, null, null);
312
353
  return {
313
354
  platformId,
314
- status: "missing",
315
- nextStep: "provide_credential_context",
355
+ status: health.state,
356
+ nextStep: health.diagnosticCode === "missing_runtime_secret"
357
+ ? "set_SECOND_NATURE_ENCRYPTION_KEY_then_re_probe"
358
+ : "provide_credential_context",
359
+ keyHealth: health.keyHealth,
316
360
  };
317
361
  }
362
+ // T1.4.1: attempt decryption to detect decrypt_failed / wrong_key.
363
+ const encryptedValue = record.encryptedValue ??
364
+ record.encrypted_value;
365
+ const baseUrl = record.baseUrl ??
366
+ record.base_url;
367
+ const health = probeCredentialHealth(platformId, encryptedValue, baseUrl);
368
+ // If decryption failed, surface the honest diagnostic; otherwise surface DB status.
369
+ const effectiveStatus = health.state === "decrypt_failed"
370
+ ? "decrypt_failed"
371
+ : record.status;
318
372
  return {
319
373
  platformId: record.platformId ??
320
374
  record.platform_id,
321
- status: record.status,
375
+ status: effectiveStatus,
322
376
  verificationDeadline: record.expiresAt ?? undefined,
323
377
  attemptsRemaining: record.attemptsRemaining ?? undefined,
324
- nextStep: buildCredentialNextStep(record.status),
378
+ nextStep: health.diagnosticCode === "missing_runtime_secret"
379
+ ? "set_SECOND_NATURE_ENCRYPTION_KEY_then_re_probe"
380
+ : health.diagnosticCode === "credential_recovery_required"
381
+ ? "verify_or_re_create_credential_then_re_import"
382
+ : buildCredentialNextStep(effectiveStatus),
383
+ keyHealth: health.keyHealth,
325
384
  };
326
385
  },
327
386
  async loadFallbackView(ref) {
@@ -374,6 +433,34 @@ export function createCliReadModels(deps) {
374
433
  evidenceRefs: [],
375
434
  };
376
435
  }
436
+ // T1.4.2: relationship explain reads RelationshipMemory store directly.
437
+ if (subject.kind === "relationship") {
438
+ const relationshipStore = createRelationshipMemoryStore(deps.stateDb);
439
+ const memory = await relationshipStore.loadRelationshipMemory(subject.id);
440
+ if (!memory) {
441
+ return {
442
+ subjectType: "relationship",
443
+ conclusion: "nothing_yet",
444
+ keyFactors: ["no_relationship_memory_recorded"],
445
+ evidenceRefs: [],
446
+ nextStep: "interact_with_agent_then_re_check",
447
+ };
448
+ }
449
+ return {
450
+ subjectType: "relationship",
451
+ conclusion: `tone:${memory.tonePreference} replies:${memory.noReplyCount === 0 ? "responsive" : "cooldown"}`,
452
+ keyFactors: [
453
+ `tone_preference:${memory.tonePreference}`,
454
+ ...(memory.averageReplyDelayMinutes
455
+ ? [`avg_reply_delay_minutes:${memory.averageReplyDelayMinutes}`]
456
+ : []),
457
+ ...(memory.topicAffinities.length > 0
458
+ ? [`topics:${memory.topicAffinities.map((t) => t.topic).join(",")}`]
459
+ : ["insufficient_history"]),
460
+ ],
461
+ evidenceRefs: memory.sourceRefs.map((s) => s.sourceId),
462
+ };
463
+ }
377
464
  const query = subject.kind === "decision" ||
378
465
  subject.kind === "platform-selection" ||
379
466
  subject.kind === "outreach"
@@ -387,5 +474,220 @@ export function createCliReadModels(deps) {
387
474
  evidenceRefs: bundle.explanation.evidenceRefs,
388
475
  };
389
476
  },
477
+ // T1.2.2 — read recent DreamTrace events from audit store.
478
+ async loadDreamRecent(limit = 5) {
479
+ const events = auditStore.list().filter((e) => e.family === "dream.trace");
480
+ const recent = events
481
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt))
482
+ .slice(0, limit);
483
+ return {
484
+ runs: recent.map((e) => {
485
+ const p = e.payload;
486
+ return {
487
+ traceId: p.traceId,
488
+ runId: p.runId,
489
+ durationMs: p.durationMs ?? 0,
490
+ inputCounts: p.inputCounts ?? { evidence: 0, chronicle: 0, memoryEntries: 0 },
491
+ fallbackReason: p.fallbackReason,
492
+ lifecycleStatus: p.fallbackReason ? "partial" : "completed",
493
+ insightsCount: 0, // would require deeper payload parsing
494
+ createdAt: e.createdAt,
495
+ };
496
+ }),
497
+ totalRuns: events.length,
498
+ };
499
+ },
500
+ // T1.2.5 — aggregate recent heartbeat, narrative, dream, delivery events into cycles.
501
+ async loadCycleRecent(limit = 5) {
502
+ const events = auditStore.list();
503
+ const decisions = events.filter((e) => e.family === "heartbeat.decision");
504
+ const narratives = events.filter((e) => e.family === "narrative.trace");
505
+ const dreams = events.filter((e) => e.family === "dream.trace");
506
+ const deliveries = events.filter((e) => e.family === "delivery");
507
+ const connectors = events.filter((e) => e.family === "connector.attempt");
508
+ // Group by time buckets (hourly)
509
+ const buckets = new Map();
510
+ for (const e of decisions) {
511
+ const hour = e.createdAt.slice(0, 13);
512
+ const b = buckets.get(hour) ?? { timestamp: `${hour}:00:00Z`, dimensions: [] };
513
+ if (!b.dimensions.includes("decision"))
514
+ b.dimensions.push("decision");
515
+ const p = e.payload;
516
+ if (p.outcome)
517
+ b.decisionOutcome = p.outcome;
518
+ buckets.set(hour, b);
519
+ }
520
+ for (const e of narratives) {
521
+ const hour = e.createdAt.slice(0, 13);
522
+ const b = buckets.get(hour) ?? { timestamp: `${hour}:00:00Z`, dimensions: [] };
523
+ if (!b.dimensions.includes("narrative"))
524
+ b.dimensions.push("narrative");
525
+ const p = e.payload;
526
+ if (p.groundingStatus)
527
+ b.narrativeGrounding = p.groundingStatus;
528
+ buckets.set(hour, b);
529
+ }
530
+ for (const e of dreams) {
531
+ const hour = e.createdAt.slice(0, 13);
532
+ const b = buckets.get(hour) ?? { timestamp: `${hour}:00:00Z`, dimensions: [] };
533
+ if (!b.dimensions.includes("dream"))
534
+ b.dimensions.push("dream");
535
+ const p = e.payload;
536
+ if (p.fallbackReason)
537
+ b.dreamFallback = p.fallbackReason;
538
+ buckets.set(hour, b);
539
+ }
540
+ for (const e of deliveries) {
541
+ const hour = e.createdAt.slice(0, 13);
542
+ const b = buckets.get(hour) ?? { timestamp: `${hour}:00:00Z`, dimensions: [] };
543
+ if (!b.dimensions.includes("delivery"))
544
+ b.dimensions.push("delivery");
545
+ const p = e.payload;
546
+ if (p.status)
547
+ b.deliveryStatus = p.status;
548
+ buckets.set(hour, b);
549
+ }
550
+ for (const e of connectors) {
551
+ const hour = e.createdAt.slice(0, 13);
552
+ const b = buckets.get(hour) ?? { timestamp: `${hour}:00:00Z`, dimensions: [] };
553
+ if (!b.dimensions.includes("connector"))
554
+ b.dimensions.push("connector");
555
+ buckets.set(hour, b);
556
+ }
557
+ const cycles = Array.from(buckets.values())
558
+ .sort((a, b) => b.timestamp.localeCompare(a.timestamp))
559
+ .slice(0, limit);
560
+ return { cycles, totalCycles: buckets.size };
561
+ },
562
+ // T1.2.6 — v6 status aggregate: compose base status + narrative + dream + cycle sections.
563
+ // Each section returns a sentinel status (nothing_yet / has_runs / has_cycles) so operators
564
+ // always get a meaningful non-empty response, never a raw empty object.
565
+ async loadV6Status(scope) {
566
+ // Load NarrativeState asynchronously; audit events are synchronous reads from in-memory store.
567
+ const narrativeStore = createNarrativeStateStore(deps.stateDb);
568
+ let narrativeState;
569
+ try {
570
+ narrativeState = await narrativeStore.loadNarrativeState();
571
+ }
572
+ catch {
573
+ narrativeState = null;
574
+ }
575
+ const allAuditEvents = auditStore.list();
576
+ const dreamSection = allAuditEvents.filter((e) => e.family === "dream.trace");
577
+ const cycleSection = allAuditEvents;
578
+ const baseStatus = await buildBaseStatus(deps);
579
+ // Narrative section
580
+ let narrativeSectionOut;
581
+ if (!narrativeState) {
582
+ narrativeSectionOut = { status: "nothing_yet", focus: "", groundingStatus: "blocked", nextIntent: "", sourceRefCount: 0 };
583
+ }
584
+ else {
585
+ const groundingStatus = deriveGroundingStatus(narrativeState.status, narrativeState.confidence);
586
+ narrativeSectionOut = { status: narrativeState.status, focus: narrativeState.focus, groundingStatus, nextIntent: narrativeState.nextIntent, sourceRefCount: narrativeState.sourceRefs.length };
587
+ }
588
+ // Dream section — degraded when all recorded dream runs have a fallbackReason.
589
+ const dreamEvents = dreamSection;
590
+ let dreamSectionOut;
591
+ if (dreamEvents.length === 0) {
592
+ dreamSectionOut = { status: "nothing_yet", totalRuns: 0, recentRunCount: 0 };
593
+ }
594
+ else {
595
+ const recentDreams = dreamEvents.sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, 3);
596
+ const lastFallback = recentDreams.map((e) => e.payload.fallbackReason).find(Boolean);
597
+ const allDegraded = dreamEvents.every((e) => !!e.payload.fallbackReason);
598
+ dreamSectionOut = {
599
+ status: allDegraded ? "degraded" : "has_runs",
600
+ totalRuns: dreamEvents.length,
601
+ recentRunCount: recentDreams.length,
602
+ lastFallbackReason: lastFallback,
603
+ };
604
+ }
605
+ // Cycle section — degraded when buckets exist but cover fewer than 3 dimensions.
606
+ const allEvents = cycleSection;
607
+ const decisionEvents = allEvents.filter((e) => e.family === "heartbeat.decision");
608
+ const narrativeEvents = allEvents.filter((e) => e.family === "narrative.trace");
609
+ const dreamEventsForCycle = allEvents.filter((e) => e.family === "dream.trace");
610
+ const deliveryEvents = allEvents.filter((e) => e.family === "delivery");
611
+ const connectorEvents = allEvents.filter((e) => e.family === "connector.attempt");
612
+ const hourBuckets = new Set();
613
+ const dimensionSet = new Set();
614
+ for (const e of decisionEvents) {
615
+ hourBuckets.add(e.createdAt.slice(0, 13));
616
+ dimensionSet.add("decision");
617
+ }
618
+ for (const e of narrativeEvents) {
619
+ hourBuckets.add(e.createdAt.slice(0, 13));
620
+ dimensionSet.add("narrative");
621
+ }
622
+ for (const e of dreamEventsForCycle) {
623
+ hourBuckets.add(e.createdAt.slice(0, 13));
624
+ dimensionSet.add("dream");
625
+ }
626
+ for (const e of deliveryEvents) {
627
+ hourBuckets.add(e.createdAt.slice(0, 13));
628
+ dimensionSet.add("delivery");
629
+ }
630
+ for (const e of connectorEvents) {
631
+ hourBuckets.add(e.createdAt.slice(0, 13));
632
+ dimensionSet.add("connector");
633
+ }
634
+ let cycleSectionOut;
635
+ if (hourBuckets.size === 0) {
636
+ cycleSectionOut = { status: "nothing_yet", totalCycles: 0, recentCycleCount: 0, dimensions: [] };
637
+ }
638
+ else if (dimensionSet.size < 3) {
639
+ cycleSectionOut = { status: "degraded", totalCycles: hourBuckets.size, recentCycleCount: Math.min(hourBuckets.size, 5), dimensions: Array.from(dimensionSet) };
640
+ }
641
+ else {
642
+ cycleSectionOut = { status: "has_cycles", totalCycles: hourBuckets.size, recentCycleCount: Math.min(hourBuckets.size, 5), dimensions: Array.from(dimensionSet) };
643
+ }
644
+ void scope; // scope param reserved for future scoping — not used in v6 aggregate yet
645
+ return { ...baseStatus, narrative: narrativeSectionOut, dream: dreamSectionOut, cycles: cycleSectionOut };
646
+ },
647
+ // T1.2.1 — read current NarrativeState and map to NarrativeReadModel.
648
+ // Returns `nothing_yet` status when no data exists — honest empty, not an error.
649
+ async loadNarrative(narrativeId) {
650
+ const narrativeStore = createNarrativeStateStore(deps.stateDb);
651
+ let state;
652
+ try {
653
+ state = await narrativeStore.loadNarrativeState(narrativeId);
654
+ }
655
+ catch {
656
+ state = null;
657
+ }
658
+ if (!state) {
659
+ return {
660
+ narrativeId: narrativeId ?? "default",
661
+ revision: 0,
662
+ focus: "",
663
+ progress: [],
664
+ nextIntent: "",
665
+ confidence: 0,
666
+ sourceRefs: [],
667
+ unsupportedClaims: [],
668
+ groundingStatus: "blocked",
669
+ status: "nothing_yet",
670
+ updatedAt: "",
671
+ };
672
+ }
673
+ const groundingStatus = deriveGroundingStatus(state.status, state.confidence);
674
+ return {
675
+ narrativeId: state.narrativeId,
676
+ revision: state.revision,
677
+ focus: state.focus,
678
+ progress: state.progress,
679
+ nextIntent: state.nextIntent,
680
+ confidence: state.confidence,
681
+ sourceRefs: state.sourceRefs.map((r) => ({
682
+ sourceId: r.sourceId,
683
+ kind: r.kind,
684
+ url: r.url,
685
+ })),
686
+ unsupportedClaims: state.unsupportedClaims,
687
+ groundingStatus,
688
+ status: state.status,
689
+ updatedAt: state.updatedAt,
690
+ };
691
+ },
390
692
  };
391
693
  }
@@ -25,8 +25,10 @@ export interface ConnectorSummary {
25
25
  }
26
26
  export interface CredentialSummary {
27
27
  platformId: string;
28
- status: "missing" | "pending_verification" | "active" | "expired" | "revoked" | "failed";
28
+ status: "missing" | "pending_verification" | "active" | "expired" | "revoked" | "failed" | "decrypt_failed";
29
29
  nextStep?: string;
30
+ /** T1.4.1 — diagnostic key health without leaking raw secret. */
31
+ keyHealth?: "missing_key" | "wrong_key" | "ok";
30
32
  }
31
33
  export interface RiskSummary {
32
34
  level: "low" | "medium" | "high";
@@ -73,6 +75,38 @@ export interface StatusReadModel {
73
75
  */
74
76
  deliveryPosture?: DeliveryPosture;
75
77
  }
78
+ /**
79
+ * T1.2.6 — v6 status aggregate: extends StatusReadModel with narrative, dream recent,
80
+ * cycle recent, and per-section `nothing_yet` / `awaiting_sources` / `degraded` sentinels
81
+ * so operators always get a meaningful view even when individual data sources are empty.
82
+ */
83
+ export interface StatusV6NarrativeSection {
84
+ status: "active" | "insufficient_sources" | "awaiting_sources" | "nothing_yet";
85
+ focus: string;
86
+ groundingStatus: "pass" | "degraded" | "blocked";
87
+ nextIntent: string;
88
+ sourceRefCount: number;
89
+ }
90
+ export interface StatusV6DreamSection {
91
+ status: "has_runs" | "degraded" | "nothing_yet";
92
+ totalRuns: number;
93
+ recentRunCount: number;
94
+ lastFallbackReason?: string;
95
+ }
96
+ export interface StatusV6CycleSection {
97
+ status: "has_cycles" | "degraded" | "nothing_yet";
98
+ totalCycles: number;
99
+ recentCycleCount: number;
100
+ dimensions: string[];
101
+ }
102
+ export interface StatusV6ReadModel extends StatusReadModel {
103
+ /** v6 narrative section; status is nothing_yet when no NarrativeState row exists. */
104
+ narrative: StatusV6NarrativeSection;
105
+ /** v6 dream recent section; status is nothing_yet when no DreamTrace events exist. */
106
+ dream: StatusV6DreamSection;
107
+ /** v6 cycle recent section; status is nothing_yet when no cycle events exist. */
108
+ cycles: StatusV6CycleSection;
109
+ }
76
110
  export interface DailyReportReadModel {
77
111
  day: string;
78
112
  summary: string;
@@ -97,12 +131,17 @@ export interface SessionDetailReadModel {
97
131
  }
98
132
  export interface CredentialReadModel {
99
133
  platformId: string;
100
- status: "missing" | "pending_verification" | "active" | "expired" | "revoked" | "failed";
134
+ status: "missing" | "pending_verification" | "active" | "expired" | "revoked" | "failed" | "decrypt_failed";
101
135
  verificationDeadline?: string;
102
136
  attemptsRemaining?: number;
103
137
  nextStep?: string;
138
+ /**
139
+ * T1.4.1 — redacted diagnostic: when true, the raw encrypted value could not be
140
+ * decrypted because SECOND_NATURE_ENCRYPTION_KEY is missing or wrong.
141
+ */
142
+ keyHealth?: "missing_key" | "wrong_key" | "ok";
104
143
  }
105
- export type ExplainSubjectKind = "decision" | "platform-selection" | "outreach" | "soul-change" | "fallback" | "probe" | "delivery" | "report" | "source_ref";
144
+ export type ExplainSubjectKind = "decision" | "platform-selection" | "outreach" | "soul-change" | "fallback" | "probe" | "delivery" | "report" | "source_ref" | "relationship";
106
145
  export interface ExplainReadModel {
107
146
  subjectType: ExplainSubjectKind;
108
147
  conclusion: string;
@@ -127,3 +166,51 @@ export interface AuditSummaryReadModel {
127
166
  totalEvents: number;
128
167
  events: AuditEventSummaryEntry[];
129
168
  }
169
+ /** T1.2.2 — recent Dream run summary for operator `dream:recent` command. */
170
+ export interface DreamRecentReadModel {
171
+ runs: Array<{
172
+ traceId: string;
173
+ runId: string;
174
+ durationMs: number;
175
+ inputCounts: {
176
+ evidence: number;
177
+ chronicle: number;
178
+ memoryEntries: number;
179
+ };
180
+ fallbackReason?: string;
181
+ lifecycleStatus: string;
182
+ insightsCount: number;
183
+ createdAt: string;
184
+ }>;
185
+ totalRuns: number;
186
+ }
187
+ /** T1.2.1 — NarrativeState read model for operator `narrative` command. */
188
+ export interface NarrativeReadModel {
189
+ narrativeId: string;
190
+ revision: number;
191
+ focus: string;
192
+ progress: string[];
193
+ nextIntent: string;
194
+ confidence: number;
195
+ sourceRefs: Array<{
196
+ sourceId: string;
197
+ kind: string;
198
+ url?: string;
199
+ }>;
200
+ unsupportedClaims: string[];
201
+ groundingStatus: "pass" | "degraded" | "blocked";
202
+ status: "active" | "insufficient_sources" | "awaiting_sources" | "nothing_yet";
203
+ updatedAt: string;
204
+ }
205
+ /** T1.2.5 — recent cycle summary aggregating heartbeat, narrative, dream, delivery. */
206
+ export interface CycleRecentReadModel {
207
+ cycles: Array<{
208
+ timestamp: string;
209
+ dimensions: Array<"decision" | "narrative" | "dream" | "delivery" | "connector">;
210
+ decisionOutcome?: string;
211
+ narrativeGrounding?: string;
212
+ dreamFallback?: string;
213
+ deliveryStatus?: string;
214
+ }>;
215
+ totalCycles: number;
216
+ }