@haaaiawd/second-nature 0.2.1 → 0.2.4
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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/runtime/cli/index.js +5 -1
- package/runtime/cli/ops/heartbeat-surface.d.ts +23 -0
- package/runtime/cli/ops/heartbeat-surface.js +73 -1
- package/runtime/cli/ops/manual-run-dispatcher.d.ts +2 -0
- package/runtime/cli/ops/manual-run-dispatcher.js +10 -0
- package/runtime/cli/ops/ops-router.js +117 -31
- package/runtime/cli/ops/workspace-heartbeat-runner.d.ts +3 -0
- package/runtime/cli/ops/workspace-heartbeat-runner.js +2 -0
- package/runtime/connectors/base/contract.d.ts +10 -0
- package/runtime/connectors/base/policy-bound-write-dispatch.d.ts +29 -0
- package/runtime/connectors/base/policy-bound-write-dispatch.js +127 -0
- package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.js +336 -25
- package/runtime/core/second-nature/control-plane/real-runtime-spine.d.ts +33 -0
- package/runtime/core/second-nature/control-plane/real-runtime-spine.js +41 -0
- package/runtime/core/second-nature/guidance/impulse-context-reader.d.ts +44 -0
- package/runtime/core/second-nature/guidance/impulse-context-reader.js +84 -0
- package/runtime/core/second-nature/guidance/impulse-context-writer.d.ts +39 -0
- package/runtime/core/second-nature/guidance/impulse-context-writer.js +70 -0
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +6 -1
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +11 -0
- package/runtime/core/second-nature/perception/judgment-engine.d.ts +2 -0
- package/runtime/core/second-nature/perception/judgment-engine.js +11 -1
- package/runtime/core/second-nature/perception/perception-builder.d.ts +6 -2
- package/runtime/core/second-nature/perception/perception-builder.js +18 -7
- package/runtime/core/second-nature/quiet/run-source-backed-quiet.d.ts +3 -0
- package/runtime/core/second-nature/quiet/run-source-backed-quiet.js +42 -1
- package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.d.ts +43 -0
- package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.js +157 -0
- package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.js +17 -16
- package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.d.ts +3 -0
- package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.js +4 -0
- package/runtime/observability/living-loop-health-gate.d.ts +45 -0
- package/runtime/observability/living-loop-health-gate.js +94 -0
- package/runtime/observability/loop-status.d.ts +11 -0
- package/runtime/observability/loop-status.js +49 -3
- package/runtime/observability/services/audit-closure-recorders.d.ts +31 -0
- package/runtime/observability/services/audit-closure-recorders.js +87 -0
- package/runtime/observability/services/heartbeat-digest-assembler.d.ts +12 -0
- package/runtime/observability/services/heartbeat-digest-assembler.js +22 -3
- package/runtime/shared/types/v8-contracts.d.ts +2 -2
- package/runtime/storage/db/index.js +34 -0
- package/runtime/storage/db/migrations/index.js +4 -0
- package/runtime/storage/db/migrations/v8-001-living-perception-loop.js +119 -119
- package/runtime/storage/db/migrations/v8-002-perception-contract-alignment.d.ts +12 -0
- package/runtime/storage/db/migrations/v8-002-perception-contract-alignment.js +14 -0
- package/runtime/storage/db/migrations/v8-003-quiet-closure-refs.d.ts +10 -0
- package/runtime/storage/db/migrations/v8-003-quiet-closure-refs.js +12 -0
- package/runtime/storage/db/schema/v8-entities.d.ts +586 -0
- package/runtime/storage/db/schema/v8-entities.js +39 -0
- package/runtime/storage/v8-state-stores.d.ts +32 -2
- package/runtime/storage/v8-state-stores.js +121 -2
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImpulseContextWriter — Persist assembled impulse + atmosphere as agent-facing artifact.
|
|
3
|
+
*
|
|
4
|
+
* Core logic: Convert impulse assembly result into a durable artifact that can be
|
|
5
|
+
* read during setup, heartbeat, and platform-scene entry without re-assembly.
|
|
6
|
+
*
|
|
7
|
+
* Design authority:
|
|
8
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/guidance-voice-system.md §1`
|
|
9
|
+
* - `docs/validation/openclaw-plugin-classification.md §5`
|
|
10
|
+
*
|
|
11
|
+
* Dependencies:
|
|
12
|
+
* - `src/storage/v8-state-stores.js` (writeImpulseContextArtifact)
|
|
13
|
+
* - `src/guidance/impulse-assembler.js` (ImpulseAssemblerResult)
|
|
14
|
+
*
|
|
15
|
+
* Boundary:
|
|
16
|
+
* - Does NOT register a fake OpenClaw context-engine.
|
|
17
|
+
* - Does NOT claim delivery or decision capability.
|
|
18
|
+
* - Overwrites existing artifact for same scene/capability combo (upsert behavior).
|
|
19
|
+
*/
|
|
20
|
+
import type { StateDatabase } from "../../../storage/db/index.js";
|
|
21
|
+
import type { ImpulseAssemblerResult } from "../../../guidance/impulse-assembler.js";
|
|
22
|
+
import type { SourceRef, DegradedOperationResult } from "../../../shared/types/v8-contracts.js";
|
|
23
|
+
export interface ImpulseContextArtifactInput {
|
|
24
|
+
sceneType: string;
|
|
25
|
+
capabilityIntent?: string;
|
|
26
|
+
platformId?: string;
|
|
27
|
+
impulseResult: ImpulseAssemblerResult;
|
|
28
|
+
atmosphereText?: string;
|
|
29
|
+
expressionBoundaryConstraints: string[];
|
|
30
|
+
expressionBoundaryStyle?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface WriteArtifactResult {
|
|
33
|
+
id: string;
|
|
34
|
+
freshnessVersion: number;
|
|
35
|
+
}
|
|
36
|
+
export declare function writeImpulseContext(db: StateDatabase, input: ImpulseContextArtifactInput, options?: {
|
|
37
|
+
now?: string;
|
|
38
|
+
sourceRefs?: SourceRef[];
|
|
39
|
+
}): Promise<WriteArtifactResult | DegradedOperationResult>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImpulseContextWriter — Persist assembled impulse + atmosphere as agent-facing artifact.
|
|
3
|
+
*
|
|
4
|
+
* Core logic: Convert impulse assembly result into a durable artifact that can be
|
|
5
|
+
* read during setup, heartbeat, and platform-scene entry without re-assembly.
|
|
6
|
+
*
|
|
7
|
+
* Design authority:
|
|
8
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/guidance-voice-system.md §1`
|
|
9
|
+
* - `docs/validation/openclaw-plugin-classification.md §5`
|
|
10
|
+
*
|
|
11
|
+
* Dependencies:
|
|
12
|
+
* - `src/storage/v8-state-stores.js` (writeImpulseContextArtifact)
|
|
13
|
+
* - `src/guidance/impulse-assembler.js` (ImpulseAssemblerResult)
|
|
14
|
+
*
|
|
15
|
+
* Boundary:
|
|
16
|
+
* - Does NOT register a fake OpenClaw context-engine.
|
|
17
|
+
* - Does NOT claim delivery or decision capability.
|
|
18
|
+
* - Overwrites existing artifact for same scene/capability combo (upsert behavior).
|
|
19
|
+
*/
|
|
20
|
+
import { writeImpulseContextArtifact } from "../../../storage/v8-state-stores.js";
|
|
21
|
+
// ───────────────────────────────────────────────────────────────
|
|
22
|
+
// Helpers
|
|
23
|
+
// ───────────────────────────────────────────────────────────────
|
|
24
|
+
function buildArtifactId(sceneType, capabilityIntent, platformId) {
|
|
25
|
+
const cap = capabilityIntent ?? "none";
|
|
26
|
+
const plat = platformId ?? "generic";
|
|
27
|
+
return `ica_${sceneType}_${cap}_${plat}`;
|
|
28
|
+
}
|
|
29
|
+
// ───────────────────────────────────────────────────────────────
|
|
30
|
+
// Public API
|
|
31
|
+
// ───────────────────────────────────────────────────────────────
|
|
32
|
+
export async function writeImpulseContext(db, input, options) {
|
|
33
|
+
const now = options?.now ?? new Date().toISOString();
|
|
34
|
+
const id = buildArtifactId(input.sceneType, input.capabilityIntent, input.platformId);
|
|
35
|
+
const sourceRefs = options?.sourceRefs ?? [
|
|
36
|
+
{
|
|
37
|
+
uri: `sn://impulse-context/${id}`,
|
|
38
|
+
family: "projection",
|
|
39
|
+
id,
|
|
40
|
+
redactionClass: "none",
|
|
41
|
+
resolveStatus: "resolvable",
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
const result = await writeImpulseContextArtifact(db, {
|
|
45
|
+
id,
|
|
46
|
+
createdAt: now,
|
|
47
|
+
updatedAt: now,
|
|
48
|
+
sceneType: input.sceneType,
|
|
49
|
+
capabilityIntent: input.capabilityIntent ?? null,
|
|
50
|
+
platformId: input.platformId ?? null,
|
|
51
|
+
capabilityClass: input.impulseResult.capabilityClass ?? null,
|
|
52
|
+
impulseSource: input.impulseResult.source,
|
|
53
|
+
impulseText: input.impulseResult.impulse?.text ?? null,
|
|
54
|
+
atmosphereText: input.atmosphereText ?? null,
|
|
55
|
+
expressionBoundaryConstraintsJson: JSON.stringify(input.expressionBoundaryConstraints),
|
|
56
|
+
expressionBoundaryStyle: input.expressionBoundaryStyle ?? null,
|
|
57
|
+
freshnessVersion: 1,
|
|
58
|
+
sourceRefs,
|
|
59
|
+
redactionClass: "none",
|
|
60
|
+
payloadJson: JSON.stringify({
|
|
61
|
+
impulseKind: input.impulseResult.impulse?.kind ?? null,
|
|
62
|
+
impulseReviewStatus: input.impulseResult.impulse?.reviewStatus ?? null,
|
|
63
|
+
}),
|
|
64
|
+
lifecycleStatus: "active",
|
|
65
|
+
});
|
|
66
|
+
if ("reason" in result) {
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
return { id, freshnessVersion: 1 };
|
|
70
|
+
}
|
|
@@ -25,6 +25,7 @@ import type { NarrativeStateStore } from "../../../storage/narrative/narrative-s
|
|
|
25
25
|
import type { NarrativeTracePayload } from "../../../observability/services/lived-experience-audit.js";
|
|
26
26
|
import type { ExperienceWriter } from "../body/tool-experience/experience-writer.js";
|
|
27
27
|
import type { QuietDreamSchedulePort } from "../quiet/run-source-backed-quiet.js";
|
|
28
|
+
import type { AppendOnlyAuditStore } from "../../../observability/audit/append-only-audit-store.js";
|
|
28
29
|
import type { GoalLifecyclePolicy } from "./goal-lifecycle-policy.js";
|
|
29
30
|
import type { IdleCuriosityPolicy } from "./idle-curiosity-policy.js";
|
|
30
31
|
import type { CircuitBreakerManager } from "../body/circuit-breaker/circuit-breaker-manager.js";
|
|
@@ -50,12 +51,14 @@ export interface HeartbeatQuietWorkflowDeps {
|
|
|
50
51
|
workspaceRoot: string;
|
|
51
52
|
/** v7 T-V7C.C.3: when present, a successful Quiet write auto-triggers Dream scheduling. */
|
|
52
53
|
dreamSchedulePort?: QuietDreamSchedulePort;
|
|
54
|
+
/** T-OBS.R.1: audit sink for Quiet outcomes consumed by heartbeat_digest. */
|
|
55
|
+
auditStore?: AppendOnlyAuditStore;
|
|
53
56
|
}
|
|
54
57
|
/**
|
|
55
58
|
* Resolves the heartbeat outcome for a guard-allowed intent (outreach dispatch, quiet orchestration, or default).
|
|
56
59
|
* Exported for unit tests (CR-M1 wiring).
|
|
57
60
|
*/
|
|
58
|
-
export declare function resolveAllowedIntentResult(intent: CandidateIntent, runtime: HeartbeatRuntimeSnapshot, inputs: SnapshotInputs, signal: HeartbeatSignal, deps: Pick<HeartbeatDeps, "outreachDispatch" | "quietWorkflow" | "connectorExecutor" | "state" | "workspaceRoot" | "experienceWriter" | "circuitBreakerManager">): Promise<HeartbeatCycleResult>;
|
|
61
|
+
export declare function resolveAllowedIntentResult(intent: CandidateIntent, runtime: HeartbeatRuntimeSnapshot, inputs: SnapshotInputs, signal: HeartbeatSignal, deps: Pick<HeartbeatDeps, "outreachDispatch" | "quietWorkflow" | "connectorExecutor" | "state" | "workspaceRoot" | "experienceWriter" | "circuitBreakerManager" | "auditStore">): Promise<HeartbeatCycleResult>;
|
|
59
62
|
export interface HeartbeatDeps {
|
|
60
63
|
/** Load snapshot inputs from state-system */
|
|
61
64
|
loadSnapshotInputs: () => Promise<SnapshotInputs>;
|
|
@@ -86,6 +89,8 @@ export interface HeartbeatDeps {
|
|
|
86
89
|
idleCuriosityPolicy?: IdleCuriosityPolicy;
|
|
87
90
|
/** v7 T-BTS.C.5: when present, updates breaker state after connector execution. */
|
|
88
91
|
circuitBreakerManager?: CircuitBreakerManager;
|
|
92
|
+
/** T-OBS.R.1: shared audit sink for connector attempts consumed by heartbeat_digest. */
|
|
93
|
+
auditStore?: AppendOnlyAuditStore;
|
|
89
94
|
}
|
|
90
95
|
/**
|
|
91
96
|
* Ingest a heartbeat rhythm signal and drive one full decision round.
|
|
@@ -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 { recordConnectorAttemptAudit } from "../../../observability/services/audit-closure-recorders.js";
|
|
13
14
|
/**
|
|
14
15
|
* Resolves the heartbeat outcome for a guard-allowed intent (outreach dispatch, quiet orchestration, or default).
|
|
15
16
|
* Exported for unit tests (CR-M1 wiring).
|
|
@@ -40,6 +41,7 @@ export async function resolveAllowedIntentResult(intent, runtime, inputs, signal
|
|
|
40
41
|
workspaceRoot: deps.quietWorkflow.workspaceRoot,
|
|
41
42
|
// v7 T-V7C.C.3: pass Dream schedule port so Quiet completion triggers Dream.
|
|
42
43
|
dreamSchedulePort: deps.quietWorkflow.dreamSchedulePort,
|
|
44
|
+
auditStore: deps.quietWorkflow.auditStore,
|
|
43
45
|
});
|
|
44
46
|
return quietRun.result;
|
|
45
47
|
}
|
|
@@ -82,6 +84,15 @@ export async function resolveAllowedIntentResult(intent, runtime, inputs, signal
|
|
|
82
84
|
}
|
|
83
85
|
: undefined,
|
|
84
86
|
});
|
|
87
|
+
recordConnectorAttemptAudit({
|
|
88
|
+
auditStore: deps.auditStore,
|
|
89
|
+
platformId: intent.platformId,
|
|
90
|
+
capability: toCapabilityIntent(intent),
|
|
91
|
+
result,
|
|
92
|
+
triggerSource: "heartbeat",
|
|
93
|
+
decisionId,
|
|
94
|
+
intentId: intent.id,
|
|
95
|
+
});
|
|
85
96
|
// T3.3.1: on success, map connector result to life evidence and append.
|
|
86
97
|
// On failure or empty result, no evidence is fabricated — attempt audit
|
|
87
98
|
// is already recorded by the connector policy layer telemetry.
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import type { StateDatabase } from "../../../storage/db/index.js";
|
|
25
25
|
import type { SourceRef, DegradedOperationResult, PlatformNeutralActionKind, V8ReasonCode } from "../../../shared/types/v8-contracts.js";
|
|
26
|
+
import type { AcceptedProjection } from "../control-plane/accepted-projection-loader.js";
|
|
26
27
|
export interface JudgmentVerdictResult {
|
|
27
28
|
id: string;
|
|
28
29
|
cycleId: string;
|
|
@@ -41,6 +42,7 @@ export interface RunAgentJudgmentResult {
|
|
|
41
42
|
}
|
|
42
43
|
export interface RunAgentJudgmentOptions {
|
|
43
44
|
now?: string;
|
|
45
|
+
acceptedProjections?: AcceptedProjection[];
|
|
44
46
|
}
|
|
45
47
|
export declare function runAgentJudgment(db: StateDatabase, perceptionCardId: string, options?: RunAgentJudgmentOptions): Promise<RunAgentJudgmentResult | DegradedOperationResult>;
|
|
46
48
|
export interface BatchJudgmentResult {
|
|
@@ -153,7 +153,17 @@ export async function runAgentJudgment(db, perceptionCardId, options) {
|
|
|
153
153
|
/* ignore */
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
|
-
|
|
156
|
+
let { actionKind, reason, finalConfidence } = selectVerdict(card.relevance ?? 0.3, card.confidence ?? 0.6, riskPosture, hasSourceRefs, possibleIntents);
|
|
157
|
+
// T-DQ.R.3: Boost verdict when accepted memory projection matches topic
|
|
158
|
+
const acceptedProjections = options?.acceptedProjections ?? [];
|
|
159
|
+
const matchingProjection = acceptedProjections.find((p) => p.topicKey.toLowerCase() === (card.topic ?? "").toLowerCase());
|
|
160
|
+
if (matchingProjection) {
|
|
161
|
+
finalConfidence = Math.min(0.95, finalConfidence + 0.1);
|
|
162
|
+
if (actionKind === "ignore") {
|
|
163
|
+
actionKind = "remember";
|
|
164
|
+
reason = "projection_topic_matched";
|
|
165
|
+
}
|
|
166
|
+
}
|
|
157
167
|
const verdict = {
|
|
158
168
|
id: `jud_${perceptionCardId}_${now.replace(/[:.]/g, "")}`,
|
|
159
169
|
cycleId: card.cycleId,
|
|
@@ -37,8 +37,12 @@ export interface PerceptionCardResult {
|
|
|
37
37
|
cycleId: string;
|
|
38
38
|
topic: string;
|
|
39
39
|
entities: string[];
|
|
40
|
-
novelty:
|
|
41
|
-
|
|
40
|
+
/** Canonical novelty class: new | changed | duplicate | stale */
|
|
41
|
+
noveltyClass: "new" | "changed" | "duplicate" | "stale";
|
|
42
|
+
/** Numeric relevance score in [0, 1] */
|
|
43
|
+
relevanceScore: number;
|
|
44
|
+
/** Derived relevance class: low | medium | high */
|
|
45
|
+
relevanceClass: "low" | "medium" | "high";
|
|
42
46
|
summary: string;
|
|
43
47
|
possibleIntents: PlatformNeutralActionKind[];
|
|
44
48
|
reviewPriority: "low" | "medium" | "high";
|
|
@@ -76,12 +76,13 @@ function extractEntities(evidence) {
|
|
|
76
76
|
}
|
|
77
77
|
return [...new Set(entities)];
|
|
78
78
|
}
|
|
79
|
-
function
|
|
79
|
+
function inferNoveltyClass(evidence) {
|
|
80
|
+
// Canonical novelty classification
|
|
80
81
|
if (evidence.sensitivityHint === "public_technical")
|
|
81
|
-
return "
|
|
82
|
+
return "changed";
|
|
82
83
|
return "new";
|
|
83
84
|
}
|
|
84
|
-
function
|
|
85
|
+
function inferRelevanceScore(evidence) {
|
|
85
86
|
if (evidence.sensitivityHint === "sensitive")
|
|
86
87
|
return 0.9;
|
|
87
88
|
if (evidence.sensitivityHint === "public_technical")
|
|
@@ -90,6 +91,13 @@ function inferRelevance(evidence) {
|
|
|
90
91
|
return 0.5;
|
|
91
92
|
return 0.3;
|
|
92
93
|
}
|
|
94
|
+
function inferRelevanceClass(score) {
|
|
95
|
+
if (score >= 0.7)
|
|
96
|
+
return "high";
|
|
97
|
+
if (score >= 0.4)
|
|
98
|
+
return "medium";
|
|
99
|
+
return "low";
|
|
100
|
+
}
|
|
93
101
|
function inferSummary(evidence) {
|
|
94
102
|
const platform = evidence.platformId;
|
|
95
103
|
const topic = extractTopic(evidence);
|
|
@@ -124,13 +132,15 @@ function inferRiskFlags(evidence) {
|
|
|
124
132
|
}
|
|
125
133
|
function buildCardFromEvidence(evidence, cycleId, now) {
|
|
126
134
|
const sourceRefs = parseSourceRefs(evidence.sourceRefsJson);
|
|
135
|
+
const relevanceScore = inferRelevanceScore(evidence);
|
|
127
136
|
return {
|
|
128
137
|
id: `per_${evidence.id}`,
|
|
129
138
|
cycleId,
|
|
130
139
|
topic: extractTopic(evidence),
|
|
131
140
|
entities: extractEntities(evidence),
|
|
132
|
-
|
|
133
|
-
|
|
141
|
+
noveltyClass: inferNoveltyClass(evidence),
|
|
142
|
+
relevanceScore,
|
|
143
|
+
relevanceClass: inferRelevanceClass(relevanceScore),
|
|
134
144
|
summary: inferSummary(evidence),
|
|
135
145
|
possibleIntents: inferPossibleIntents(evidence),
|
|
136
146
|
reviewPriority: inferReviewPriority(evidence),
|
|
@@ -177,8 +187,9 @@ export async function buildPerceptionCards(db, options) {
|
|
|
177
187
|
cycleId: card.cycleId,
|
|
178
188
|
topic: card.topic,
|
|
179
189
|
entitiesJson: JSON.stringify(card.entities),
|
|
180
|
-
novelty: card.
|
|
181
|
-
relevance: card.
|
|
190
|
+
novelty: card.noveltyClass,
|
|
191
|
+
relevance: card.relevanceScore,
|
|
192
|
+
relevanceClass: card.relevanceClass,
|
|
182
193
|
summary: card.summary,
|
|
183
194
|
riskFlagsJson: JSON.stringify(card.riskFlags),
|
|
184
195
|
confidence: card.confidence,
|
|
@@ -10,6 +10,7 @@ import type { HeartbeatRuntimeSnapshot } from "../heartbeat/runtime-snapshot.js"
|
|
|
10
10
|
import type { HeartbeatCycleResult } from "../heartbeat/signal.js";
|
|
11
11
|
import { type QuietArtifactAck } from "../../../storage/quiet/quiet-artifact-writer.js";
|
|
12
12
|
import type { UserInterestSnapshot } from "../../../storage/user-interest/types.js";
|
|
13
|
+
import type { AppendOnlyAuditStore } from "../../../observability/audit/append-only-audit-store.js";
|
|
13
14
|
/**
|
|
14
15
|
* Minimal port for triggering Dream after Quiet completion (T-V7C.C.3).
|
|
15
16
|
* Kept narrow so run-source-backed-quiet does not take a hard dependency on dream-scheduler.
|
|
@@ -32,6 +33,8 @@ export interface RunSourceBackedQuietParams {
|
|
|
32
33
|
workspaceRoot?: string;
|
|
33
34
|
/** v7 T-V7C.C.3: when present, a successful Quiet artifact write auto-triggers Dream scheduling. */
|
|
34
35
|
dreamSchedulePort?: QuietDreamSchedulePort;
|
|
36
|
+
/** T-OBS.R.1: when present, Quiet outcomes write audit truth consumed by heartbeat_digest. */
|
|
37
|
+
auditStore?: AppendOnlyAuditStore;
|
|
35
38
|
}
|
|
36
39
|
export interface RunSourceBackedQuietResult {
|
|
37
40
|
result: HeartbeatCycleResult;
|
|
@@ -2,6 +2,7 @@ import { isLifeEvidenceSliceEmpty } from "../heartbeat/runtime-snapshot.js";
|
|
|
2
2
|
import { writeQuietArtifact } from "../../../storage/quiet/quiet-artifact-writer.js";
|
|
3
3
|
import { persistQuietArtifactToWorkspace } from "../../../storage/quiet/persist-quiet-artifact.js";
|
|
4
4
|
import { buildEvidencePack, buildQuietNarrativeGuidance, selectInterestBasis } from "../../../guidance/evidence-guidance.js";
|
|
5
|
+
import { recordQuietArtifactAudit } from "../../../observability/services/audit-closure-recorders.js";
|
|
5
6
|
function toGuidanceRef(r) {
|
|
6
7
|
return {
|
|
7
8
|
id: r.id,
|
|
@@ -37,7 +38,7 @@ async function maybeScheduleDreamAfterQuiet(dreamSchedulePort, day) {
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
export async function runSourceBackedQuiet(params) {
|
|
40
|
-
const { candidate, runtime, day, userInterestSnapshot, workspaceRoot, dreamSchedulePort } = params;
|
|
41
|
+
const { candidate, runtime, day, userInterestSnapshot, workspaceRoot, dreamSchedulePort, auditStore } = params;
|
|
41
42
|
const empty = isLifeEvidenceSliceEmpty(runtime.lifeEvidence);
|
|
42
43
|
if (empty) {
|
|
43
44
|
const input = {
|
|
@@ -54,6 +55,15 @@ export async function runSourceBackedQuiet(params) {
|
|
|
54
55
|
const p = await persistQuietArtifactToWorkspace(workspaceRoot, ack, input);
|
|
55
56
|
persistedRelativePath = p.relativePath;
|
|
56
57
|
}
|
|
58
|
+
recordQuietArtifactAudit({
|
|
59
|
+
auditStore,
|
|
60
|
+
day,
|
|
61
|
+
kind: "empty_state",
|
|
62
|
+
status: "empty",
|
|
63
|
+
reasons: ["quiet_empty_state", "no_fictional_narrative"],
|
|
64
|
+
artifactAck: ack,
|
|
65
|
+
persistedRelativePath,
|
|
66
|
+
});
|
|
57
67
|
return {
|
|
58
68
|
result: {
|
|
59
69
|
scope: "rhythm",
|
|
@@ -68,6 +78,13 @@ export async function runSourceBackedQuiet(params) {
|
|
|
68
78
|
const guidanceRefs = runtime.lifeEvidence.evidenceRefs.map(toGuidanceRef);
|
|
69
79
|
const ep = buildEvidencePack(guidanceRefs);
|
|
70
80
|
if (!ep.ok) {
|
|
81
|
+
recordQuietArtifactAudit({
|
|
82
|
+
auditStore,
|
|
83
|
+
day,
|
|
84
|
+
kind: "daily_report",
|
|
85
|
+
status: "blocked",
|
|
86
|
+
reasons: ep.reasons,
|
|
87
|
+
});
|
|
71
88
|
return {
|
|
72
89
|
result: {
|
|
73
90
|
scope: "rhythm",
|
|
@@ -78,6 +95,13 @@ export async function runSourceBackedQuiet(params) {
|
|
|
78
95
|
};
|
|
79
96
|
}
|
|
80
97
|
if (ep.pack.sensitiveBlocked) {
|
|
98
|
+
recordQuietArtifactAudit({
|
|
99
|
+
auditStore,
|
|
100
|
+
day,
|
|
101
|
+
kind: "daily_report",
|
|
102
|
+
status: "blocked",
|
|
103
|
+
reasons: ["quiet_guidance_sensitive_source_blocked"],
|
|
104
|
+
});
|
|
81
105
|
return {
|
|
82
106
|
result: {
|
|
83
107
|
scope: "rhythm",
|
|
@@ -128,6 +152,14 @@ export async function runSourceBackedQuiet(params) {
|
|
|
128
152
|
outline: claims.map((c) => c.text),
|
|
129
153
|
});
|
|
130
154
|
if (gq.status === "unavailable") {
|
|
155
|
+
recordQuietArtifactAudit({
|
|
156
|
+
auditStore,
|
|
157
|
+
day,
|
|
158
|
+
kind: "daily_report",
|
|
159
|
+
status: "blocked",
|
|
160
|
+
reasons: gq.reasons,
|
|
161
|
+
artifactAck: ack,
|
|
162
|
+
});
|
|
131
163
|
return {
|
|
132
164
|
result: {
|
|
133
165
|
scope: "rhythm",
|
|
@@ -147,6 +179,15 @@ export async function runSourceBackedQuiet(params) {
|
|
|
147
179
|
const reasons = ["quiet_artifact_written", ...gq.hints.slice(0, 2)];
|
|
148
180
|
if (dreamReason)
|
|
149
181
|
reasons.push(dreamReason);
|
|
182
|
+
recordQuietArtifactAudit({
|
|
183
|
+
auditStore,
|
|
184
|
+
day,
|
|
185
|
+
kind: "daily_report",
|
|
186
|
+
status: "completed",
|
|
187
|
+
reasons,
|
|
188
|
+
artifactAck: ack,
|
|
189
|
+
persistedRelativePath,
|
|
190
|
+
});
|
|
150
191
|
return {
|
|
151
192
|
result: {
|
|
152
193
|
scope: "rhythm",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DailyRhythmScheduler — Independent Quiet/Dream cadence with absence reasons.
|
|
3
|
+
*
|
|
4
|
+
* Core logic: Check if today's Quiet review is due (closures exist but no review
|
|
5
|
+
* yet), schedule/run it, then check Dream status. Records durable states so
|
|
6
|
+
* loop_status can report exact missing stages even when heartbeat does not
|
|
7
|
+
* select a quiet intent.
|
|
8
|
+
*
|
|
9
|
+
* Design authority:
|
|
10
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/dream-quiet-memory-system.md §4`
|
|
11
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/dream-quiet-memory-system.detail.md §3.1-§3.4`
|
|
12
|
+
*
|
|
13
|
+
* Dependencies:
|
|
14
|
+
* - `src/storage/v8-state-stores.js` (write/read DailyRhythmState)
|
|
15
|
+
* - `src/core/second-nature/quiet-dream/quiet-daily-review-builder.js`
|
|
16
|
+
* - `src/core/second-nature/quiet-dream/dream-scheduler.js`
|
|
17
|
+
*
|
|
18
|
+
* Boundary:
|
|
19
|
+
* - Does not run consolidation; only schedules and records lifecycle.
|
|
20
|
+
* - Does not bypass Dream runner; only records due/completed/blocked.
|
|
21
|
+
*/
|
|
22
|
+
import type { StateDatabase } from "../../../storage/db/index.js";
|
|
23
|
+
import type { DegradedOperationResult, V8ReasonCode } from "../../../shared/types/v8-contracts.js";
|
|
24
|
+
export type RhythmStatus = "due" | "completed" | "skipped" | "blocked" | "not_due";
|
|
25
|
+
export interface DailyRhythmState {
|
|
26
|
+
day: string;
|
|
27
|
+
quietStatus: RhythmStatus;
|
|
28
|
+
dreamStatus: RhythmStatus;
|
|
29
|
+
quietReason?: V8ReasonCode;
|
|
30
|
+
dreamReason?: V8ReasonCode;
|
|
31
|
+
quietCompletedAt?: string;
|
|
32
|
+
dreamCompletedAt?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface CheckDailyRhythmOptions {
|
|
35
|
+
now?: string;
|
|
36
|
+
forceQuiet?: boolean;
|
|
37
|
+
schedulerAvailable?: boolean;
|
|
38
|
+
}
|
|
39
|
+
export type CheckDailyRhythmResult = {
|
|
40
|
+
status: "checked";
|
|
41
|
+
state: DailyRhythmState;
|
|
42
|
+
} | DegradedOperationResult;
|
|
43
|
+
export declare function checkDailyRhythm(db: StateDatabase, options?: CheckDailyRhythmOptions): Promise<CheckDailyRhythmResult>;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DailyRhythmScheduler — Independent Quiet/Dream cadence with absence reasons.
|
|
3
|
+
*
|
|
4
|
+
* Core logic: Check if today's Quiet review is due (closures exist but no review
|
|
5
|
+
* yet), schedule/run it, then check Dream status. Records durable states so
|
|
6
|
+
* loop_status can report exact missing stages even when heartbeat does not
|
|
7
|
+
* select a quiet intent.
|
|
8
|
+
*
|
|
9
|
+
* Design authority:
|
|
10
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/dream-quiet-memory-system.md §4`
|
|
11
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/dream-quiet-memory-system.detail.md §3.1-§3.4`
|
|
12
|
+
*
|
|
13
|
+
* Dependencies:
|
|
14
|
+
* - `src/storage/v8-state-stores.js` (write/read DailyRhythmState)
|
|
15
|
+
* - `src/core/second-nature/quiet-dream/quiet-daily-review-builder.js`
|
|
16
|
+
* - `src/core/second-nature/quiet-dream/dream-scheduler.js`
|
|
17
|
+
*
|
|
18
|
+
* Boundary:
|
|
19
|
+
* - Does not run consolidation; only schedules and records lifecycle.
|
|
20
|
+
* - Does not bypass Dream runner; only records due/completed/blocked.
|
|
21
|
+
*/
|
|
22
|
+
import { writeDailyRhythmState, readDailyRhythmStateByDay, readActionClosuresByDay, } from "../../../storage/v8-state-stores.js";
|
|
23
|
+
import { buildQuietDailyReview } from "./quiet-daily-review-builder.js";
|
|
24
|
+
import { scheduleDreamAfterQuiet } from "./dream-scheduler.js";
|
|
25
|
+
// ───────────────────────────────────────────────────────────────
|
|
26
|
+
// Helpers
|
|
27
|
+
// ───────────────────────────────────────────────────────────────
|
|
28
|
+
function todayString(now) {
|
|
29
|
+
return now.slice(0, 10);
|
|
30
|
+
}
|
|
31
|
+
// ───────────────────────────────────────────────────────────────
|
|
32
|
+
// Public API
|
|
33
|
+
// ───────────────────────────────────────────────────────────────
|
|
34
|
+
export async function checkDailyRhythm(db, options) {
|
|
35
|
+
const now = options?.now ?? new Date().toISOString();
|
|
36
|
+
const day = todayString(now);
|
|
37
|
+
// Read existing rhythm state
|
|
38
|
+
const existing = await readDailyRhythmStateByDay(db, day);
|
|
39
|
+
if (existing.degraded) {
|
|
40
|
+
return existing.degraded;
|
|
41
|
+
}
|
|
42
|
+
const state = existing.row
|
|
43
|
+
? {
|
|
44
|
+
day: existing.row.day,
|
|
45
|
+
quietStatus: existing.row.quietStatus,
|
|
46
|
+
dreamStatus: existing.row.dreamStatus,
|
|
47
|
+
quietReason: existing.row.quietReason,
|
|
48
|
+
dreamReason: existing.row.dreamReason,
|
|
49
|
+
quietCompletedAt: existing.row.quietCompletedAt ?? undefined,
|
|
50
|
+
dreamCompletedAt: existing.row.dreamCompletedAt ?? undefined,
|
|
51
|
+
}
|
|
52
|
+
: {
|
|
53
|
+
day,
|
|
54
|
+
quietStatus: "not_due",
|
|
55
|
+
dreamStatus: "not_due",
|
|
56
|
+
};
|
|
57
|
+
// Check if closures exist for today
|
|
58
|
+
const closuresRead = await readActionClosuresByDay(db, day);
|
|
59
|
+
if (closuresRead.degraded) {
|
|
60
|
+
return closuresRead.degraded;
|
|
61
|
+
}
|
|
62
|
+
const hasClosures = closuresRead.rows.length > 0;
|
|
63
|
+
// Determine Quiet status
|
|
64
|
+
if (state.quietStatus === "completed" || state.quietStatus === "blocked") {
|
|
65
|
+
// Already handled; don't re-run
|
|
66
|
+
}
|
|
67
|
+
else if (!hasClosures) {
|
|
68
|
+
state.quietStatus = "not_due";
|
|
69
|
+
state.quietReason = "quiet_empty_input";
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// Closures exist but Quiet not completed → due
|
|
73
|
+
state.quietStatus = "due";
|
|
74
|
+
state.quietReason = "quiet_empty_input";
|
|
75
|
+
// Auto-run Quiet if forced or if not yet attempted
|
|
76
|
+
if (options?.forceQuiet || state.quietStatus === "due") {
|
|
77
|
+
const quietResult = await buildQuietDailyReview(db, { day, now });
|
|
78
|
+
if (quietResult.status === "completed") {
|
|
79
|
+
state.quietStatus = "completed";
|
|
80
|
+
state.quietReason = "quiet_completed";
|
|
81
|
+
state.quietCompletedAt = now;
|
|
82
|
+
}
|
|
83
|
+
else if (quietResult.status === "empty") {
|
|
84
|
+
state.quietStatus = "skipped";
|
|
85
|
+
state.quietReason = quietResult.reason ?? "quiet_empty_input";
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// Degraded
|
|
89
|
+
state.quietStatus = "blocked";
|
|
90
|
+
state.quietReason = quietResult.reason ?? "state_unreadable";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Determine Dream status based on Quiet outcome
|
|
95
|
+
if (state.quietStatus === "completed") {
|
|
96
|
+
if (state.dreamStatus === "completed" || state.dreamStatus === "blocked") {
|
|
97
|
+
// Already handled
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
state.dreamStatus = "due";
|
|
101
|
+
state.dreamReason = "dream_scheduled";
|
|
102
|
+
// Schedule Dream
|
|
103
|
+
const quietId = `quiet_${day}`;
|
|
104
|
+
const dreamResult = await scheduleDreamAfterQuiet(db, quietId, {
|
|
105
|
+
now,
|
|
106
|
+
schedulerAvailable: options?.schedulerAvailable ?? true,
|
|
107
|
+
});
|
|
108
|
+
if ("reason" in dreamResult) {
|
|
109
|
+
state.dreamStatus = "blocked";
|
|
110
|
+
state.dreamReason = dreamResult.reason;
|
|
111
|
+
}
|
|
112
|
+
else if (dreamResult.status === "blocked") {
|
|
113
|
+
state.dreamStatus = "blocked";
|
|
114
|
+
state.dreamReason = dreamResult.reason ?? "dream_scheduler_unavailable";
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
state.dreamStatus = "scheduled";
|
|
118
|
+
state.dreamReason = "dream_scheduled";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else if (state.quietStatus === "not_due") {
|
|
123
|
+
state.dreamStatus = "not_due";
|
|
124
|
+
state.dreamReason = "quiet_empty_input";
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
// Quiet blocked/skipped → Dream cannot run
|
|
128
|
+
state.dreamStatus = "blocked";
|
|
129
|
+
state.dreamReason = "dream_blocked_redaction";
|
|
130
|
+
}
|
|
131
|
+
// Persist state
|
|
132
|
+
const writeResult = await writeDailyRhythmState(db, {
|
|
133
|
+
id: `rhythm_${day}`,
|
|
134
|
+
day,
|
|
135
|
+
quietStatus: state.quietStatus,
|
|
136
|
+
dreamStatus: state.dreamStatus,
|
|
137
|
+
quietReason: state.quietReason ?? null,
|
|
138
|
+
dreamReason: state.dreamReason ?? null,
|
|
139
|
+
quietCompletedAt: state.quietCompletedAt ?? null,
|
|
140
|
+
dreamCompletedAt: state.dreamCompletedAt ?? null,
|
|
141
|
+
sourceRefs: [
|
|
142
|
+
{
|
|
143
|
+
uri: `sn://rhythm/${day}`,
|
|
144
|
+
family: "dream_run",
|
|
145
|
+
id: `rhythm_${day}`,
|
|
146
|
+
redactionClass: "none",
|
|
147
|
+
resolveStatus: "resolvable",
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
payloadJson: JSON.stringify({ checkedAt: now, hasClosures: closuresRead.rows.length }),
|
|
151
|
+
updatedAt: now,
|
|
152
|
+
});
|
|
153
|
+
if ("reason" in writeResult) {
|
|
154
|
+
return writeResult;
|
|
155
|
+
}
|
|
156
|
+
return { status: "checked", state };
|
|
157
|
+
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
* Test coverage: tests/unit/dream/memory-projection-lifecycle.test.ts
|
|
22
22
|
*/
|
|
23
|
-
import { readMemoryProjectionsByTopic, writeLongTermMemoryProjection, } from "../../../storage/v8-state-stores.js";
|
|
23
|
+
import { readMemoryProjectionsByTopic, writeLongTermMemoryProjection, updateLongTermMemoryProjectionStatus, } from "../../../storage/v8-state-stores.js";
|
|
24
24
|
// ───────────────────────────────────────────────────────────────
|
|
25
25
|
// Public API
|
|
26
26
|
// ───────────────────────────────────────────────────────────────
|
|
@@ -44,21 +44,12 @@ export async function acceptMemoryProjection(db, candidateId, topicKey, memoryTe
|
|
|
44
44
|
const activeProjection = existing.rows.find((r) => r.status === "active" || r.status === "accepted");
|
|
45
45
|
let supersedesId;
|
|
46
46
|
if (activeProjection) {
|
|
47
|
-
// Supersede existing active projection
|
|
48
|
-
const supersedeResult = await
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
status: "superseded",
|
|
54
|
-
sourceRefs: parseSourceRefs(activeProjection.sourceRefsJson),
|
|
55
|
-
redactionClass: activeProjection.redactionClass,
|
|
56
|
-
lifecycleStatus: "superseded",
|
|
57
|
-
payloadJson: JSON.stringify({
|
|
58
|
-
supersededAt: now,
|
|
59
|
-
supersededBy: candidateId,
|
|
60
|
-
}),
|
|
61
|
-
});
|
|
47
|
+
// Supersede existing active projection — UPDATE instead of INSERT
|
|
48
|
+
const supersedeResult = await updateLongTermMemoryProjectionStatus(db, activeProjection.id, "superseded", JSON.stringify({
|
|
49
|
+
...parsePayloadJson(activeProjection.payloadJson),
|
|
50
|
+
supersededAt: now,
|
|
51
|
+
supersededBy: candidateId,
|
|
52
|
+
}));
|
|
62
53
|
if ("reason" in supersedeResult) {
|
|
63
54
|
return supersedeResult;
|
|
64
55
|
}
|
|
@@ -149,3 +140,13 @@ function parseSourceRefs(json) {
|
|
|
149
140
|
return [];
|
|
150
141
|
}
|
|
151
142
|
}
|
|
143
|
+
function parsePayloadJson(json) {
|
|
144
|
+
if (!json)
|
|
145
|
+
return {};
|
|
146
|
+
try {
|
|
147
|
+
return JSON.parse(json);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return {};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -27,7 +27,10 @@ export interface QuietDailyReviewResult {
|
|
|
27
27
|
day: string;
|
|
28
28
|
closureCount: number;
|
|
29
29
|
memoryCandidateCount: number;
|
|
30
|
+
/** Generic source refs (closure + perception + other) */
|
|
30
31
|
sourceRefs: SourceRef[];
|
|
32
|
+
/** Explicit closure refs — first-class provenance for reviewed ActionClosureRecords */
|
|
33
|
+
closureRefs: SourceRef[];
|
|
31
34
|
reviewSummary: string;
|
|
32
35
|
importanceSignals: string[];
|
|
33
36
|
createdAt: string;
|