@haaaiawd/second-nature 0.2.6 → 0.2.8
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 +9 -0
- package/openclaw.plugin.json +2 -2
- package/package.json +1 -1
- package/runtime/cli/commands/index.js +9 -0
- package/runtime/cli/ops/heartbeat-surface.js +1 -0
- package/runtime/connectors/base/normalized-evidence-content.d.ts +71 -0
- package/runtime/connectors/base/normalized-evidence-content.js +273 -0
- package/runtime/connectors/evidence-normalizer.d.ts +4 -3
- package/runtime/connectors/evidence-normalizer.js +128 -23
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +15 -0
- package/runtime/core/second-nature/perception/perception-builder.d.ts +3 -1
- package/runtime/core/second-nature/perception/perception-builder.js +98 -44
- package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.d.ts +6 -3
- package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.js +198 -22
- package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.d.ts +19 -3
- package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.js +189 -12
- package/runtime/shared/types/v8-contracts.d.ts +1 -1
- package/runtime/storage/db/index.js +2 -0
- package/runtime/storage/db/migrations/v8-003-quiet-closure-refs.js +2 -1
- package/runtime/storage/services/write-validation-gate.d.ts +2 -0
- package/runtime/storage/services/write-validation-gate.js +69 -17
- package/runtime/storage/v8-state-stores.d.ts +25 -0
- package/runtime/storage/v8-state-stores.js +87 -1
|
@@ -10,6 +10,7 @@ import { toCapabilityIntent } from "../orchestrator/effect-dispatcher.js";
|
|
|
10
10
|
import { updateNarrativeAfterEffect } from "../orchestrator/narrative-update.js";
|
|
11
11
|
import { mapLifeEvidence } from "../../../connectors/base/map-life-evidence.js";
|
|
12
12
|
import { appendLifeEvidence } from "../../../storage/life-evidence/append-life-evidence.js";
|
|
13
|
+
import { normalizeConnectorEvidence } from "../../../connectors/evidence-normalizer.js";
|
|
13
14
|
import { recordConnectorAttemptAudit } from "../../../observability/services/audit-closure-recorders.js";
|
|
14
15
|
/**
|
|
15
16
|
* Resolves the heartbeat outcome for a guard-allowed intent (outreach dispatch, quiet orchestration, or default).
|
|
@@ -116,6 +117,20 @@ export async function resolveAllowedIntentResult(intent, runtime, inputs, signal
|
|
|
116
117
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
117
118
|
console.warn(`[heartbeat] evidence append failed for ${intent.platformId ?? "unknown"}: ${errorMessage}`);
|
|
118
119
|
}
|
|
120
|
+
// Wave 109 T-CS.R.5: also normalize into v8 EvidenceItem with content-bearing payload.
|
|
121
|
+
try {
|
|
122
|
+
await normalizeConnectorEvidence(deps.state, {
|
|
123
|
+
status: "success",
|
|
124
|
+
platformId: intent.platformId,
|
|
125
|
+
capabilityId: toCapabilityIntent(intent),
|
|
126
|
+
data: result.data,
|
|
127
|
+
observedAt: new Date().toISOString(),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
132
|
+
console.warn(`[heartbeat] v8 evidence normalization failed for ${intent.platformId ?? "unknown"}: ${errorMessage}`);
|
|
133
|
+
}
|
|
119
134
|
}
|
|
120
135
|
// v7 T-V7C.C.2: record ToolExperience for all connector attempts in heartbeat.
|
|
121
136
|
if (deps.experienceWriter) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PerceptionBuilder — Generate PerceptionCard records from EvidenceItem batches.
|
|
3
3
|
*
|
|
4
|
-
* Core logic: Read pending evidence, deduplicate by content hash, build
|
|
4
|
+
* Core logic: Read pending evidence, deduplicate by externalId/content hash, build
|
|
5
5
|
* PerceptionCard with topic, entities, novelty, relevance, summary, risk
|
|
6
6
|
* flags, confidence, and reviewPriority. Rules-only fallback when model
|
|
7
7
|
* assist is unavailable.
|
|
@@ -51,6 +51,8 @@ export interface PerceptionCardResult {
|
|
|
51
51
|
confidence: number;
|
|
52
52
|
evidenceRefs: SourceRef[];
|
|
53
53
|
createdAt: string;
|
|
54
|
+
/** True when the evidence payload lacked readable content and only refs are present. */
|
|
55
|
+
contentMissing?: boolean;
|
|
54
56
|
}
|
|
55
57
|
export interface BuildPerceptionCardsResult {
|
|
56
58
|
status: "completed" | "rules_only" | "blocked" | "empty" | "degraded";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PerceptionBuilder — Generate PerceptionCard records from EvidenceItem batches.
|
|
3
3
|
*
|
|
4
|
-
* Core logic: Read pending evidence, deduplicate by content hash, build
|
|
4
|
+
* Core logic: Read pending evidence, deduplicate by externalId/content hash, build
|
|
5
5
|
* PerceptionCard with topic, entities, novelty, relevance, summary, risk
|
|
6
6
|
* flags, confidence, and reviewPriority. Rules-only fallback when model
|
|
7
7
|
* assist is unavailable.
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*
|
|
22
22
|
* Test coverage: tests/unit/perception/perception-builder.test.ts
|
|
23
23
|
*/
|
|
24
|
-
import { readEvidenceItemsByStatus, writePerceptionCard, } from "../../../storage/v8-state-stores.js";
|
|
24
|
+
import { readEvidenceItemsByStatus, writePerceptionCard, updateEvidenceItemLifecycleStatus, } from "../../../storage/v8-state-stores.js";
|
|
25
25
|
// ───────────────────────────────────────────────────────────────
|
|
26
26
|
// Config
|
|
27
27
|
// ───────────────────────────────────────────────────────────────
|
|
@@ -38,46 +38,64 @@ function parseSourceRefs(json) {
|
|
|
38
38
|
return [];
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
function
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return String(payload.title);
|
|
49
|
-
if (payload.subject)
|
|
50
|
-
return String(payload.subject);
|
|
41
|
+
function parsePayload(json) {
|
|
42
|
+
if (!json)
|
|
43
|
+
return undefined;
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(json);
|
|
46
|
+
if (parsed && typeof parsed === "object" && "schemaVersion" in parsed) {
|
|
47
|
+
return parsed;
|
|
51
48
|
}
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
if (parsed && typeof parsed === "object" && "summary" in parsed) {
|
|
50
|
+
// Legacy payload or direct summary object
|
|
51
|
+
return {
|
|
52
|
+
schemaVersion: 1,
|
|
53
|
+
sourceKind: "unknown",
|
|
54
|
+
platformId: "",
|
|
55
|
+
capabilityId: "",
|
|
56
|
+
summary: String(parsed.summary ?? ""),
|
|
57
|
+
observedAt: new Date().toISOString(),
|
|
58
|
+
summaryProducer: "connector_rules",
|
|
59
|
+
...parsed,
|
|
60
|
+
};
|
|
54
61
|
}
|
|
55
62
|
}
|
|
63
|
+
catch {
|
|
64
|
+
/* ignore */
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
function extractTopic(evidence) {
|
|
69
|
+
const payload = parsePayload(evidence.payloadJson);
|
|
70
|
+
if (payload?.title)
|
|
71
|
+
return String(payload.title);
|
|
72
|
+
if (payload?.summary) {
|
|
73
|
+
const summary = String(payload.summary);
|
|
74
|
+
return summary.length > 60 ? `${summary.slice(0, 60)}…` : summary;
|
|
75
|
+
}
|
|
56
76
|
return `${evidence.platformId}_observation`;
|
|
57
77
|
}
|
|
58
78
|
function extractEntities(evidence) {
|
|
59
79
|
const entities = [evidence.platformId];
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (payload.entities && Array.isArray(payload.entities)) {
|
|
64
|
-
entities.push(...payload.entities.map(String));
|
|
65
|
-
}
|
|
66
|
-
if (payload.tags && Array.isArray(payload.tags)) {
|
|
67
|
-
entities.push(...payload.tags.map(String));
|
|
68
|
-
}
|
|
69
|
-
if (payload.mentions && Array.isArray(payload.mentions)) {
|
|
70
|
-
entities.push(...payload.mentions.map(String));
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
/* ignore */
|
|
75
|
-
}
|
|
80
|
+
const payload = parsePayload(evidence.payloadJson);
|
|
81
|
+
if (payload?.entities && Array.isArray(payload.entities)) {
|
|
82
|
+
entities.push(...payload.entities.map(String));
|
|
76
83
|
}
|
|
77
|
-
|
|
84
|
+
if (payload?.tags && Array.isArray(payload.tags)) {
|
|
85
|
+
entities.push(...payload.tags.map(String));
|
|
86
|
+
}
|
|
87
|
+
if (payload?.actor?.displayName) {
|
|
88
|
+
entities.push(payload.actor.displayName);
|
|
89
|
+
}
|
|
90
|
+
return [...new Set(entities.filter((e) => e.length > 0))];
|
|
78
91
|
}
|
|
79
|
-
function inferNoveltyClass(evidence) {
|
|
80
|
-
|
|
92
|
+
function inferNoveltyClass(evidence, duplicateKey, seenKeys) {
|
|
93
|
+
if (seenKeys.has(duplicateKey)) {
|
|
94
|
+
const firstObserved = seenKeys.get(duplicateKey);
|
|
95
|
+
const current = evidence.observedAt;
|
|
96
|
+
// If same calendar day, treat as duplicate; otherwise stale.
|
|
97
|
+
return firstObserved.slice(0, 10) === current.slice(0, 10) ? "duplicate" : "stale";
|
|
98
|
+
}
|
|
81
99
|
if (evidence.sensitivityHint === "public_technical")
|
|
82
100
|
return "changed";
|
|
83
101
|
return "new";
|
|
@@ -99,9 +117,17 @@ function inferRelevanceClass(score) {
|
|
|
99
117
|
return "low";
|
|
100
118
|
}
|
|
101
119
|
function inferSummary(evidence) {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
120
|
+
const payload = parsePayload(evidence.payloadJson);
|
|
121
|
+
if (payload?.summary && String(payload.summary).trim().length > 0) {
|
|
122
|
+
return { summary: String(payload.summary), contentMissing: false };
|
|
123
|
+
}
|
|
124
|
+
if (payload?.title) {
|
|
125
|
+
return { summary: `Observation from ${evidence.platformId}: ${payload.title}`, contentMissing: false };
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
summary: `Ref-only observation from ${evidence.platformId}: no readable content`,
|
|
129
|
+
contentMissing: true,
|
|
130
|
+
};
|
|
105
131
|
}
|
|
106
132
|
function inferPossibleIntents(evidence) {
|
|
107
133
|
const intents = ["watch"];
|
|
@@ -130,25 +156,35 @@ function inferRiskFlags(evidence) {
|
|
|
130
156
|
}
|
|
131
157
|
return flags;
|
|
132
158
|
}
|
|
133
|
-
function
|
|
159
|
+
function duplicateKey(evidence) {
|
|
160
|
+
const payload = parsePayload(evidence.payloadJson);
|
|
161
|
+
if (payload?.externalId) {
|
|
162
|
+
return `${evidence.platformId}:${evidence.contentHash}:${payload.externalId}`;
|
|
163
|
+
}
|
|
164
|
+
return `${evidence.platformId}:${evidence.contentHash}`;
|
|
165
|
+
}
|
|
166
|
+
function buildCardFromEvidence(evidence, cycleId, now, seenKeys) {
|
|
134
167
|
const sourceRefs = parseSourceRefs(evidence.sourceRefsJson);
|
|
135
168
|
const relevanceScore = inferRelevanceScore(evidence);
|
|
169
|
+
const key = duplicateKey(evidence);
|
|
170
|
+
const { summary, contentMissing } = inferSummary(evidence);
|
|
136
171
|
return {
|
|
137
172
|
id: `per_${evidence.id}`,
|
|
138
173
|
cycleId,
|
|
139
174
|
topic: extractTopic(evidence),
|
|
140
175
|
entities: extractEntities(evidence),
|
|
141
|
-
noveltyClass: inferNoveltyClass(evidence),
|
|
176
|
+
noveltyClass: inferNoveltyClass(evidence, key, seenKeys),
|
|
142
177
|
relevanceScore,
|
|
143
178
|
relevanceClass: inferRelevanceClass(relevanceScore),
|
|
144
|
-
summary
|
|
179
|
+
summary,
|
|
145
180
|
possibleIntents: inferPossibleIntents(evidence),
|
|
146
181
|
reviewPriority: inferReviewPriority(evidence),
|
|
147
182
|
sensitivityClass: evidence.sensitivityHint || "public_general",
|
|
148
183
|
riskFlags: inferRiskFlags(evidence),
|
|
149
|
-
confidence: 0.6,
|
|
184
|
+
confidence: contentMissing ? 0.3 : 0.6,
|
|
150
185
|
evidenceRefs: sourceRefs,
|
|
151
186
|
createdAt: now,
|
|
187
|
+
contentMissing,
|
|
152
188
|
};
|
|
153
189
|
}
|
|
154
190
|
export async function buildPerceptionCards(db, options) {
|
|
@@ -169,6 +205,7 @@ export async function buildPerceptionCards(db, options) {
|
|
|
169
205
|
const truncated = evidenceItems.length > maxEvidence;
|
|
170
206
|
const selectedEvidence = evidenceItems.slice(0, maxEvidence);
|
|
171
207
|
const cards = [];
|
|
208
|
+
const seenKeys = new Map();
|
|
172
209
|
for (const evidence of selectedEvidence) {
|
|
173
210
|
const card = buildCardFromEvidence({
|
|
174
211
|
id: evidence.id,
|
|
@@ -178,9 +215,21 @@ export async function buildPerceptionCards(db, options) {
|
|
|
178
215
|
sensitivityHint: evidence.sensitivityHint ?? undefined,
|
|
179
216
|
sourceRefsJson: evidence.sourceRefsJson,
|
|
180
217
|
payloadJson: evidence.payloadJson,
|
|
181
|
-
}, options.cycleId, now);
|
|
218
|
+
}, options.cycleId, now, seenKeys);
|
|
182
219
|
cards.push(card);
|
|
183
|
-
//
|
|
220
|
+
// Track first observation timestamp for duplicate/stale classification
|
|
221
|
+
const key = duplicateKey({
|
|
222
|
+
id: evidence.id,
|
|
223
|
+
platformId: evidence.platformId,
|
|
224
|
+
contentHash: evidence.contentHash,
|
|
225
|
+
observedAt: evidence.observedAt,
|
|
226
|
+
sourceRefsJson: evidence.sourceRefsJson,
|
|
227
|
+
payloadJson: evidence.payloadJson,
|
|
228
|
+
});
|
|
229
|
+
if (!seenKeys.has(key)) {
|
|
230
|
+
seenKeys.set(key, evidence.observedAt);
|
|
231
|
+
}
|
|
232
|
+
// Write card to state and advance evidence lifecycle
|
|
184
233
|
const writeResult = await writePerceptionCard(db, {
|
|
185
234
|
id: card.id,
|
|
186
235
|
createdAt: now,
|
|
@@ -200,6 +249,7 @@ export async function buildPerceptionCards(db, options) {
|
|
|
200
249
|
payloadJson: JSON.stringify({
|
|
201
250
|
possibleIntents: card.possibleIntents,
|
|
202
251
|
sensitivityClass: card.sensitivityClass,
|
|
252
|
+
contentMissing: card.contentMissing,
|
|
203
253
|
}),
|
|
204
254
|
});
|
|
205
255
|
if ("reason" in writeResult) {
|
|
@@ -209,11 +259,15 @@ export async function buildPerceptionCards(db, options) {
|
|
|
209
259
|
reason: writeResult.reason,
|
|
210
260
|
};
|
|
211
261
|
}
|
|
262
|
+
await updateEvidenceItemLifecycleStatus(db, evidence.id, "perceived");
|
|
212
263
|
}
|
|
264
|
+
const hasContentMissing = cards.some((c) => c.contentMissing);
|
|
265
|
+
const allContentMissing = cards.length > 0 && cards.every((c) => c.contentMissing);
|
|
266
|
+
const status = allContentMissing ? "rules_only" : "completed";
|
|
213
267
|
return {
|
|
214
|
-
status
|
|
268
|
+
status,
|
|
215
269
|
cards,
|
|
216
270
|
truncated,
|
|
217
|
-
reason: truncated ? "evidence_batch_truncated" : undefined,
|
|
271
|
+
reason: status === "rules_only" ? "evidence_content_missing" : (truncated ? "evidence_batch_truncated" : undefined),
|
|
218
272
|
};
|
|
219
273
|
}
|
|
@@ -4,19 +4,22 @@
|
|
|
4
4
|
* Core logic: Check if today's Quiet review is due (closures exist but no review
|
|
5
5
|
* yet), schedule/run it, then check Dream status. Records durable states so
|
|
6
6
|
* loop_status can report exact missing stages even when heartbeat does not
|
|
7
|
-
* select a quiet intent.
|
|
7
|
+
* select a quiet intent. Also executes stale Dream consolidation runs that
|
|
8
|
+
* were left at "scheduled" because no runner picked them up.
|
|
8
9
|
*
|
|
9
10
|
* Design authority:
|
|
10
11
|
* - `.anws/v8/04_SYSTEM_DESIGN/dream-quiet-memory-system.md §4`
|
|
11
12
|
* - `.anws/v8/04_SYSTEM_DESIGN/dream-quiet-memory-system.detail.md §3.1-§3.4`
|
|
12
13
|
*
|
|
13
14
|
* Dependencies:
|
|
14
|
-
* - `src/storage/v8-state-stores.js` (write/read DailyRhythmState)
|
|
15
|
+
* - `src/storage/v8-state-stores.js` (write/read DailyRhythmState, readDreamConsolidationRunById, writeDreamConsolidationRun)
|
|
15
16
|
* - `src/core/second-nature/quiet-dream/quiet-daily-review-builder.js`
|
|
16
17
|
* - `src/core/second-nature/quiet-dream/dream-scheduler.js`
|
|
18
|
+
* - `src/core/second-nature/quiet-dream/dream-consolidation-runner.js`
|
|
17
19
|
*
|
|
18
20
|
* Boundary:
|
|
19
|
-
* -
|
|
21
|
+
* - Schedules and records lifecycle; additionally executes stale scheduled runs
|
|
22
|
+
* so `dreamStatus` reaches completed/blocked.
|
|
20
23
|
* - Does not bypass Dream runner; only records due/completed/blocked.
|
|
21
24
|
*/
|
|
22
25
|
import type { StateDatabase } from "../../../storage/db/index.js";
|
|
@@ -4,30 +4,138 @@
|
|
|
4
4
|
* Core logic: Check if today's Quiet review is due (closures exist but no review
|
|
5
5
|
* yet), schedule/run it, then check Dream status. Records durable states so
|
|
6
6
|
* loop_status can report exact missing stages even when heartbeat does not
|
|
7
|
-
* select a quiet intent.
|
|
7
|
+
* select a quiet intent. Also executes stale Dream consolidation runs that
|
|
8
|
+
* were left at "scheduled" because no runner picked them up.
|
|
8
9
|
*
|
|
9
10
|
* Design authority:
|
|
10
11
|
* - `.anws/v8/04_SYSTEM_DESIGN/dream-quiet-memory-system.md §4`
|
|
11
12
|
* - `.anws/v8/04_SYSTEM_DESIGN/dream-quiet-memory-system.detail.md §3.1-§3.4`
|
|
12
13
|
*
|
|
13
14
|
* Dependencies:
|
|
14
|
-
* - `src/storage/v8-state-stores.js` (write/read DailyRhythmState)
|
|
15
|
+
* - `src/storage/v8-state-stores.js` (write/read DailyRhythmState, readDreamConsolidationRunById, writeDreamConsolidationRun)
|
|
15
16
|
* - `src/core/second-nature/quiet-dream/quiet-daily-review-builder.js`
|
|
16
17
|
* - `src/core/second-nature/quiet-dream/dream-scheduler.js`
|
|
18
|
+
* - `src/core/second-nature/quiet-dream/dream-consolidation-runner.js`
|
|
17
19
|
*
|
|
18
20
|
* Boundary:
|
|
19
|
-
* -
|
|
21
|
+
* - Schedules and records lifecycle; additionally executes stale scheduled runs
|
|
22
|
+
* so `dreamStatus` reaches completed/blocked.
|
|
20
23
|
* - Does not bypass Dream runner; only records due/completed/blocked.
|
|
21
24
|
*/
|
|
22
|
-
import { writeDailyRhythmState, readDailyRhythmStateByDay, readActionClosuresByDay, } from "../../../storage/v8-state-stores.js";
|
|
25
|
+
import { writeDailyRhythmState, readDailyRhythmStateByDay, readActionClosuresByDay, readDreamConsolidationRunById, readDreamConsolidationRunsByQuietId, updateDreamConsolidationRunStatus, } from "../../../storage/v8-state-stores.js";
|
|
23
26
|
import { buildQuietDailyReview } from "./quiet-daily-review-builder.js";
|
|
24
27
|
import { scheduleDreamAfterQuiet } from "./dream-scheduler.js";
|
|
28
|
+
import { runDreamConsolidation } from "./dream-consolidation-runner.js";
|
|
29
|
+
// ───────────────────────────────────────────────────────────────
|
|
30
|
+
// Config
|
|
31
|
+
// ───────────────────────────────────────────────────────────────
|
|
32
|
+
const DREAM_DEFAULT_INTERVAL_DAYS = 7;
|
|
33
|
+
const STALE_SCHEDULED_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes
|
|
25
34
|
// ───────────────────────────────────────────────────────────────
|
|
26
35
|
// Helpers
|
|
27
36
|
// ───────────────────────────────────────────────────────────────
|
|
28
37
|
function todayString(now) {
|
|
29
38
|
return now.slice(0, 10);
|
|
30
39
|
}
|
|
40
|
+
function parsePayloadJson(json) {
|
|
41
|
+
if (!json)
|
|
42
|
+
return {};
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(json);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function isWithinDays(isoDate, now, days) {
|
|
51
|
+
const then = new Date(isoDate).getTime();
|
|
52
|
+
const current = new Date(now).getTime();
|
|
53
|
+
if (Number.isNaN(then) || Number.isNaN(current))
|
|
54
|
+
return false;
|
|
55
|
+
return current - then < days * 24 * 60 * 60 * 1000;
|
|
56
|
+
}
|
|
57
|
+
function isStaleScheduled(run, now) {
|
|
58
|
+
const created = new Date(run.createdAt).getTime();
|
|
59
|
+
const current = new Date(now).getTime();
|
|
60
|
+
if (Number.isNaN(created) || Number.isNaN(current))
|
|
61
|
+
return false;
|
|
62
|
+
return current - created > STALE_SCHEDULED_THRESHOLD_MS;
|
|
63
|
+
}
|
|
64
|
+
async function loadLatestDreamRunForQuiet(db, quietId) {
|
|
65
|
+
const runsRead = await readDreamConsolidationRunsByQuietId(db, quietId);
|
|
66
|
+
if (runsRead.degraded) {
|
|
67
|
+
return { degraded: runsRead.degraded };
|
|
68
|
+
}
|
|
69
|
+
return { row: runsRead.rows[0] };
|
|
70
|
+
}
|
|
71
|
+
async function executeStaleScheduledDreams(db, state, now) {
|
|
72
|
+
// Look for any scheduled dream runs for today and execute them.
|
|
73
|
+
const quietId = `quiet_${state.day}`;
|
|
74
|
+
const knownRunIds = [];
|
|
75
|
+
// Direct lookup by quiet review id is more reliable than rhythm payload cache.
|
|
76
|
+
const runsRead = await readDreamConsolidationRunsByQuietId(db, quietId);
|
|
77
|
+
if (runsRead.degraded) {
|
|
78
|
+
return runsRead.degraded;
|
|
79
|
+
}
|
|
80
|
+
for (const run of runsRead.rows) {
|
|
81
|
+
knownRunIds.push(run.id);
|
|
82
|
+
}
|
|
83
|
+
// Also read the rhythm payload for any ids that may have been recorded before.
|
|
84
|
+
const rhythmRead = await readDailyRhythmStateByDay(db, state.day);
|
|
85
|
+
if (!rhythmRead.degraded && rhythmRead.row?.payloadJson) {
|
|
86
|
+
const payload = parsePayloadJson(rhythmRead.row.payloadJson);
|
|
87
|
+
if (payload.dreamRunId) {
|
|
88
|
+
knownRunIds.push(String(payload.dreamRunId));
|
|
89
|
+
}
|
|
90
|
+
if (Array.isArray(payload.dreamRunIds)) {
|
|
91
|
+
knownRunIds.push(...payload.dreamRunIds);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const uniqueRunIds = [...new Set(knownRunIds)];
|
|
95
|
+
if (uniqueRunIds.length === 0) {
|
|
96
|
+
return { completed: false };
|
|
97
|
+
}
|
|
98
|
+
let lastResult = { completed: false };
|
|
99
|
+
for (const runId of uniqueRunIds) {
|
|
100
|
+
const runRead = await readDreamConsolidationRunById(db, runId);
|
|
101
|
+
if (runRead.degraded) {
|
|
102
|
+
return runRead.degraded;
|
|
103
|
+
}
|
|
104
|
+
const run = runRead.row;
|
|
105
|
+
if (!run)
|
|
106
|
+
continue;
|
|
107
|
+
if ((run.status === "scheduled" || run.status === "started") && isStaleScheduled(run, now)) {
|
|
108
|
+
const consolidateResult = await runDreamConsolidation(db, runId, { now });
|
|
109
|
+
if ("status" in consolidateResult && consolidateResult.status !== "degraded") {
|
|
110
|
+
const dreamResult = consolidateResult;
|
|
111
|
+
const finalStatus = dreamResult.status;
|
|
112
|
+
const finalReason = dreamResult.reason ?? undefined;
|
|
113
|
+
const updateResult = await updateDreamConsolidationRunStatus(db, runId, finalStatus, {
|
|
114
|
+
reason: finalReason ?? null,
|
|
115
|
+
lifecycleStatus: finalStatus === "completed" ? "completed" : "archived",
|
|
116
|
+
payloadJson: JSON.stringify({
|
|
117
|
+
...parsePayloadJson(run.payloadJson),
|
|
118
|
+
consolidatedAt: now,
|
|
119
|
+
candidateCount: dreamResult.candidates.length,
|
|
120
|
+
staleRepairedAt: now,
|
|
121
|
+
}),
|
|
122
|
+
});
|
|
123
|
+
if ("reason" in updateResult) {
|
|
124
|
+
return updateResult;
|
|
125
|
+
}
|
|
126
|
+
lastResult = { completed: true, reason: finalReason ?? "dream_scheduled_stalled" };
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
const degraded = consolidateResult;
|
|
130
|
+
return degraded;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else if (run.status === "completed") {
|
|
134
|
+
lastResult = { completed: true, reason: run.reason ?? undefined };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return lastResult;
|
|
138
|
+
}
|
|
31
139
|
// ───────────────────────────────────────────────────────────────
|
|
32
140
|
// Public API
|
|
33
141
|
// ───────────────────────────────────────────────────────────────
|
|
@@ -90,33 +198,96 @@ export async function checkDailyRhythm(db, options) {
|
|
|
90
198
|
}
|
|
91
199
|
}
|
|
92
200
|
}
|
|
201
|
+
// Track scheduled dream run ids across attempts
|
|
202
|
+
const dreamRunIds = [];
|
|
93
203
|
// Determine Dream status based on Quiet outcome
|
|
94
204
|
if (state.quietStatus === "completed") {
|
|
95
|
-
if (state.dreamStatus === "completed"
|
|
96
|
-
|
|
97
|
-
|
|
205
|
+
if (state.dreamStatus === "completed") {
|
|
206
|
+
// Already completed; nothing to do
|
|
207
|
+
}
|
|
208
|
+
else if (state.dreamStatus === "scheduled") {
|
|
209
|
+
// Stale scheduled run: try to execute consolidation now
|
|
210
|
+
const staleResult = await executeStaleScheduledDreams(db, state, now);
|
|
211
|
+
if ("status" in staleResult && staleResult.status === "degraded") {
|
|
212
|
+
return staleResult;
|
|
213
|
+
}
|
|
214
|
+
const { completed, reason } = staleResult;
|
|
215
|
+
if (completed) {
|
|
216
|
+
state.dreamStatus = "completed";
|
|
217
|
+
state.dreamReason = reason ?? "dream_completed";
|
|
218
|
+
state.dreamCompletedAt = now;
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
// Still cannot locate/run scheduled dream; remain scheduled
|
|
222
|
+
state.dreamReason = state.dreamReason ?? "dream_scheduled";
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
else if (state.dreamStatus === "blocked") {
|
|
98
226
|
// Already handled; do not re-schedule
|
|
99
227
|
}
|
|
100
228
|
else {
|
|
101
|
-
state.dreamStatus = "due";
|
|
102
|
-
state.dreamReason = "dream_scheduled";
|
|
103
|
-
// Schedule Dream
|
|
104
229
|
const quietId = `quiet_${day}`;
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
});
|
|
109
|
-
if ("reason" in dreamResult) {
|
|
110
|
-
state.dreamStatus = "blocked";
|
|
111
|
-
state.dreamReason = dreamResult.reason;
|
|
230
|
+
const latestRun = await loadLatestDreamRunForQuiet(db, quietId);
|
|
231
|
+
if (latestRun.degraded) {
|
|
232
|
+
return latestRun.degraded;
|
|
112
233
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
234
|
+
// If a completed/blocked run already exists within the 7-day interval, honor it.
|
|
235
|
+
if (latestRun.row &&
|
|
236
|
+
(latestRun.row.status === "completed" || latestRun.row.status === "blocked") &&
|
|
237
|
+
isWithinDays(latestRun.row.createdAt, now, DREAM_DEFAULT_INTERVAL_DAYS)) {
|
|
238
|
+
state.dreamStatus = latestRun.row.status;
|
|
239
|
+
state.dreamReason = latestRun.row.reason ?? "dream_completed";
|
|
240
|
+
if (latestRun.row.status === "completed") {
|
|
241
|
+
state.dreamCompletedAt = now;
|
|
242
|
+
}
|
|
116
243
|
}
|
|
117
244
|
else {
|
|
118
|
-
state.dreamStatus = "
|
|
245
|
+
state.dreamStatus = "due";
|
|
119
246
|
state.dreamReason = "dream_scheduled";
|
|
247
|
+
// Schedule Dream
|
|
248
|
+
const dreamResult = await scheduleDreamAfterQuiet(db, quietId, {
|
|
249
|
+
now,
|
|
250
|
+
schedulerAvailable: options?.schedulerAvailable ?? true,
|
|
251
|
+
});
|
|
252
|
+
if ("reason" in dreamResult) {
|
|
253
|
+
state.dreamStatus = "blocked";
|
|
254
|
+
state.dreamReason = dreamResult.reason;
|
|
255
|
+
}
|
|
256
|
+
else if (dreamResult.status === "blocked") {
|
|
257
|
+
state.dreamStatus = "blocked";
|
|
258
|
+
state.dreamReason = dreamResult.reason ?? "dream_scheduler_unavailable";
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
state.dreamStatus = "scheduled";
|
|
262
|
+
state.dreamReason = "dream_scheduled";
|
|
263
|
+
dreamRunIds.push(dreamResult.id);
|
|
264
|
+
// Immediately execute the freshly scheduled dream so it does not sit
|
|
265
|
+
// pending forever (T-DQ.R.7).
|
|
266
|
+
const consolidateResult = await runDreamConsolidation(db, dreamResult.id, { now });
|
|
267
|
+
if ("status" in consolidateResult && consolidateResult.status !== "degraded") {
|
|
268
|
+
const dreamOutcome = consolidateResult;
|
|
269
|
+
const updateResult = await updateDreamConsolidationRunStatus(db, dreamResult.id, dreamOutcome.status, {
|
|
270
|
+
reason: dreamOutcome.reason ?? null,
|
|
271
|
+
lifecycleStatus: dreamOutcome.status === "completed" ? "completed" : "archived",
|
|
272
|
+
payloadJson: JSON.stringify({
|
|
273
|
+
consolidatedAt: now,
|
|
274
|
+
candidateCount: dreamOutcome.candidates.length,
|
|
275
|
+
}),
|
|
276
|
+
});
|
|
277
|
+
if ("reason" in updateResult) {
|
|
278
|
+
return updateResult;
|
|
279
|
+
}
|
|
280
|
+
state.dreamStatus = dreamOutcome.status === "completed" ? "completed" : "blocked";
|
|
281
|
+
state.dreamReason = dreamOutcome.reason ?? (dreamOutcome.status === "completed" ? "dream_completed" : "dream_failed");
|
|
282
|
+
if (dreamOutcome.status === "completed") {
|
|
283
|
+
state.dreamCompletedAt = now;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
const degraded = consolidateResult;
|
|
288
|
+
return degraded;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
120
291
|
}
|
|
121
292
|
}
|
|
122
293
|
}
|
|
@@ -134,6 +305,11 @@ export async function checkDailyRhythm(db, options) {
|
|
|
134
305
|
state.dreamReason = state.quietReason ?? "dream_blocked_redaction";
|
|
135
306
|
}
|
|
136
307
|
// Persist state
|
|
308
|
+
const payload = { checkedAt: now, hasClosures: closuresRead.rows.length };
|
|
309
|
+
if (dreamRunIds.length > 0) {
|
|
310
|
+
payload.dreamRunId = dreamRunIds[0];
|
|
311
|
+
payload.dreamRunIds = dreamRunIds;
|
|
312
|
+
}
|
|
137
313
|
const writeResult = await writeDailyRhythmState(db, {
|
|
138
314
|
id: `rhythm_${day}`,
|
|
139
315
|
day,
|
|
@@ -152,7 +328,7 @@ export async function checkDailyRhythm(db, options) {
|
|
|
152
328
|
resolveStatus: "resolvable",
|
|
153
329
|
},
|
|
154
330
|
],
|
|
155
|
-
payloadJson: JSON.stringify(
|
|
331
|
+
payloadJson: JSON.stringify(payload),
|
|
156
332
|
updatedAt: now,
|
|
157
333
|
});
|
|
158
334
|
if ("reason" in writeResult) {
|
|
@@ -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
|
|
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:
|
|
@@ -22,6 +23,14 @@
|
|
|
22
23
|
*/
|
|
23
24
|
import type { StateDatabase } from "../../../storage/db/index.js";
|
|
24
25
|
import type { SourceRef, DegradedOperationResult, V8ReasonCode } from "../../../shared/types/v8-contracts.js";
|
|
26
|
+
export interface QuietReviewEntry {
|
|
27
|
+
closureId: string;
|
|
28
|
+
platformId?: string;
|
|
29
|
+
actionKind?: string;
|
|
30
|
+
status: string;
|
|
31
|
+
summary?: string;
|
|
32
|
+
reason?: string;
|
|
33
|
+
}
|
|
25
34
|
export interface QuietDailyReviewResult {
|
|
26
35
|
id: string;
|
|
27
36
|
day: string;
|
|
@@ -32,9 +41,16 @@ export interface QuietDailyReviewResult {
|
|
|
32
41
|
/** Explicit closure refs — first-class provenance for reviewed ActionClosureRecords */
|
|
33
42
|
closureRefs: SourceRef[];
|
|
34
43
|
reviewSummary: string;
|
|
44
|
+
/** Human-readable review sections */
|
|
45
|
+
sections: QuietReviewSection[];
|
|
35
46
|
importanceSignals: string[];
|
|
36
47
|
createdAt: string;
|
|
37
48
|
}
|
|
49
|
+
export interface QuietReviewSection {
|
|
50
|
+
kind: "headline" | "completed" | "deferred" | "failed" | "memory_candidates" | "observations";
|
|
51
|
+
title: string;
|
|
52
|
+
lines: string[];
|
|
53
|
+
}
|
|
38
54
|
export interface BuildQuietDailyReviewOptions {
|
|
39
55
|
day?: string;
|
|
40
56
|
now?: string;
|