@haaaiawd/second-nature 0.1.24 → 0.1.25
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.
- package/index.js +77 -0
- package/package.json +5 -5
- package/runtime/cli/commands/goal.d.ts +26 -0
- package/runtime/cli/commands/goal.js +159 -0
- package/runtime/cli/commands/index.js +37 -2
- package/runtime/cli/ops/ops-router.d.ts +1 -1
- package/runtime/cli/ops/ops-router.js +55 -1
- package/runtime/cli/read-models/index.d.ts +14 -2
- package/runtime/cli/read-models/index.js +328 -97
- package/runtime/cli/read-models/types.d.ts +80 -0
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +3 -0
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +33 -6
- package/runtime/core/second-nature/orchestrator/goal-priority.d.ts +2 -1
- package/runtime/core/second-nature/orchestrator/goal-priority.js +8 -7
- package/runtime/core/second-nature/outreach/build-outreach-draft-request.d.ts +3 -1
- package/runtime/core/second-nature/outreach/build-outreach-draft-request.js +39 -1
- package/runtime/core/second-nature/outreach/dispatch-user-outreach.js +21 -2
- package/runtime/guidance/draft-outreach-message.js +14 -1
- package/runtime/guidance/outreach-draft-schema.d.ts +104 -0
- package/runtime/guidance/outreach-draft-schema.js +14 -0
- package/runtime/observability/audit/audit-envelope.d.ts +1 -1
- package/runtime/observability/services/lived-experience-audit.d.ts +22 -0
- package/runtime/observability/services/lived-experience-audit.js +30 -0
- package/runtime/storage/db/schema/narrative-state.d.ts +1 -1
- package/runtime/storage/db/schema/narrative-state.js +2 -2
|
@@ -11,6 +11,7 @@ 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";
|
|
14
15
|
const INTERNAL_RUNTIME_PLATFORM_ID = "second-nature-runtime";
|
|
15
16
|
const INTERNAL_RUNTIME_TRACE_PREFIX = "sn-runtime-";
|
|
16
17
|
function toExplainQuery(subject) {
|
|
@@ -112,6 +113,117 @@ function mapConnectorStatus(attempt) {
|
|
|
112
113
|
}
|
|
113
114
|
return "healthy";
|
|
114
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Derive groundingStatus from confidence and status.
|
|
118
|
+
*
|
|
119
|
+
* Rules (in priority order):
|
|
120
|
+
* 1. blocked: status === "awaiting_sources" OR confidence < 0.4
|
|
121
|
+
* 2. pass: confidence >= 0.7 AND status === "active"
|
|
122
|
+
* 3. degraded: all other cases (0.4 <= confidence < 0.7, or status is insufficient_sources)
|
|
123
|
+
*/
|
|
124
|
+
function deriveGroundingStatus(status, confidence) {
|
|
125
|
+
if (status === "awaiting_sources" || confidence < 0.4) {
|
|
126
|
+
return "blocked";
|
|
127
|
+
}
|
|
128
|
+
if (confidence >= 0.7 && status === "active") {
|
|
129
|
+
return "pass";
|
|
130
|
+
}
|
|
131
|
+
return "degraded";
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Build the base StatusReadModel that is shared by loadStatus and loadV6Status.
|
|
135
|
+
* Centralising this logic eliminates the DRY violation identified in CR-01.
|
|
136
|
+
*/
|
|
137
|
+
async function buildBaseStatus(deps) {
|
|
138
|
+
let recentAttempts = [];
|
|
139
|
+
let recentDecisions = [];
|
|
140
|
+
let credentials = [];
|
|
141
|
+
try {
|
|
142
|
+
recentAttempts = await deps.observabilityDb.db
|
|
143
|
+
.select()
|
|
144
|
+
.from(executionAttempts)
|
|
145
|
+
.orderBy(desc(executionAttempts.startedAt), desc(executionAttempts.finishedAt))
|
|
146
|
+
.limit(50);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
recentAttempts = [];
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
recentDecisions = await deps.observabilityDb.db
|
|
153
|
+
.select()
|
|
154
|
+
.from(decisionLedger)
|
|
155
|
+
.orderBy(desc(decisionLedger.createdAt))
|
|
156
|
+
.limit(50);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
recentDecisions = [];
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
credentials = await deps.stateDb.db.query.credentialRecords.findMany();
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
credentials = [];
|
|
166
|
+
}
|
|
167
|
+
const latestRuntimeAttempt = recentAttempts.find((attempt) => attempt.platformId === INTERNAL_RUNTIME_PLATFORM_ID);
|
|
168
|
+
const latestConnectorAttempt = recentAttempts.find((attempt) => attempt.platformId !== INTERNAL_RUNTIME_PLATFORM_ID);
|
|
169
|
+
const latestRuntimeDecision = recentDecisions.find((decision) => decision.traceId.startsWith(INTERNAL_RUNTIME_TRACE_PREFIX));
|
|
170
|
+
const runtimeUpdatedAt = latestRuntimeAttempt?.finishedAt ??
|
|
171
|
+
latestRuntimeAttempt?.startedAt ??
|
|
172
|
+
latestRuntimeDecision?.createdAt ??
|
|
173
|
+
"";
|
|
174
|
+
const quietMode = latestRuntimeDecision?.mode === "quiet" ||
|
|
175
|
+
latestRuntimeDecision?.mode === "maintenance_only" ||
|
|
176
|
+
latestRuntimeDecision?.mode === "paused_for_interrupt"
|
|
177
|
+
? latestRuntimeDecision.mode
|
|
178
|
+
: "unknown";
|
|
179
|
+
const riskFlags = [
|
|
180
|
+
latestRuntimeAttempt?.failureClass,
|
|
181
|
+
latestConnectorAttempt?.failureClass,
|
|
182
|
+
].filter((value) => Boolean(value));
|
|
183
|
+
const connectorSummary = latestConnectorAttempt
|
|
184
|
+
? [
|
|
185
|
+
{
|
|
186
|
+
platformId: latestConnectorAttempt.platformId,
|
|
187
|
+
status: mapConnectorStatus(latestConnectorAttempt),
|
|
188
|
+
channel: latestConnectorAttempt.channel,
|
|
189
|
+
failureClass: latestConnectorAttempt.failureClass ?? undefined,
|
|
190
|
+
},
|
|
191
|
+
]
|
|
192
|
+
: [];
|
|
193
|
+
return {
|
|
194
|
+
runtime: {
|
|
195
|
+
host: "openclaw-plugin",
|
|
196
|
+
serviceStatus: mapRuntimeStatus(latestRuntimeAttempt),
|
|
197
|
+
updatedAt: runtimeUpdatedAt,
|
|
198
|
+
},
|
|
199
|
+
rhythm: {
|
|
200
|
+
mode: latestRuntimeDecision?.mode ?? "unknown",
|
|
201
|
+
},
|
|
202
|
+
quiet: {
|
|
203
|
+
mode: quietMode,
|
|
204
|
+
lastEvent: latestRuntimeDecision?.traceId,
|
|
205
|
+
interrupted: latestRuntimeDecision?.mode === "paused_for_interrupt"
|
|
206
|
+
? true
|
|
207
|
+
: undefined,
|
|
208
|
+
},
|
|
209
|
+
connectors: connectorSummary,
|
|
210
|
+
credentials: credentials.map((item) => ({
|
|
211
|
+
platformId: item.platformId ??
|
|
212
|
+
item.platform_id,
|
|
213
|
+
status: item.status,
|
|
214
|
+
nextStep: buildCredentialNextStep(item.status),
|
|
215
|
+
})),
|
|
216
|
+
risk: {
|
|
217
|
+
level: riskFlags.length > 0 ? "medium" : "low",
|
|
218
|
+
flags: riskFlags,
|
|
219
|
+
},
|
|
220
|
+
deliveryPosture: {
|
|
221
|
+
verdict: "none",
|
|
222
|
+
source: "workspace_default_none",
|
|
223
|
+
reasonCode: "delivery_target_none",
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
115
227
|
export function createCliReadModels(deps) {
|
|
116
228
|
const assetRepository = new AssetRepository(deps.stateDb);
|
|
117
229
|
const credentialRepository = new CredentialRepository(deps.stateDb);
|
|
@@ -124,103 +236,7 @@ export function createCliReadModels(deps) {
|
|
|
124
236
|
const auditStore = deps.livedExperienceAuditStore ?? new AppendOnlyAuditStore();
|
|
125
237
|
return {
|
|
126
238
|
async loadStatus(_scope) {
|
|
127
|
-
|
|
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
|
-
};
|
|
239
|
+
return buildBaseStatus(deps);
|
|
224
240
|
},
|
|
225
241
|
async loadDailyReport(day) {
|
|
226
242
|
let bundle;
|
|
@@ -387,5 +403,220 @@ export function createCliReadModels(deps) {
|
|
|
387
403
|
evidenceRefs: bundle.explanation.evidenceRefs,
|
|
388
404
|
};
|
|
389
405
|
},
|
|
406
|
+
// T1.2.2 — read recent DreamTrace events from audit store.
|
|
407
|
+
async loadDreamRecent(limit = 5) {
|
|
408
|
+
const events = auditStore.list().filter((e) => e.family === "dream.trace");
|
|
409
|
+
const recent = events
|
|
410
|
+
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
|
411
|
+
.slice(0, limit);
|
|
412
|
+
return {
|
|
413
|
+
runs: recent.map((e) => {
|
|
414
|
+
const p = e.payload;
|
|
415
|
+
return {
|
|
416
|
+
traceId: p.traceId,
|
|
417
|
+
runId: p.runId,
|
|
418
|
+
durationMs: p.durationMs ?? 0,
|
|
419
|
+
inputCounts: p.inputCounts ?? { evidence: 0, chronicle: 0, memoryEntries: 0 },
|
|
420
|
+
fallbackReason: p.fallbackReason,
|
|
421
|
+
lifecycleStatus: p.fallbackReason ? "partial" : "completed",
|
|
422
|
+
insightsCount: 0, // would require deeper payload parsing
|
|
423
|
+
createdAt: e.createdAt,
|
|
424
|
+
};
|
|
425
|
+
}),
|
|
426
|
+
totalRuns: events.length,
|
|
427
|
+
};
|
|
428
|
+
},
|
|
429
|
+
// T1.2.5 — aggregate recent heartbeat, narrative, dream, delivery events into cycles.
|
|
430
|
+
async loadCycleRecent(limit = 5) {
|
|
431
|
+
const events = auditStore.list();
|
|
432
|
+
const decisions = events.filter((e) => e.family === "heartbeat.decision");
|
|
433
|
+
const narratives = events.filter((e) => e.family === "narrative.trace");
|
|
434
|
+
const dreams = events.filter((e) => e.family === "dream.trace");
|
|
435
|
+
const deliveries = events.filter((e) => e.family === "delivery");
|
|
436
|
+
const connectors = events.filter((e) => e.family === "connector.attempt");
|
|
437
|
+
// Group by time buckets (hourly)
|
|
438
|
+
const buckets = new Map();
|
|
439
|
+
for (const e of decisions) {
|
|
440
|
+
const hour = e.createdAt.slice(0, 13);
|
|
441
|
+
const b = buckets.get(hour) ?? { timestamp: `${hour}:00:00Z`, dimensions: [] };
|
|
442
|
+
if (!b.dimensions.includes("decision"))
|
|
443
|
+
b.dimensions.push("decision");
|
|
444
|
+
const p = e.payload;
|
|
445
|
+
if (p.outcome)
|
|
446
|
+
b.decisionOutcome = p.outcome;
|
|
447
|
+
buckets.set(hour, b);
|
|
448
|
+
}
|
|
449
|
+
for (const e of narratives) {
|
|
450
|
+
const hour = e.createdAt.slice(0, 13);
|
|
451
|
+
const b = buckets.get(hour) ?? { timestamp: `${hour}:00:00Z`, dimensions: [] };
|
|
452
|
+
if (!b.dimensions.includes("narrative"))
|
|
453
|
+
b.dimensions.push("narrative");
|
|
454
|
+
const p = e.payload;
|
|
455
|
+
if (p.groundingStatus)
|
|
456
|
+
b.narrativeGrounding = p.groundingStatus;
|
|
457
|
+
buckets.set(hour, b);
|
|
458
|
+
}
|
|
459
|
+
for (const e of dreams) {
|
|
460
|
+
const hour = e.createdAt.slice(0, 13);
|
|
461
|
+
const b = buckets.get(hour) ?? { timestamp: `${hour}:00:00Z`, dimensions: [] };
|
|
462
|
+
if (!b.dimensions.includes("dream"))
|
|
463
|
+
b.dimensions.push("dream");
|
|
464
|
+
const p = e.payload;
|
|
465
|
+
if (p.fallbackReason)
|
|
466
|
+
b.dreamFallback = p.fallbackReason;
|
|
467
|
+
buckets.set(hour, b);
|
|
468
|
+
}
|
|
469
|
+
for (const e of deliveries) {
|
|
470
|
+
const hour = e.createdAt.slice(0, 13);
|
|
471
|
+
const b = buckets.get(hour) ?? { timestamp: `${hour}:00:00Z`, dimensions: [] };
|
|
472
|
+
if (!b.dimensions.includes("delivery"))
|
|
473
|
+
b.dimensions.push("delivery");
|
|
474
|
+
const p = e.payload;
|
|
475
|
+
if (p.status)
|
|
476
|
+
b.deliveryStatus = p.status;
|
|
477
|
+
buckets.set(hour, b);
|
|
478
|
+
}
|
|
479
|
+
for (const e of connectors) {
|
|
480
|
+
const hour = e.createdAt.slice(0, 13);
|
|
481
|
+
const b = buckets.get(hour) ?? { timestamp: `${hour}:00:00Z`, dimensions: [] };
|
|
482
|
+
if (!b.dimensions.includes("connector"))
|
|
483
|
+
b.dimensions.push("connector");
|
|
484
|
+
buckets.set(hour, b);
|
|
485
|
+
}
|
|
486
|
+
const cycles = Array.from(buckets.values())
|
|
487
|
+
.sort((a, b) => b.timestamp.localeCompare(a.timestamp))
|
|
488
|
+
.slice(0, limit);
|
|
489
|
+
return { cycles, totalCycles: buckets.size };
|
|
490
|
+
},
|
|
491
|
+
// T1.2.6 — v6 status aggregate: compose base status + narrative + dream + cycle sections.
|
|
492
|
+
// Each section returns a sentinel status (nothing_yet / has_runs / has_cycles) so operators
|
|
493
|
+
// always get a meaningful non-empty response, never a raw empty object.
|
|
494
|
+
async loadV6Status(scope) {
|
|
495
|
+
// Load NarrativeState asynchronously; audit events are synchronous reads from in-memory store.
|
|
496
|
+
const narrativeStore = createNarrativeStateStore(deps.stateDb);
|
|
497
|
+
let narrativeState;
|
|
498
|
+
try {
|
|
499
|
+
narrativeState = await narrativeStore.loadNarrativeState();
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
narrativeState = null;
|
|
503
|
+
}
|
|
504
|
+
const allAuditEvents = auditStore.list();
|
|
505
|
+
const dreamSection = allAuditEvents.filter((e) => e.family === "dream.trace");
|
|
506
|
+
const cycleSection = allAuditEvents;
|
|
507
|
+
const baseStatus = await buildBaseStatus(deps);
|
|
508
|
+
// Narrative section
|
|
509
|
+
let narrativeSectionOut;
|
|
510
|
+
if (!narrativeState) {
|
|
511
|
+
narrativeSectionOut = { status: "nothing_yet", focus: "", groundingStatus: "blocked", nextIntent: "", sourceRefCount: 0 };
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
const groundingStatus = deriveGroundingStatus(narrativeState.status, narrativeState.confidence);
|
|
515
|
+
narrativeSectionOut = { status: narrativeState.status, focus: narrativeState.focus, groundingStatus, nextIntent: narrativeState.nextIntent, sourceRefCount: narrativeState.sourceRefs.length };
|
|
516
|
+
}
|
|
517
|
+
// Dream section — degraded when all recorded dream runs have a fallbackReason.
|
|
518
|
+
const dreamEvents = dreamSection;
|
|
519
|
+
let dreamSectionOut;
|
|
520
|
+
if (dreamEvents.length === 0) {
|
|
521
|
+
dreamSectionOut = { status: "nothing_yet", totalRuns: 0, recentRunCount: 0 };
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
const recentDreams = dreamEvents.sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, 3);
|
|
525
|
+
const lastFallback = recentDreams.map((e) => e.payload.fallbackReason).find(Boolean);
|
|
526
|
+
const allDegraded = dreamEvents.every((e) => !!e.payload.fallbackReason);
|
|
527
|
+
dreamSectionOut = {
|
|
528
|
+
status: allDegraded ? "degraded" : "has_runs",
|
|
529
|
+
totalRuns: dreamEvents.length,
|
|
530
|
+
recentRunCount: recentDreams.length,
|
|
531
|
+
lastFallbackReason: lastFallback,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
// Cycle section — degraded when buckets exist but cover fewer than 3 dimensions.
|
|
535
|
+
const allEvents = cycleSection;
|
|
536
|
+
const decisionEvents = allEvents.filter((e) => e.family === "heartbeat.decision");
|
|
537
|
+
const narrativeEvents = allEvents.filter((e) => e.family === "narrative.trace");
|
|
538
|
+
const dreamEventsForCycle = allEvents.filter((e) => e.family === "dream.trace");
|
|
539
|
+
const deliveryEvents = allEvents.filter((e) => e.family === "delivery");
|
|
540
|
+
const connectorEvents = allEvents.filter((e) => e.family === "connector.attempt");
|
|
541
|
+
const hourBuckets = new Set();
|
|
542
|
+
const dimensionSet = new Set();
|
|
543
|
+
for (const e of decisionEvents) {
|
|
544
|
+
hourBuckets.add(e.createdAt.slice(0, 13));
|
|
545
|
+
dimensionSet.add("decision");
|
|
546
|
+
}
|
|
547
|
+
for (const e of narrativeEvents) {
|
|
548
|
+
hourBuckets.add(e.createdAt.slice(0, 13));
|
|
549
|
+
dimensionSet.add("narrative");
|
|
550
|
+
}
|
|
551
|
+
for (const e of dreamEventsForCycle) {
|
|
552
|
+
hourBuckets.add(e.createdAt.slice(0, 13));
|
|
553
|
+
dimensionSet.add("dream");
|
|
554
|
+
}
|
|
555
|
+
for (const e of deliveryEvents) {
|
|
556
|
+
hourBuckets.add(e.createdAt.slice(0, 13));
|
|
557
|
+
dimensionSet.add("delivery");
|
|
558
|
+
}
|
|
559
|
+
for (const e of connectorEvents) {
|
|
560
|
+
hourBuckets.add(e.createdAt.slice(0, 13));
|
|
561
|
+
dimensionSet.add("connector");
|
|
562
|
+
}
|
|
563
|
+
let cycleSectionOut;
|
|
564
|
+
if (hourBuckets.size === 0) {
|
|
565
|
+
cycleSectionOut = { status: "nothing_yet", totalCycles: 0, recentCycleCount: 0, dimensions: [] };
|
|
566
|
+
}
|
|
567
|
+
else if (dimensionSet.size < 3) {
|
|
568
|
+
cycleSectionOut = { status: "degraded", totalCycles: hourBuckets.size, recentCycleCount: Math.min(hourBuckets.size, 5), dimensions: Array.from(dimensionSet) };
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
cycleSectionOut = { status: "has_cycles", totalCycles: hourBuckets.size, recentCycleCount: Math.min(hourBuckets.size, 5), dimensions: Array.from(dimensionSet) };
|
|
572
|
+
}
|
|
573
|
+
void scope; // scope param reserved for future scoping — not used in v6 aggregate yet
|
|
574
|
+
return { ...baseStatus, narrative: narrativeSectionOut, dream: dreamSectionOut, cycles: cycleSectionOut };
|
|
575
|
+
},
|
|
576
|
+
// T1.2.1 — read current NarrativeState and map to NarrativeReadModel.
|
|
577
|
+
// Returns `nothing_yet` status when no data exists — honest empty, not an error.
|
|
578
|
+
async loadNarrative(narrativeId) {
|
|
579
|
+
const narrativeStore = createNarrativeStateStore(deps.stateDb);
|
|
580
|
+
let state;
|
|
581
|
+
try {
|
|
582
|
+
state = await narrativeStore.loadNarrativeState(narrativeId);
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
state = null;
|
|
586
|
+
}
|
|
587
|
+
if (!state) {
|
|
588
|
+
return {
|
|
589
|
+
narrativeId: narrativeId ?? "default",
|
|
590
|
+
revision: 0,
|
|
591
|
+
focus: "",
|
|
592
|
+
progress: [],
|
|
593
|
+
nextIntent: "",
|
|
594
|
+
confidence: 0,
|
|
595
|
+
sourceRefs: [],
|
|
596
|
+
unsupportedClaims: [],
|
|
597
|
+
groundingStatus: "blocked",
|
|
598
|
+
status: "nothing_yet",
|
|
599
|
+
updatedAt: "",
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
const groundingStatus = deriveGroundingStatus(state.status, state.confidence);
|
|
603
|
+
return {
|
|
604
|
+
narrativeId: state.narrativeId,
|
|
605
|
+
revision: state.revision,
|
|
606
|
+
focus: state.focus,
|
|
607
|
+
progress: state.progress,
|
|
608
|
+
nextIntent: state.nextIntent,
|
|
609
|
+
confidence: state.confidence,
|
|
610
|
+
sourceRefs: state.sourceRefs.map((r) => ({
|
|
611
|
+
sourceId: r.sourceId,
|
|
612
|
+
kind: r.kind,
|
|
613
|
+
url: r.url,
|
|
614
|
+
})),
|
|
615
|
+
unsupportedClaims: state.unsupportedClaims,
|
|
616
|
+
groundingStatus,
|
|
617
|
+
status: state.status,
|
|
618
|
+
updatedAt: state.updatedAt,
|
|
619
|
+
};
|
|
620
|
+
},
|
|
390
621
|
};
|
|
391
622
|
}
|
|
@@ -73,6 +73,38 @@ export interface StatusReadModel {
|
|
|
73
73
|
*/
|
|
74
74
|
deliveryPosture?: DeliveryPosture;
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* T1.2.6 — v6 status aggregate: extends StatusReadModel with narrative, dream recent,
|
|
78
|
+
* cycle recent, and per-section `nothing_yet` / `awaiting_sources` / `degraded` sentinels
|
|
79
|
+
* so operators always get a meaningful view even when individual data sources are empty.
|
|
80
|
+
*/
|
|
81
|
+
export interface StatusV6NarrativeSection {
|
|
82
|
+
status: "active" | "insufficient_sources" | "awaiting_sources" | "nothing_yet";
|
|
83
|
+
focus: string;
|
|
84
|
+
groundingStatus: "pass" | "degraded" | "blocked";
|
|
85
|
+
nextIntent: string;
|
|
86
|
+
sourceRefCount: number;
|
|
87
|
+
}
|
|
88
|
+
export interface StatusV6DreamSection {
|
|
89
|
+
status: "has_runs" | "degraded" | "nothing_yet";
|
|
90
|
+
totalRuns: number;
|
|
91
|
+
recentRunCount: number;
|
|
92
|
+
lastFallbackReason?: string;
|
|
93
|
+
}
|
|
94
|
+
export interface StatusV6CycleSection {
|
|
95
|
+
status: "has_cycles" | "degraded" | "nothing_yet";
|
|
96
|
+
totalCycles: number;
|
|
97
|
+
recentCycleCount: number;
|
|
98
|
+
dimensions: string[];
|
|
99
|
+
}
|
|
100
|
+
export interface StatusV6ReadModel extends StatusReadModel {
|
|
101
|
+
/** v6 narrative section; status is nothing_yet when no NarrativeState row exists. */
|
|
102
|
+
narrative: StatusV6NarrativeSection;
|
|
103
|
+
/** v6 dream recent section; status is nothing_yet when no DreamTrace events exist. */
|
|
104
|
+
dream: StatusV6DreamSection;
|
|
105
|
+
/** v6 cycle recent section; status is nothing_yet when no cycle events exist. */
|
|
106
|
+
cycles: StatusV6CycleSection;
|
|
107
|
+
}
|
|
76
108
|
export interface DailyReportReadModel {
|
|
77
109
|
day: string;
|
|
78
110
|
summary: string;
|
|
@@ -127,3 +159,51 @@ export interface AuditSummaryReadModel {
|
|
|
127
159
|
totalEvents: number;
|
|
128
160
|
events: AuditEventSummaryEntry[];
|
|
129
161
|
}
|
|
162
|
+
/** T1.2.2 — recent Dream run summary for operator `dream:recent` command. */
|
|
163
|
+
export interface DreamRecentReadModel {
|
|
164
|
+
runs: Array<{
|
|
165
|
+
traceId: string;
|
|
166
|
+
runId: string;
|
|
167
|
+
durationMs: number;
|
|
168
|
+
inputCounts: {
|
|
169
|
+
evidence: number;
|
|
170
|
+
chronicle: number;
|
|
171
|
+
memoryEntries: number;
|
|
172
|
+
};
|
|
173
|
+
fallbackReason?: string;
|
|
174
|
+
lifecycleStatus: string;
|
|
175
|
+
insightsCount: number;
|
|
176
|
+
createdAt: string;
|
|
177
|
+
}>;
|
|
178
|
+
totalRuns: number;
|
|
179
|
+
}
|
|
180
|
+
/** T1.2.1 — NarrativeState read model for operator `narrative` command. */
|
|
181
|
+
export interface NarrativeReadModel {
|
|
182
|
+
narrativeId: string;
|
|
183
|
+
revision: number;
|
|
184
|
+
focus: string;
|
|
185
|
+
progress: string[];
|
|
186
|
+
nextIntent: string;
|
|
187
|
+
confidence: number;
|
|
188
|
+
sourceRefs: Array<{
|
|
189
|
+
sourceId: string;
|
|
190
|
+
kind: string;
|
|
191
|
+
url?: string;
|
|
192
|
+
}>;
|
|
193
|
+
unsupportedClaims: string[];
|
|
194
|
+
groundingStatus: "pass" | "degraded" | "blocked";
|
|
195
|
+
status: "active" | "insufficient_sources" | "awaiting_sources" | "nothing_yet";
|
|
196
|
+
updatedAt: string;
|
|
197
|
+
}
|
|
198
|
+
/** T1.2.5 — recent cycle summary aggregating heartbeat, narrative, dream, delivery. */
|
|
199
|
+
export interface CycleRecentReadModel {
|
|
200
|
+
cycles: Array<{
|
|
201
|
+
timestamp: string;
|
|
202
|
+
dimensions: Array<"decision" | "narrative" | "dream" | "delivery" | "connector">;
|
|
203
|
+
decisionOutcome?: string;
|
|
204
|
+
narrativeGrounding?: string;
|
|
205
|
+
dreamFallback?: string;
|
|
206
|
+
deliveryStatus?: string;
|
|
207
|
+
}>;
|
|
208
|
+
totalCycles: number;
|
|
209
|
+
}
|
|
@@ -21,6 +21,7 @@ import type { StateDatabase } from "../../../storage/db/index.js";
|
|
|
21
21
|
import { type OpenClawDeliveryPort } from "../outreach/dispatch-user-outreach.js";
|
|
22
22
|
import type { ConnectorExecutor } from "../../../connectors/base/contract.js";
|
|
23
23
|
import type { NarrativeStateStore } from "../../../storage/narrative/narrative-state-store.js";
|
|
24
|
+
import type { NarrativeTracePayload } from "../../../observability/services/lived-experience-audit.js";
|
|
24
25
|
export interface HeartbeatDecisionTracePayload {
|
|
25
26
|
scope: RuntimeScope;
|
|
26
27
|
status: HeartbeatCycleStatus;
|
|
@@ -61,6 +62,8 @@ export interface HeartbeatDeps {
|
|
|
61
62
|
connectorExecutor?: ConnectorExecutor;
|
|
62
63
|
/** T2.1.5: when present, heartbeat writes a source-backed NarrativeState revision after each cycle. */
|
|
63
64
|
narrativeStateStore?: NarrativeStateStore;
|
|
65
|
+
/** T5.1.2: when present, heartbeat records a NarrativeTrace after successful narrative state update. */
|
|
66
|
+
recordNarrativeTrace?: (payload: NarrativeTracePayload) => Promise<void>;
|
|
64
67
|
}
|
|
65
68
|
/**
|
|
66
69
|
* Ingest a heartbeat rhythm signal and drive one full decision round.
|
|
@@ -91,7 +91,7 @@ export async function resolveAllowedIntentResult(intent, runtime, inputs, signal
|
|
|
91
91
|
* is never blocked by a store failure. Store failures are optionally traced
|
|
92
92
|
* via recordDecisionTrace so operators can monitor store health.
|
|
93
93
|
*/
|
|
94
|
-
async function maybeUpdateNarrativeState(result, selectedIntent, runtime, store, recordTrace, signal) {
|
|
94
|
+
async function maybeUpdateNarrativeState(result, selectedIntent, runtime, store, recordTrace, signal, recordNarrativeTrace) {
|
|
95
95
|
if (!store)
|
|
96
96
|
return;
|
|
97
97
|
try {
|
|
@@ -103,6 +103,33 @@ async function maybeUpdateNarrativeState(result, selectedIntent, runtime, store,
|
|
|
103
103
|
priorNarrative: prior,
|
|
104
104
|
});
|
|
105
105
|
await store.updateNarrativeState(update);
|
|
106
|
+
// T5.1.2: record NarrativeTrace on successful state update
|
|
107
|
+
if (recordNarrativeTrace) {
|
|
108
|
+
try {
|
|
109
|
+
await recordNarrativeTrace({
|
|
110
|
+
traceId: `narrative_trace:${crypto.randomUUID()}`,
|
|
111
|
+
narrativeId: update.narrativeId,
|
|
112
|
+
revision: update.revision,
|
|
113
|
+
updateSource: "heartbeat",
|
|
114
|
+
sourceRefs: update.sourceRefs.map((r) => ({
|
|
115
|
+
id: r.sourceId,
|
|
116
|
+
kind: r.kind,
|
|
117
|
+
uri: r.url,
|
|
118
|
+
})),
|
|
119
|
+
unsupportedClaims: update.unsupportedClaims,
|
|
120
|
+
groundingStatus: update.unsupportedClaims.length > 0
|
|
121
|
+
? "degraded"
|
|
122
|
+
: update.status === "insufficient_sources"
|
|
123
|
+
? "blocked"
|
|
124
|
+
: "pass",
|
|
125
|
+
goalInfluenceRefs: selectedIntent?.goalInfluenceRefs ?? [],
|
|
126
|
+
createdAt: update.updatedAt,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// trace emission must not block the cycle
|
|
131
|
+
}
|
|
132
|
+
}
|
|
106
133
|
}
|
|
107
134
|
catch {
|
|
108
135
|
// degrade silently; narrative update is best-effort
|
|
@@ -178,7 +205,7 @@ export async function ingestRhythmSignal(signal, deps) {
|
|
|
178
205
|
? { ...resolved, reasons: evaluation.reasons }
|
|
179
206
|
: resolved;
|
|
180
207
|
await emitTrace(result);
|
|
181
|
-
await maybeUpdateNarrativeState(result, intent, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal);
|
|
208
|
+
await maybeUpdateNarrativeState(result, intent, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal, deps.recordNarrativeTrace);
|
|
182
209
|
return result;
|
|
183
210
|
}
|
|
184
211
|
if (evaluation.verdict === "defer") {
|
|
@@ -196,7 +223,7 @@ export async function ingestRhythmSignal(signal, deps) {
|
|
|
196
223
|
reasons: ["silent_no_candidates"],
|
|
197
224
|
};
|
|
198
225
|
await emitTrace(result);
|
|
199
|
-
await maybeUpdateNarrativeState(result, undefined, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal);
|
|
226
|
+
await maybeUpdateNarrativeState(result, undefined, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal, deps.recordNarrativeTrace);
|
|
200
227
|
return result;
|
|
201
228
|
}
|
|
202
229
|
if (!anyAllow && anyDefer && !anyDeny) {
|
|
@@ -206,7 +233,7 @@ export async function ingestRhythmSignal(signal, deps) {
|
|
|
206
233
|
reasons: denyReasons.length > 0 ? denyReasons : ["all_candidates_deferred"],
|
|
207
234
|
};
|
|
208
235
|
await emitTrace(result);
|
|
209
|
-
await maybeUpdateNarrativeState(result, undefined, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal);
|
|
236
|
+
await maybeUpdateNarrativeState(result, undefined, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal, deps.recordNarrativeTrace);
|
|
210
237
|
return result;
|
|
211
238
|
}
|
|
212
239
|
if (!anyAllow && denyReasons.length > 0) {
|
|
@@ -216,7 +243,7 @@ export async function ingestRhythmSignal(signal, deps) {
|
|
|
216
243
|
reasons: denyReasons,
|
|
217
244
|
};
|
|
218
245
|
await emitTrace(result);
|
|
219
|
-
await maybeUpdateNarrativeState(result, undefined, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal);
|
|
246
|
+
await maybeUpdateNarrativeState(result, undefined, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal, deps.recordNarrativeTrace);
|
|
220
247
|
return result;
|
|
221
248
|
}
|
|
222
249
|
const result = {
|
|
@@ -225,7 +252,7 @@ export async function ingestRhythmSignal(signal, deps) {
|
|
|
225
252
|
reasons: ["no_allow_verdict"],
|
|
226
253
|
};
|
|
227
254
|
await emitTrace(result);
|
|
228
|
-
await maybeUpdateNarrativeState(result, undefined, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal);
|
|
255
|
+
await maybeUpdateNarrativeState(result, undefined, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal, deps.recordNarrativeTrace);
|
|
229
256
|
return result;
|
|
230
257
|
}
|
|
231
258
|
/**
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `applyGoalPriority` adjusts candidate intent priorities based on accepted AgentGoals.
|
|
5
5
|
* Priority order: user_task > accepted_goal > rhythm.
|
|
6
|
-
* Only goals with status === "accepted"
|
|
6
|
+
* Only goals with status === "accepted" are considered.
|
|
7
|
+
* Agent-proposed goals are included ONLY if policy-accepted (acceptedBy === "policy_allowlist").
|
|
7
8
|
* All other statuses (proposal / rejected / completed / paused) are implicitly excluded.
|
|
8
9
|
*/
|
|
9
10
|
import type { CandidateIntent } from "../types.js";
|