@haaaiawd/second-nature 0.2.12 → 0.2.13
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 +96 -6
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/runtime/cli/commands/index.js +85 -11
- package/runtime/cli/host-capability/host-discovery-port.d.ts +85 -0
- package/runtime/cli/host-capability/host-discovery-port.js +137 -0
- package/runtime/cli/ops/heartbeat-surface.d.ts +3 -3
- package/runtime/cli/ops/heartbeat-surface.js +6 -5
- package/runtime/cli/ops/ops-router.d.ts +6 -2
- package/runtime/cli/ops/ops-router.js +1273 -1145
- package/runtime/connectors/base/normalized-evidence-content.d.ts +4 -0
- package/runtime/connectors/base/normalized-evidence-content.js +21 -2
- package/runtime/connectors/evidence-normalizer.js +32 -1
- package/runtime/core/second-nature/action/action-closure-recorder.d.ts +2 -0
- package/runtime/core/second-nature/action/action-closure-recorder.js +49 -34
- package/runtime/core/second-nature/action/action-proposal-builder.js +3 -2
- package/runtime/core/second-nature/action/policy-bound-dispatch.d.ts +2 -0
- package/runtime/core/second-nature/action/policy-bound-dispatch.js +7 -3
- package/runtime/core/second-nature/control-plane/cycle-finalizer.d.ts +82 -0
- package/runtime/core/second-nature/control-plane/cycle-finalizer.js +187 -0
- package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.js +13 -9
- package/runtime/core/second-nature/control-plane/real-runtime-spine.js +1 -1
- package/runtime/core/second-nature/guidance/guidance-proposal-consumer.d.ts +2 -1
- package/runtime/core/second-nature/guidance/guidance-proposal-consumer.js +4 -2
- package/runtime/core/second-nature/perception/judgment-engine.js +8 -4
- package/runtime/core/second-nature/perception/perception-builder.js +14 -2
- package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.js +30 -3
- package/runtime/core/second-nature/quiet-dream/dream-consolidation-runner.d.ts +5 -1
- package/runtime/core/second-nature/quiet-dream/dream-consolidation-runner.js +68 -29
- package/runtime/core/second-nature/quiet-dream/dream-scheduler.js +2 -1
- package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.js +2 -1
- package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.d.ts +1 -0
- package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.js +33 -0
- package/runtime/observability/causal-loop-health.d.ts +2 -1
- package/runtime/observability/causal-loop-health.js +7 -0
- package/runtime/observability/loop-stage-event-sink.js +6 -1
- package/runtime/observability/loop-status.d.ts +2 -0
- package/runtime/observability/loop-status.js +14 -1
- package/runtime/observability/services/heartbeat-digest-assembler.d.ts +3 -0
- package/runtime/observability/services/heartbeat-digest-assembler.js +9 -0
- package/runtime/shared/degraded-status-classifier.d.ts +16 -0
- package/runtime/shared/degraded-status-classifier.js +68 -0
- package/runtime/shared/evidence-level-classifier.d.ts +61 -0
- package/runtime/shared/evidence-level-classifier.js +116 -0
- package/runtime/shared/provenance-tier.d.ts +37 -0
- package/runtime/shared/provenance-tier.js +97 -0
- package/runtime/shared/setup-ack.d.ts +54 -0
- package/runtime/shared/setup-ack.js +108 -0
- package/runtime/shared/source-ref-compat.js +5 -2
- package/runtime/shared/types/v8-contracts.d.ts +13 -2
- package/runtime/storage/db/index.js +71 -28
- package/runtime/storage/db/migrations/v8-005-single-status-schema.js +2 -2
- package/runtime/storage/db/migrations/v8-006-loop-stage-event-proof-trace-columns.d.ts +9 -0
- package/runtime/storage/db/migrations/v8-006-loop-stage-event-proof-trace-columns.js +15 -0
- package/runtime/storage/db/schema/v8-entities.d.ts +76 -0
- package/runtime/storage/db/schema/v8-entities.js +4 -0
- package/runtime/storage/services/write-validation-gate.js +1 -1
- package/runtime/storage/v8-state-stores.d.ts +7 -2
- package/runtime/storage/v8-state-stores.js +37 -19
|
@@ -31,6 +31,7 @@ import { evaluateActionPolicy } from "../action/autonomy-policy-evaluator.js";
|
|
|
31
31
|
import { dispatchAllowedAction } from "../action/policy-bound-dispatch.js";
|
|
32
32
|
import { recordNoActionClosure, recordRememberClosure, recordPolicyOutcomeClosure, recordExecutionClosure, } from "../action/action-closure-recorder.js";
|
|
33
33
|
import { checkDailyRhythm } from "../quiet-dream/daily-rhythm-scheduler.js";
|
|
34
|
+
import { classifyDegradedStatus } from "../../../shared/degraded-status-classifier.js";
|
|
34
35
|
// ───────────────────────────────────────────────────────────────
|
|
35
36
|
// Helpers
|
|
36
37
|
// ───────────────────────────────────────────────────────────────
|
|
@@ -84,7 +85,7 @@ async function advanceAndRecordDailyRhythm(db, cycleId, cycleSequence, cycleRef,
|
|
|
84
85
|
catch (rhythmErr) {
|
|
85
86
|
const errMsg = rhythmErr instanceof Error ? rhythmErr.message : String(rhythmErr);
|
|
86
87
|
const degraded = {
|
|
87
|
-
status: "
|
|
88
|
+
status: classifyDegradedStatus("state_unreadable"),
|
|
88
89
|
reason: "state_unreadable",
|
|
89
90
|
ownerStage: "quiet",
|
|
90
91
|
sourceRefs: [cycleRef],
|
|
@@ -130,7 +131,7 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
130
131
|
});
|
|
131
132
|
if ("reason" in traceResult) {
|
|
132
133
|
return {
|
|
133
|
-
status: "
|
|
134
|
+
status: classifyDegradedStatus("state_unreadable"),
|
|
134
135
|
reason: "state_unreadable",
|
|
135
136
|
ownerStage: "ingestion",
|
|
136
137
|
sourceRefs: [cycleRef],
|
|
@@ -150,7 +151,7 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
150
151
|
});
|
|
151
152
|
// ── Perception stage ──
|
|
152
153
|
const perceptionResult = await buildPerceptionCards(db, { cycleId, now });
|
|
153
|
-
const perceptionDegraded = "
|
|
154
|
+
const perceptionDegraded = "ownerStage" in perceptionResult
|
|
154
155
|
? perceptionResult
|
|
155
156
|
: null;
|
|
156
157
|
await recordLoopStageEvent(db, {
|
|
@@ -199,8 +200,8 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
199
200
|
noActionReason: degradedReason,
|
|
200
201
|
degraded: perceptionDegraded
|
|
201
202
|
? {
|
|
202
|
-
status:
|
|
203
|
-
reason:
|
|
203
|
+
status: classifyDegradedStatus(degradedReason),
|
|
204
|
+
reason: degradedReason,
|
|
204
205
|
ownerStage: "perception",
|
|
205
206
|
sourceRefs: [cycleRef],
|
|
206
207
|
operatorNextAction: "Retry heartbeat after perception recovery",
|
|
@@ -327,7 +328,7 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
327
328
|
else {
|
|
328
329
|
// Build proposal for the actionable verdict
|
|
329
330
|
const proposalResult = await buildActionProposal(db, actionableVerdict.id, { now });
|
|
330
|
-
if ("
|
|
331
|
+
if ("operatorNextAction" in proposalResult) {
|
|
331
332
|
// Proposal build failed — still need a closure
|
|
332
333
|
closureDegraded = proposalResult;
|
|
333
334
|
const closureResult = await recordNoActionClosure(db, cycleId, closureDegraded.reason, { now });
|
|
@@ -398,7 +399,8 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
398
399
|
status: "completed",
|
|
399
400
|
occurredAt: new Date().toISOString(),
|
|
400
401
|
reason: decision.decisionReason,
|
|
401
|
-
sourceRefs:
|
|
402
|
+
sourceRefs: proposal.sourceRefs,
|
|
403
|
+
proofRefs: decision.proofRefs,
|
|
402
404
|
});
|
|
403
405
|
// Record execution stage started
|
|
404
406
|
await recordLoopStageEvent(db, {
|
|
@@ -408,7 +410,8 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
408
410
|
stage: "execution",
|
|
409
411
|
status: "started",
|
|
410
412
|
occurredAt: new Date().toISOString(),
|
|
411
|
-
sourceRefs:
|
|
413
|
+
sourceRefs: proposal.sourceRefs,
|
|
414
|
+
proofRefs: decision.proofRefs,
|
|
412
415
|
});
|
|
413
416
|
// Dispatch — no real external write in T-CP.R.2
|
|
414
417
|
const dispatchResult = dispatchAllowedAction(proposal, decision, { guidanceAvailable: false });
|
|
@@ -512,7 +515,8 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
512
515
|
status: closureDegraded ? "failed" : "completed",
|
|
513
516
|
occurredAt: new Date().toISOString(),
|
|
514
517
|
reason: closureDegraded?.reason,
|
|
515
|
-
sourceRefs:
|
|
518
|
+
sourceRefs: proposal.sourceRefs,
|
|
519
|
+
proofRefs: decision.proofRefs,
|
|
516
520
|
});
|
|
517
521
|
}
|
|
518
522
|
}
|
|
@@ -27,7 +27,7 @@ export async function runRealRuntimeHeartbeatCycle(options) {
|
|
|
27
27
|
};
|
|
28
28
|
const result = await runHeartbeatCycle(options.state, request);
|
|
29
29
|
// Pass through degraded results directly
|
|
30
|
-
if ("status" in result
|
|
30
|
+
if ("status" in result) {
|
|
31
31
|
return result;
|
|
32
32
|
}
|
|
33
33
|
const orchestrationResult = result;
|
|
@@ -29,10 +29,11 @@ export interface GuidanceOutput {
|
|
|
29
29
|
mode: "draft" | "notify";
|
|
30
30
|
textRef: SourceRef;
|
|
31
31
|
sourceRefs: SourceRef[];
|
|
32
|
+
proofRefs: SourceRef[];
|
|
32
33
|
deliveryClaim: "not_delivered";
|
|
33
34
|
decisionId: string;
|
|
34
35
|
actionKind: PlatformNeutralActionKind;
|
|
35
|
-
ownerVisible:
|
|
36
|
+
ownerVisible: true;
|
|
36
37
|
}
|
|
37
38
|
export type GuidanceValidationResult = {
|
|
38
39
|
ok: true;
|
|
@@ -68,7 +68,8 @@ function buildGuidanceOutput(proposal, decision) {
|
|
|
68
68
|
id: `guidance_${decision.id}_${Date.now()}`,
|
|
69
69
|
mode,
|
|
70
70
|
textRef,
|
|
71
|
-
sourceRefs:
|
|
71
|
+
sourceRefs: proposal.sourceRefs,
|
|
72
|
+
proofRefs: decision.proofRefs,
|
|
72
73
|
deliveryClaim: "not_delivered",
|
|
73
74
|
decisionId: decision.id,
|
|
74
75
|
actionKind,
|
|
@@ -87,7 +88,8 @@ export function consumeGuidanceProposal(proposal, decision) {
|
|
|
87
88
|
status: "blocked",
|
|
88
89
|
reason: "policy_denied_high_risk",
|
|
89
90
|
ownerStage: "execution",
|
|
90
|
-
sourceRefs:
|
|
91
|
+
sourceRefs: proposal.sourceRefs,
|
|
92
|
+
proofRefs: decision.proofRefs,
|
|
91
93
|
operatorNextAction: "Review policy decision before requesting guidance",
|
|
92
94
|
retryable: false,
|
|
93
95
|
},
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import { readPerceptionCardById, writeJudgmentVerdict, } from "../../../storage/v8-state-stores.js";
|
|
25
25
|
import { parseSourceRefs } from "../../../shared/serialization.js";
|
|
26
26
|
import { ACTION_KIND_REGISTRY } from "../../../shared/types/v8-contracts.js";
|
|
27
|
+
import { classifyDegradedStatus } from "../../../shared/degraded-status-classifier.js";
|
|
27
28
|
// ───────────────────────────────────────────────────────────────
|
|
28
29
|
// Config
|
|
29
30
|
// ───────────────────────────────────────────────────────────────
|
|
@@ -107,7 +108,7 @@ export async function runAgentJudgment(db, perceptionCardId, options) {
|
|
|
107
108
|
const card = readResult.row;
|
|
108
109
|
if (!card) {
|
|
109
110
|
return {
|
|
110
|
-
status: "
|
|
111
|
+
status: classifyDegradedStatus("state_unreadable"),
|
|
111
112
|
reason: "state_unreadable",
|
|
112
113
|
ownerStage: "judgment",
|
|
113
114
|
sourceRefs: [],
|
|
@@ -194,9 +195,12 @@ export async function runAgentJudgment(db, perceptionCardId, options) {
|
|
|
194
195
|
});
|
|
195
196
|
if ("reason" in writeResult) {
|
|
196
197
|
return {
|
|
197
|
-
status:
|
|
198
|
-
verdicts: [],
|
|
198
|
+
status: classifyDegradedStatus(writeResult.reason),
|
|
199
199
|
reason: writeResult.reason,
|
|
200
|
+
ownerStage: "judgment",
|
|
201
|
+
sourceRefs: [],
|
|
202
|
+
operatorNextAction: `Failed to persist JudgmentVerdict: ${writeResult.reason}`,
|
|
203
|
+
retryable: true,
|
|
200
204
|
};
|
|
201
205
|
}
|
|
202
206
|
return {
|
|
@@ -218,7 +222,7 @@ export async function runAgentJudgments(db, perceptionCardIds, options) {
|
|
|
218
222
|
failed.push({
|
|
219
223
|
perceptionCardId,
|
|
220
224
|
degraded: {
|
|
221
|
-
status: "
|
|
225
|
+
status: classifyDegradedStatus(result.reason ?? "judgment_low_confidence"),
|
|
222
226
|
reason: result.reason ?? "judgment_low_confidence",
|
|
223
227
|
ownerStage: "judgment",
|
|
224
228
|
sourceRefs: [],
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { readEvidenceItemsByStatus, writePerceptionCard, updateEvidenceItemLifecycleStatus, } from "../../../storage/v8-state-stores.js";
|
|
25
25
|
import { parseSourceRefs } from "../../../shared/serialization.js";
|
|
26
|
+
import { classifyDegradedStatus } from "../../../shared/degraded-status-classifier.js";
|
|
26
27
|
// ───────────────────────────────────────────────────────────────
|
|
27
28
|
// Config
|
|
28
29
|
// ───────────────────────────────────────────────────────────────
|
|
@@ -110,6 +111,14 @@ function inferRelevanceClass(score) {
|
|
|
110
111
|
}
|
|
111
112
|
function inferSummary(evidence) {
|
|
112
113
|
const payload = parsePayload(evidence.payloadJson);
|
|
114
|
+
if (payload?.contentStatus === "content_missing") {
|
|
115
|
+
return {
|
|
116
|
+
summary: payload.contentMissingReason
|
|
117
|
+
? `Content missing from ${evidence.platformId}: ${payload.contentMissingReason}`
|
|
118
|
+
: `Ref-only observation from ${evidence.platformId}: no readable content`,
|
|
119
|
+
contentMissing: true,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
113
122
|
if (payload?.summary && String(payload.summary).trim().length > 0) {
|
|
114
123
|
return { summary: String(payload.summary), contentMissing: false };
|
|
115
124
|
}
|
|
@@ -246,9 +255,12 @@ export async function buildPerceptionCards(db, options) {
|
|
|
246
255
|
});
|
|
247
256
|
if ("reason" in writeResult) {
|
|
248
257
|
return {
|
|
249
|
-
status:
|
|
250
|
-
cards,
|
|
258
|
+
status: classifyDegradedStatus(writeResult.reason),
|
|
251
259
|
reason: writeResult.reason,
|
|
260
|
+
ownerStage: "perception",
|
|
261
|
+
sourceRefs: card.evidenceRefs,
|
|
262
|
+
operatorNextAction: `Failed to persist PerceptionCard: ${writeResult.reason}`,
|
|
263
|
+
retryable: true,
|
|
252
264
|
};
|
|
253
265
|
}
|
|
254
266
|
await updateEvidenceItemLifecycleStatus(db, evidence.id, "perceived");
|
|
@@ -26,6 +26,7 @@ import { writeDailyRhythmState, readDailyRhythmStateByDay, readActionClosuresByD
|
|
|
26
26
|
import { buildQuietDailyReview } from "./quiet-daily-review-builder.js";
|
|
27
27
|
import { scheduleDreamAfterQuiet } from "./dream-scheduler.js";
|
|
28
28
|
import { runDreamConsolidation } from "./dream-consolidation-runner.js";
|
|
29
|
+
import { acceptMemoryProjection } from "./memory-projection-lifecycle.js";
|
|
29
30
|
// ───────────────────────────────────────────────────────────────
|
|
30
31
|
// Config
|
|
31
32
|
// ───────────────────────────────────────────────────────────────
|
|
@@ -106,7 +107,7 @@ async function executeStaleScheduledDreams(db, state, now) {
|
|
|
106
107
|
continue;
|
|
107
108
|
if ((run.status === "scheduled" || run.status === "started") && isStaleScheduled(run, now)) {
|
|
108
109
|
const consolidateResult = await runDreamConsolidation(db, runId, { now });
|
|
109
|
-
if ("status" in consolidateResult &&
|
|
110
|
+
if ("status" in consolidateResult && !("ownerStage" in consolidateResult)) {
|
|
110
111
|
const dreamResult = consolidateResult;
|
|
111
112
|
const finalStatus = dreamResult.status;
|
|
112
113
|
const finalReason = dreamResult.reason ?? undefined;
|
|
@@ -122,6 +123,20 @@ async function executeStaleScheduledDreams(db, state, now) {
|
|
|
122
123
|
if ("reason" in updateResult) {
|
|
123
124
|
return updateResult;
|
|
124
125
|
}
|
|
126
|
+
// T-DQ.R.10: Accept valid candidates as long-term memory projections.
|
|
127
|
+
// This step was moved out of the runner to separate candidate generation
|
|
128
|
+
// from acceptance, per design §4.2.
|
|
129
|
+
if (dreamResult.status === "completed") {
|
|
130
|
+
for (const candidate of dreamResult.candidates.filter((c) => c.validationStatus === "valid")) {
|
|
131
|
+
const acceptResult = await acceptMemoryProjection(db, candidate.id, `topic_${state.day}`, candidate.candidateText, candidate.sourceRefs, { now });
|
|
132
|
+
if ("projectionId" in acceptResult) {
|
|
133
|
+
candidate.acceptedProjectionId = acceptResult.projectionId;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
return acceptResult;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
125
140
|
lastResult = { completed: true, reason: finalReason ?? "dream_scheduled_stalled" };
|
|
126
141
|
}
|
|
127
142
|
else {
|
|
@@ -207,7 +222,7 @@ export async function checkDailyRhythm(db, options) {
|
|
|
207
222
|
else if (state.dreamStatus === "scheduled") {
|
|
208
223
|
// Stale scheduled run: try to execute consolidation now
|
|
209
224
|
const staleResult = await executeStaleScheduledDreams(db, state, now);
|
|
210
|
-
if ("status" in staleResult
|
|
225
|
+
if ("status" in staleResult) {
|
|
211
226
|
return staleResult;
|
|
212
227
|
}
|
|
213
228
|
const { completed, reason } = staleResult;
|
|
@@ -262,7 +277,7 @@ export async function checkDailyRhythm(db, options) {
|
|
|
262
277
|
// Immediately execute the freshly scheduled dream so it does not sit
|
|
263
278
|
// pending forever (T-DQ.R.7).
|
|
264
279
|
const consolidateResult = await runDreamConsolidation(db, dreamResult.id, { now });
|
|
265
|
-
if ("status" in consolidateResult &&
|
|
280
|
+
if ("status" in consolidateResult && !("ownerStage" in consolidateResult)) {
|
|
266
281
|
const dreamOutcome = consolidateResult;
|
|
267
282
|
const updateResult = await updateDreamConsolidationRunStatus(db, dreamResult.id, dreamOutcome.status, {
|
|
268
283
|
reason: dreamOutcome.reason ?? null,
|
|
@@ -278,6 +293,18 @@ export async function checkDailyRhythm(db, options) {
|
|
|
278
293
|
state.dreamReason = dreamOutcome.reason ?? (dreamOutcome.status === "completed" ? "dream_completed" : "dream_failed");
|
|
279
294
|
if (dreamOutcome.status === "completed") {
|
|
280
295
|
state.dreamCompletedAt = now;
|
|
296
|
+
// T-DQ.R.10: Accept valid candidates as long-term memory projections.
|
|
297
|
+
// This step was moved out of the runner to separate candidate generation
|
|
298
|
+
// from acceptance, per design §4.2.
|
|
299
|
+
for (const candidate of dreamOutcome.candidates.filter((c) => c.validationStatus === "valid")) {
|
|
300
|
+
const acceptResult = await acceptMemoryProjection(db, candidate.id, `topic_${day}`, candidate.candidateText, candidate.sourceRefs, { now });
|
|
301
|
+
if ("projectionId" in acceptResult) {
|
|
302
|
+
candidate.acceptedProjectionId = acceptResult.projectionId;
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
return acceptResult;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
281
308
|
}
|
|
282
309
|
}
|
|
283
310
|
else {
|
|
@@ -10,13 +10,17 @@
|
|
|
10
10
|
* - `.anws/v8/04_SYSTEM_DESIGN/dream-quiet-memory-system.md §4.2`
|
|
11
11
|
*
|
|
12
12
|
* Dependencies:
|
|
13
|
-
* - `src/storage/v8-state-stores.js` (readDreamConsolidationRunById, readQuietDailyReviewById
|
|
13
|
+
* - `src/storage/v8-state-stores.js` (readDreamConsolidationRunById, readQuietDailyReviewById)
|
|
14
14
|
* - `src/shared/types/v8-contracts.js` (SourceRef, DegradedOperationResult, V8ReasonCode)
|
|
15
15
|
*
|
|
16
16
|
* Boundary:
|
|
17
17
|
* - Rules-only candidate generation; no model assist in this version.
|
|
18
18
|
* - Does not accept/reject projections; only creates candidates.
|
|
19
19
|
* - Redaction gate blocks sensitive private content, preserves public technical.
|
|
20
|
+
* - T-DQ.R.10: Does NOT call acceptMemoryProjection. Candidate acceptance is a
|
|
21
|
+
* separate step owned by the caller (dream-scheduler or explicit accept API).
|
|
22
|
+
* The runner only generates and validates candidates; it returns them for
|
|
23
|
+
* the caller to accept via `acceptMemoryProjection(candidateId)`.
|
|
20
24
|
*
|
|
21
25
|
* Test coverage: tests/unit/dream/dream-consolidation-runner.test.ts
|
|
22
26
|
*/
|
|
@@ -10,18 +10,22 @@
|
|
|
10
10
|
* - `.anws/v8/04_SYSTEM_DESIGN/dream-quiet-memory-system.md §4.2`
|
|
11
11
|
*
|
|
12
12
|
* Dependencies:
|
|
13
|
-
* - `src/storage/v8-state-stores.js` (readDreamConsolidationRunById, readQuietDailyReviewById
|
|
13
|
+
* - `src/storage/v8-state-stores.js` (readDreamConsolidationRunById, readQuietDailyReviewById)
|
|
14
14
|
* - `src/shared/types/v8-contracts.js` (SourceRef, DegradedOperationResult, V8ReasonCode)
|
|
15
15
|
*
|
|
16
16
|
* Boundary:
|
|
17
17
|
* - Rules-only candidate generation; no model assist in this version.
|
|
18
18
|
* - Does not accept/reject projections; only creates candidates.
|
|
19
19
|
* - Redaction gate blocks sensitive private content, preserves public technical.
|
|
20
|
+
* - T-DQ.R.10: Does NOT call acceptMemoryProjection. Candidate acceptance is a
|
|
21
|
+
* separate step owned by the caller (dream-scheduler or explicit accept API).
|
|
22
|
+
* The runner only generates and validates candidates; it returns them for
|
|
23
|
+
* the caller to accept via `acceptMemoryProjection(candidateId)`.
|
|
20
24
|
*
|
|
21
25
|
* Test coverage: tests/unit/dream/dream-consolidation-runner.test.ts
|
|
22
26
|
*/
|
|
23
27
|
import { readDreamConsolidationRunById, readQuietDailyReviewById, } from "../../../storage/v8-state-stores.js";
|
|
24
|
-
import {
|
|
28
|
+
import { classifyDegradedStatus } from "../../../shared/degraded-status-classifier.js";
|
|
25
29
|
// ───────────────────────────────────────────────────────────────
|
|
26
30
|
// Helpers
|
|
27
31
|
// ───────────────────────────────────────────────────────────────
|
|
@@ -47,9 +51,13 @@ function buildSourceRefsFromReview(review) {
|
|
|
47
51
|
];
|
|
48
52
|
}
|
|
49
53
|
function redactSensitive(input) {
|
|
50
|
-
//
|
|
54
|
+
// Credential-shaped patterns first — highest sensitivity.
|
|
51
55
|
if (/\b(?:Bearer|token|secret|password|key)\s*[:=]\s*[a-zA-Z0-9+/=]{8,}\b/i.test(input)) {
|
|
52
|
-
return { text: "[redacted: credential shape detected]", blocked: true };
|
|
56
|
+
return { text: "[redacted: credential shape detected]", blocked: true, reason: "dream_blocked_credential" };
|
|
57
|
+
}
|
|
58
|
+
// Private context markers (names, addresses, phone numbers) — lower threshold than credential.
|
|
59
|
+
if (/\b(?:ssn|social.security|phone|address|email)\s*[:=]\s*[^\s]+/i.test(input)) {
|
|
60
|
+
return { text: "[redacted: private context]", blocked: true, reason: "dream_blocked_private_redacted" };
|
|
53
61
|
}
|
|
54
62
|
return { text: input, blocked: false };
|
|
55
63
|
}
|
|
@@ -57,7 +65,7 @@ function generateCandidatesFromReview(runId, reviewId, reviewPayload) {
|
|
|
57
65
|
const candidates = [];
|
|
58
66
|
const summary = String(reviewPayload.reviewSummary ?? "");
|
|
59
67
|
if (summary.length > 0) {
|
|
60
|
-
const { text, blocked } = redactSensitive(summary);
|
|
68
|
+
const { text, blocked, reason } = redactSensitive(summary);
|
|
61
69
|
if (blocked) {
|
|
62
70
|
candidates.push({
|
|
63
71
|
id: `cand_${runId}_summary`,
|
|
@@ -67,7 +75,7 @@ function generateCandidatesFromReview(runId, reviewId, reviewPayload) {
|
|
|
67
75
|
sourceRefs: buildSourceRefsFromReview({ id: reviewId, day: "" }),
|
|
68
76
|
confidence: 0.3,
|
|
69
77
|
validationStatus: "blocked",
|
|
70
|
-
validationReason:
|
|
78
|
+
validationReason: reason,
|
|
71
79
|
});
|
|
72
80
|
}
|
|
73
81
|
else {
|
|
@@ -84,7 +92,7 @@ function generateCandidatesFromReview(runId, reviewId, reviewPayload) {
|
|
|
84
92
|
}
|
|
85
93
|
const importanceSignals = reviewPayload.importanceSignals;
|
|
86
94
|
if (importanceSignals && importanceSignals.length > 0) {
|
|
87
|
-
const { text, blocked } = redactSensitive(importanceSignals.join("; "));
|
|
95
|
+
const { text, blocked, reason } = redactSensitive(importanceSignals.join("; "));
|
|
88
96
|
if (!blocked) {
|
|
89
97
|
candidates.push({
|
|
90
98
|
id: `cand_${runId}_signals`,
|
|
@@ -96,9 +104,33 @@ function generateCandidatesFromReview(runId, reviewId, reviewPayload) {
|
|
|
96
104
|
validationStatus: "valid",
|
|
97
105
|
});
|
|
98
106
|
}
|
|
107
|
+
else {
|
|
108
|
+
candidates.push({
|
|
109
|
+
id: `cand_${runId}_signals`,
|
|
110
|
+
runId,
|
|
111
|
+
reviewId,
|
|
112
|
+
candidateText: text,
|
|
113
|
+
sourceRefs: buildSourceRefsFromReview({ id: reviewId, day: "" }),
|
|
114
|
+
confidence: 0.3,
|
|
115
|
+
validationStatus: "blocked",
|
|
116
|
+
validationReason: reason,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
99
119
|
}
|
|
100
120
|
return candidates;
|
|
101
121
|
}
|
|
122
|
+
function validateCandidate(candidate) {
|
|
123
|
+
if (!candidate.candidateText || candidate.candidateText.trim().length === 0) {
|
|
124
|
+
return "dream_blocked_validation_failed";
|
|
125
|
+
}
|
|
126
|
+
if (!candidate.sourceRefs || candidate.sourceRefs.length === 0) {
|
|
127
|
+
return "dream_blocked_validation_failed";
|
|
128
|
+
}
|
|
129
|
+
if (candidate.confidence < 0.1) {
|
|
130
|
+
return "dream_blocked_validation_failed";
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
102
134
|
// ───────────────────────────────────────────────────────────────
|
|
103
135
|
// Public API
|
|
104
136
|
// ───────────────────────────────────────────────────────────────
|
|
@@ -111,7 +143,7 @@ export async function runDreamConsolidation(db, runId, options) {
|
|
|
111
143
|
const run = runRead.row;
|
|
112
144
|
if (!run) {
|
|
113
145
|
return {
|
|
114
|
-
status: "
|
|
146
|
+
status: classifyDegradedStatus("state_unreadable"),
|
|
115
147
|
reason: "state_unreadable",
|
|
116
148
|
ownerStage: "dream",
|
|
117
149
|
sourceRefs: [],
|
|
@@ -126,7 +158,7 @@ export async function runDreamConsolidation(db, runId, options) {
|
|
|
126
158
|
const review = reviewRead.row;
|
|
127
159
|
if (!review) {
|
|
128
160
|
return {
|
|
129
|
-
status: "
|
|
161
|
+
status: classifyDegradedStatus("state_unreadable"),
|
|
130
162
|
reason: "state_unreadable",
|
|
131
163
|
ownerStage: "dream",
|
|
132
164
|
sourceRefs: [],
|
|
@@ -135,34 +167,41 @@ export async function runDreamConsolidation(db, runId, options) {
|
|
|
135
167
|
};
|
|
136
168
|
}
|
|
137
169
|
const reviewPayload = parsePayloadJson(review.payloadJson);
|
|
170
|
+
const contentStatus = String(reviewPayload.contentStatus ?? "");
|
|
171
|
+
// Block placeholder or empty Quiet reviews before candidate generation.
|
|
172
|
+
if (contentStatus === "placeholder_rejected" || contentStatus === "content_missing" || contentStatus === "empty") {
|
|
173
|
+
return {
|
|
174
|
+
runId,
|
|
175
|
+
status: "blocked",
|
|
176
|
+
candidates: [],
|
|
177
|
+
reason: "dream_blocked_no_content",
|
|
178
|
+
};
|
|
179
|
+
}
|
|
138
180
|
const candidates = generateCandidatesFromReview(runId, run.quietReviewId, reviewPayload);
|
|
139
|
-
//
|
|
181
|
+
// Run candidate validation; invalid candidates block the run with a precise reason.
|
|
182
|
+
for (const candidate of candidates) {
|
|
183
|
+
if (candidate.validationStatus === "valid") {
|
|
184
|
+
const validationReason = validateCandidate(candidate);
|
|
185
|
+
if (validationReason) {
|
|
186
|
+
candidate.validationStatus = "blocked";
|
|
187
|
+
candidate.validationReason = validationReason;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// If all candidates blocked → run blocked with the first precise reason.
|
|
140
192
|
if (candidates.length > 0 && candidates.every((c) => c.validationStatus === "blocked")) {
|
|
193
|
+
const firstReason = candidates[0]?.validationReason;
|
|
141
194
|
return {
|
|
142
195
|
runId,
|
|
143
196
|
status: "blocked",
|
|
144
197
|
candidates,
|
|
145
|
-
reason: "
|
|
198
|
+
reason: firstReason ?? "dream_blocked_private_redacted",
|
|
146
199
|
};
|
|
147
200
|
}
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
for (const candidate of validCandidates) {
|
|
153
|
-
const acceptResult = await acceptMemoryProjection(db, candidate.id, `topic_${review.day}`, candidate.candidateText, candidate.sourceRefs, { now });
|
|
154
|
-
if ("projectionId" in acceptResult) {
|
|
155
|
-
candidate.acceptedProjectionId = acceptResult.projectionId;
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
return {
|
|
159
|
-
runId,
|
|
160
|
-
status: "failed",
|
|
161
|
-
candidates,
|
|
162
|
-
reason: acceptResult.reason,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
}
|
|
201
|
+
// T-DQ.R.10: Runner only generates and validates candidates.
|
|
202
|
+
// Acceptance is a separate step owned by the caller via acceptMemoryProjection.
|
|
203
|
+
// Valid candidates are returned with validationStatus="valid" for the caller
|
|
204
|
+
// to accept; the runner does NOT call acceptMemoryProjection here.
|
|
166
205
|
return {
|
|
167
206
|
runId,
|
|
168
207
|
status: "completed",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* Test coverage: tests/unit/dream/dream-scheduler-lifecycle.test.ts
|
|
22
22
|
*/
|
|
23
23
|
import { readQuietDailyReviewById, writeDreamConsolidationRun, } from "../../../storage/v8-state-stores.js";
|
|
24
|
+
import { classifyDegradedStatus } from "../../../shared/degraded-status-classifier.js";
|
|
24
25
|
// ───────────────────────────────────────────────────────────────
|
|
25
26
|
// Public API
|
|
26
27
|
// ───────────────────────────────────────────────────────────────
|
|
@@ -33,7 +34,7 @@ export async function scheduleDreamAfterQuiet(db, quietReviewId, options) {
|
|
|
33
34
|
const review = readResult.row;
|
|
34
35
|
if (!review) {
|
|
35
36
|
return {
|
|
36
|
-
status: "
|
|
37
|
+
status: classifyDegradedStatus("state_unreadable"),
|
|
37
38
|
reason: "state_unreadable",
|
|
38
39
|
ownerStage: "dream",
|
|
39
40
|
sourceRefs: [],
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* Test coverage: tests/unit/dream/memory-projection-lifecycle.test.ts
|
|
22
22
|
*/
|
|
23
23
|
import { readMemoryProjectionsByTopic, writeLongTermMemoryProjection, updateLongTermMemoryProjectionStatus, } from "../../../storage/v8-state-stores.js";
|
|
24
|
+
import { classifyDegradedStatus } from "../../../shared/degraded-status-classifier.js";
|
|
24
25
|
// ───────────────────────────────────────────────────────────────
|
|
25
26
|
// Public API
|
|
26
27
|
// ───────────────────────────────────────────────────────────────
|
|
@@ -28,7 +29,7 @@ export async function acceptMemoryProjection(db, candidateId, topicKey, memoryTe
|
|
|
28
29
|
const now = options?.now ?? new Date().toISOString();
|
|
29
30
|
if (sourceRefs.length === 0) {
|
|
30
31
|
return {
|
|
31
|
-
status: "
|
|
32
|
+
status: classifyDegradedStatus("source_refs_unresolved"),
|
|
32
33
|
reason: "source_refs_unresolved",
|
|
33
34
|
ownerStage: "projection",
|
|
34
35
|
sourceRefs: [],
|
|
@@ -62,4 +62,5 @@ export type BuildQuietDailyReviewOutput = {
|
|
|
62
62
|
status: "empty";
|
|
63
63
|
reason: V8ReasonCode;
|
|
64
64
|
} | DegradedOperationResult;
|
|
65
|
+
export type QuietReviewContentStatus = "content_present" | "empty" | "placeholder_rejected" | "content_missing";
|
|
65
66
|
export declare function buildQuietDailyReview(db: StateDatabase, options?: BuildQuietDailyReviewOptions): Promise<BuildQuietDailyReviewOutput>;
|
|
@@ -107,6 +107,25 @@ function groupByStatus(entries) {
|
|
|
107
107
|
}
|
|
108
108
|
return groups;
|
|
109
109
|
}
|
|
110
|
+
// Placeholder/template detector: true when the review has no content-bearing
|
|
111
|
+
// evidence or perception signals and no memory-review candidates. Closure-only
|
|
112
|
+
// system text is not meaningful memory input.
|
|
113
|
+
function isPlaceholderReview(notableSignals, memoryCandidates, evidenceRows, perceptionRows) {
|
|
114
|
+
if (memoryCandidates.length > 0)
|
|
115
|
+
return false;
|
|
116
|
+
if (notableSignals.length > 0)
|
|
117
|
+
return false;
|
|
118
|
+
const hasContentEvidence = evidenceRows.some((ev) => {
|
|
119
|
+
const payload = parsePayloadJson(ev.payloadJson);
|
|
120
|
+
return payload.contentStatus !== "content_missing";
|
|
121
|
+
});
|
|
122
|
+
if (hasContentEvidence)
|
|
123
|
+
return false;
|
|
124
|
+
const hasContentPerception = perceptionRows.some((p) => !!p.summary);
|
|
125
|
+
if (hasContentPerception)
|
|
126
|
+
return false;
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
110
129
|
// ───────────────────────────────────────────────────────────────
|
|
111
130
|
// Public API
|
|
112
131
|
// ───────────────────────────────────────────────────────────────
|
|
@@ -177,11 +196,18 @@ export async function buildQuietDailyReview(db, options) {
|
|
|
177
196
|
}
|
|
178
197
|
for (const perception of perceptionRows) {
|
|
179
198
|
if (perception.summary) {
|
|
199
|
+
const perceptionPayload = parsePayloadJson(perception.payloadJson);
|
|
200
|
+
if (perceptionPayload.contentMissing) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
180
203
|
notableSignals.push(`Perception: ${perception.summary}`);
|
|
181
204
|
}
|
|
182
205
|
}
|
|
183
206
|
for (const evidence of evidenceRows) {
|
|
184
207
|
const payload = parsePayloadJson(evidence.payloadJson);
|
|
208
|
+
if (payload.contentStatus === "content_missing") {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
185
211
|
if (payload.summary) {
|
|
186
212
|
notableSignals.push(`${evidence.platformId}: ${String(payload.summary)}`);
|
|
187
213
|
}
|
|
@@ -245,6 +271,12 @@ export async function buildQuietDailyReview(db, options) {
|
|
|
245
271
|
const reviewSummary = firstTopic
|
|
246
272
|
? `Day ${day}: ${closures.length} closures around ${firstTopic}${notableSignals.length > 0 ? ` with ${notableSignals.length} notable signals` : ""}.`
|
|
247
273
|
: `Day ${day}: ${closures.length} closures (${completedCount} completed, ${deniedCount} deferred/denied, ${failedCount} failed)`;
|
|
274
|
+
const isPlaceholder = isPlaceholderReview(notableSignals, memoryCandidates, evidenceRows, perceptionRows);
|
|
275
|
+
const contentStatus = isPlaceholder
|
|
276
|
+
? "placeholder_rejected"
|
|
277
|
+
: (notableSignals.length > 0 || memoryCandidates.length > 0)
|
|
278
|
+
? "content_present"
|
|
279
|
+
: "content_missing";
|
|
248
280
|
const importanceSignals = [];
|
|
249
281
|
if (memoryCandidates.length > 0) {
|
|
250
282
|
importanceSignals.push(`${memoryCandidates.length} memory-review candidates`);
|
|
@@ -268,6 +300,7 @@ export async function buildQuietDailyReview(db, options) {
|
|
|
268
300
|
lifecycleStatus: "pending",
|
|
269
301
|
payloadJson: JSON.stringify({
|
|
270
302
|
reviewSummary,
|
|
303
|
+
contentStatus,
|
|
271
304
|
importanceSignals,
|
|
272
305
|
memoryCandidates,
|
|
273
306
|
sections,
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* Test coverage: tests/unit/observability/causal-loop-health.test.ts
|
|
22
22
|
*/
|
|
23
23
|
import type { StateDatabase } from "../storage/db/index.js";
|
|
24
|
-
import type { LoopStage, DegradedOperationResult } from "../shared/types/v8-contracts.js";
|
|
24
|
+
import type { LoopStage, DegradedOperationResult, EvidenceLevel } from "../shared/types/v8-contracts.js";
|
|
25
25
|
export interface StageHealth {
|
|
26
26
|
stage: LoopStage;
|
|
27
27
|
lastEventAt?: string;
|
|
@@ -36,6 +36,7 @@ export interface CausalLoopHealthSnapshot {
|
|
|
36
36
|
lastHeartbeatAt?: string;
|
|
37
37
|
stages: StageHealth[];
|
|
38
38
|
reason?: string;
|
|
39
|
+
evidenceLevel: EvidenceLevel;
|
|
39
40
|
}
|
|
40
41
|
export interface AssembleLoopStatusOptions {
|
|
41
42
|
stallThresholdCycles?: number;
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* Test coverage: tests/unit/observability/causal-loop-health.test.ts
|
|
22
22
|
*/
|
|
23
23
|
import { readHeartbeatCycleTraces, readLoopStageEventsByStage, } from "../storage/v8-state-stores.js";
|
|
24
|
+
import { classifyEvidenceLevel } from "../shared/evidence-level-classifier.js";
|
|
24
25
|
// ───────────────────────────────────────────────────────────────
|
|
25
26
|
// Config
|
|
26
27
|
// ───────────────────────────────────────────────────────────────
|
|
@@ -77,6 +78,7 @@ export async function assembleLoopStatus(db, options) {
|
|
|
77
78
|
lastCycleSequence: 0,
|
|
78
79
|
stages: [],
|
|
79
80
|
reason: "no heartbeat cycles recorded",
|
|
81
|
+
evidenceLevel: classifyEvidenceLevel({ hasCarrierEnvelope: true }),
|
|
80
82
|
};
|
|
81
83
|
}
|
|
82
84
|
const lastCycle = cycles[0];
|
|
@@ -114,5 +116,10 @@ export async function assembleLoopStatus(db, options) {
|
|
|
114
116
|
lastHeartbeatAt: lastCycle.heartbeatStartedAt,
|
|
115
117
|
stages,
|
|
116
118
|
reason: stalledAt ? `stage ${stalledAt} stalled for >=${threshold} cycles` : undefined,
|
|
119
|
+
evidenceLevel: classifyEvidenceLevel({
|
|
120
|
+
hasCarrierEnvelope: true,
|
|
121
|
+
hasContractSmoke: true,
|
|
122
|
+
hasCycleExecution: stalledAt === undefined,
|
|
123
|
+
}),
|
|
117
124
|
};
|
|
118
125
|
}
|