@haaaiawd/second-nature 0.2.4 → 0.2.6
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/commands/index.d.ts +4 -0
- package/runtime/cli/commands/index.js +179 -5
- package/runtime/cli/index.js +2 -0
- package/runtime/cli/ops/ops-router.js +27 -17
- package/runtime/connectors/base/contract.d.ts +1 -0
- package/runtime/connectors/base/failure-taxonomy.js +45 -26
- package/runtime/connectors/services/connector-cooldown-port.d.ts +22 -0
- package/runtime/connectors/services/connector-cooldown-port.js +123 -0
- package/runtime/connectors/services/connector-executor-adapter.js +10 -4
- package/runtime/connectors/services/credential-route-context.d.ts +3 -2
- package/runtime/connectors/services/credential-route-context.js +19 -3
- package/runtime/core/second-nature/action/action-closure-recorder.d.ts +4 -0
- package/runtime/core/second-nature/action/action-closure-recorder.js +5 -0
- package/runtime/core/second-nature/action/action-proposal-builder.js +1 -0
- package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.d.ts +2 -0
- package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.js +76 -0
- package/runtime/core/second-nature/control-plane/real-runtime-spine.d.ts +2 -0
- package/runtime/core/second-nature/control-plane/real-runtime-spine.js +1 -0
- package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.d.ts +1 -1
- package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.js +10 -5
- package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.d.ts +2 -2
- package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.js +10 -28
- package/runtime/observability/db/index.d.ts +2 -0
- package/runtime/observability/db/index.js +6 -0
- package/runtime/observability/living-loop-health-gate.d.ts +6 -2
- package/runtime/observability/living-loop-health-gate.js +52 -5
- package/runtime/observability/loop-status.d.ts +19 -0
- package/runtime/observability/loop-status.js +121 -7
- package/runtime/observability/services/heartbeat-digest-assembler.d.ts +9 -0
- package/runtime/observability/services/heartbeat-digest-assembler.js +44 -9
- package/runtime/shared/types/v8-contracts.d.ts +1 -1
- package/runtime/storage/db/index.d.ts +2 -0
- package/runtime/storage/db/index.js +28 -2
- package/runtime/storage/db/schema/v8-entities.d.ts +288 -0
- package/runtime/storage/db/schema/v8-entities.js +23 -1
- package/runtime/storage/v8-state-stores.d.ts +10 -1
- package/runtime/storage/v8-state-stores.js +86 -1
|
@@ -19,6 +19,7 @@ import { parseConnectorManifestV6 } from "../manifest/manifest-parser.js";
|
|
|
19
19
|
import fs from "node:fs";
|
|
20
20
|
import path from "node:path";
|
|
21
21
|
import { pathToFileURL } from "node:url";
|
|
22
|
+
import { createConnectorCooldownPort } from "./connector-cooldown-port.js";
|
|
22
23
|
const DEFAULT_AGENT_WORLD_USERNAME = "nyx_ha";
|
|
23
24
|
const DEFAULT_AGENT_WORLD_PROFILE_PATH_TEMPLATE = "/api/agents/profile/{username}";
|
|
24
25
|
function readString(value) {
|
|
@@ -104,7 +105,7 @@ async function fetchAgentWorldJson(input) {
|
|
|
104
105
|
body: input.body === undefined ? undefined : JSON.stringify(input.body),
|
|
105
106
|
});
|
|
106
107
|
if (!resp.ok) {
|
|
107
|
-
throw { code: "api_error", detail: `agent-world ${input.label}: ${resp.status}` };
|
|
108
|
+
throw { code: "api_error", status: resp.status, detail: `agent-world ${input.label}: ${resp.status}` };
|
|
108
109
|
}
|
|
109
110
|
return resp.json();
|
|
110
111
|
}
|
|
@@ -147,7 +148,7 @@ async function fetchEvoMapJson(input) {
|
|
|
147
148
|
body: input.body === undefined ? undefined : JSON.stringify(input.body),
|
|
148
149
|
});
|
|
149
150
|
if (!resp.ok) {
|
|
150
|
-
throw { code: "api_error", detail: `evomap ${input.label}: ${resp.status}` };
|
|
151
|
+
throw { code: "api_error", status: resp.status, detail: `evomap ${input.label}: ${resp.status}` };
|
|
151
152
|
}
|
|
152
153
|
return resp.json();
|
|
153
154
|
}
|
|
@@ -256,6 +257,8 @@ function createDeclarativeHttpRunner(manifest, credential) {
|
|
|
256
257
|
body: method !== "GET" && request.payload ? JSON.stringify(request.payload) : undefined,
|
|
257
258
|
});
|
|
258
259
|
if (!resp.ok) {
|
|
260
|
+
const status = resp.status;
|
|
261
|
+
const body = await resp.text().catch(() => "");
|
|
259
262
|
return {
|
|
260
263
|
platformId: request.platformId,
|
|
261
264
|
channel: plan.channel,
|
|
@@ -263,7 +266,8 @@ function createDeclarativeHttpRunner(manifest, credential) {
|
|
|
263
266
|
success: false,
|
|
264
267
|
error: {
|
|
265
268
|
code: "api_error",
|
|
266
|
-
|
|
269
|
+
status,
|
|
270
|
+
detail: `HTTP ${status}${body ? `: ${body.slice(0, 200)}` : ""}`,
|
|
267
271
|
},
|
|
268
272
|
};
|
|
269
273
|
}
|
|
@@ -618,7 +622,8 @@ export function createConnectorExecutorAdapter(options) {
|
|
|
618
622
|
registry.register({ ...agentWorldManifest });
|
|
619
623
|
registry.register({ ...instreetManifest });
|
|
620
624
|
registerWorkspaceManifests(registry, options.workspaceRoot);
|
|
621
|
-
const
|
|
625
|
+
const cooldownPort = createConnectorCooldownPort(options.stateDb);
|
|
626
|
+
const routeContextPort = createCredentialRouteContextPort(vault, options.stateDb);
|
|
622
627
|
const routePlanner = new ConnectorRoutePlanner(registry, routeContextPort, new ChannelHealthStore());
|
|
623
628
|
const telemetry = new ExecutionTelemetry(options.observabilityDb);
|
|
624
629
|
const executionRunner = createAdaptiveExecutionRunner(vault, options.workspaceRoot);
|
|
@@ -626,6 +631,7 @@ export function createConnectorExecutorAdapter(options) {
|
|
|
626
631
|
routePlanner,
|
|
627
632
|
executionRunner,
|
|
628
633
|
telemetry,
|
|
634
|
+
cooldownPort,
|
|
629
635
|
effectCommitLedger: new InMemoryEffectCommitLedger(),
|
|
630
636
|
retryPolicy: { maxRetries: 2, jitter: true },
|
|
631
637
|
});
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Loads decrypted credentials from state DB and maps them to the
|
|
5
5
|
* CredentialContext shape expected by ConnectorRoutePlanner.
|
|
6
|
-
* Cooldown is
|
|
6
|
+
* Cooldown state is loaded from connector_cooldown_state table.
|
|
7
7
|
*/
|
|
8
8
|
import type { RouteContextPort } from "../base/contract.js";
|
|
9
9
|
import type { CredentialVault } from "../../storage/services/credential-vault.js";
|
|
10
|
-
|
|
10
|
+
import type { StateDatabase } from "../../storage/db/index.js";
|
|
11
|
+
export declare function createCredentialRouteContextPort(vault: CredentialVault, db: StateDatabase): RouteContextPort;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { readConnectorCooldownState } from "../../storage/v8-state-stores.js";
|
|
2
|
+
export function createCredentialRouteContextPort(vault, db) {
|
|
2
3
|
return {
|
|
3
4
|
async loadCredentialState(platformId) {
|
|
4
5
|
const ctx = await vault.loadCredentialContext(platformId);
|
|
@@ -12,8 +13,23 @@ export function createCredentialRouteContextPort(vault) {
|
|
|
12
13
|
}
|
|
13
14
|
return ctx;
|
|
14
15
|
},
|
|
15
|
-
async loadCooldownState() {
|
|
16
|
-
|
|
16
|
+
async loadCooldownState(platformId, intent) {
|
|
17
|
+
const read = await readConnectorCooldownState(db, platformId, intent);
|
|
18
|
+
if (read.degraded) {
|
|
19
|
+
// Fail-closed on unreadable cooldown state
|
|
20
|
+
return { blocked: true, reason: "cooldown_state_unreadable" };
|
|
21
|
+
}
|
|
22
|
+
if (!read.row) {
|
|
23
|
+
return { blocked: false };
|
|
24
|
+
}
|
|
25
|
+
const now = new Date().toISOString();
|
|
26
|
+
const blocked = new Date(read.row.blockedUntil).getTime() > new Date(now).getTime();
|
|
27
|
+
return {
|
|
28
|
+
blocked,
|
|
29
|
+
retryAfterMs: blocked
|
|
30
|
+
? Math.max(0, new Date(read.row.blockedUntil).getTime() - new Date(now).getTime())
|
|
31
|
+
: undefined,
|
|
32
|
+
};
|
|
17
33
|
},
|
|
18
34
|
};
|
|
19
35
|
}
|
|
@@ -56,6 +56,8 @@ export declare function recordRememberClosure(db: StateDatabase, cycleId: string
|
|
|
56
56
|
export declare function recordPolicyOutcomeClosure(db: StateDatabase, cycleId: string, closureStatus: ClosureStatus, reason: V8ReasonCode, params: {
|
|
57
57
|
proposalId?: string;
|
|
58
58
|
decisionId?: string;
|
|
59
|
+
platformId?: string;
|
|
60
|
+
capabilityId?: string;
|
|
59
61
|
downgradedActionKind?: string;
|
|
60
62
|
postProcessing?: string[];
|
|
61
63
|
nextState?: string;
|
|
@@ -63,6 +65,8 @@ export declare function recordPolicyOutcomeClosure(db: StateDatabase, cycleId: s
|
|
|
63
65
|
export declare function recordExecutionClosure(db: StateDatabase, cycleId: string, closureStatus: "completed" | "failed", reason: V8ReasonCode, params: {
|
|
64
66
|
proposalId: string;
|
|
65
67
|
decisionId: string;
|
|
68
|
+
platformId?: string;
|
|
69
|
+
capabilityId?: string;
|
|
66
70
|
executionResultRef?: string;
|
|
67
71
|
outputSummary?: string;
|
|
68
72
|
nextState?: string;
|
|
@@ -46,6 +46,7 @@ export async function recordNoActionClosure(db, cycleId, noActionReason, options
|
|
|
46
46
|
id: closureId,
|
|
47
47
|
createdAt: now,
|
|
48
48
|
cycleId,
|
|
49
|
+
platformId: "heartbeat",
|
|
49
50
|
status: "no_action",
|
|
50
51
|
reason: noActionReason,
|
|
51
52
|
nextState: "await_next_cycle",
|
|
@@ -122,6 +123,8 @@ export async function recordPolicyOutcomeClosure(db, cycleId, closureStatus, rea
|
|
|
122
123
|
id: closureId,
|
|
123
124
|
createdAt: now,
|
|
124
125
|
cycleId,
|
|
126
|
+
platformId: params.platformId ?? "heartbeat",
|
|
127
|
+
capabilityId: params.capabilityId,
|
|
125
128
|
proposalId: params.proposalId,
|
|
126
129
|
decisionId: params.decisionId,
|
|
127
130
|
status: closureStatus,
|
|
@@ -161,6 +164,8 @@ export async function recordExecutionClosure(db, cycleId, closureStatus, reason,
|
|
|
161
164
|
id: closureId,
|
|
162
165
|
createdAt: now,
|
|
163
166
|
cycleId,
|
|
167
|
+
platformId: params.platformId ?? "heartbeat",
|
|
168
|
+
capabilityId: params.capabilityId,
|
|
164
169
|
proposalId: params.proposalId,
|
|
165
170
|
decisionId: params.decisionId,
|
|
166
171
|
status: closureStatus,
|
|
@@ -136,6 +136,7 @@ export async function buildActionProposal(db, judgmentVerdictId, options) {
|
|
|
136
136
|
id: closureId,
|
|
137
137
|
createdAt: now,
|
|
138
138
|
cycleId,
|
|
139
|
+
platformId: "heartbeat",
|
|
139
140
|
status: "completed",
|
|
140
141
|
reason: "remember_for_review",
|
|
141
142
|
nextState: "pending_daily_review",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* Test coverage: tests/unit/control-plane/heartbeat-cycle-trace.test.ts
|
|
23
23
|
*/
|
|
24
24
|
import type { StateDatabase } from "../../../storage/db/index.js";
|
|
25
|
+
import { type DailyRhythmState } from "../quiet-dream/daily-rhythm-scheduler.js";
|
|
25
26
|
import type { SourceRef, DegradedOperationResult, V8ReasonCode } from "../../../shared/types/v8-contracts.js";
|
|
26
27
|
export interface HeartbeatOrchestrationRequest {
|
|
27
28
|
workspaceRoot: string;
|
|
@@ -34,5 +35,6 @@ export interface HeartbeatOrchestrationResult {
|
|
|
34
35
|
closureRef?: SourceRef;
|
|
35
36
|
noActionReason?: V8ReasonCode;
|
|
36
37
|
degraded?: DegradedOperationResult;
|
|
38
|
+
rhythmState?: DailyRhythmState;
|
|
37
39
|
}
|
|
38
40
|
export declare function runHeartbeatCycle(db: StateDatabase, request: HeartbeatOrchestrationRequest): Promise<HeartbeatOrchestrationResult | DegradedOperationResult>;
|
|
@@ -30,6 +30,7 @@ import { buildActionProposal, } from "../action/action-proposal-builder.js";
|
|
|
30
30
|
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
|
+
import { checkDailyRhythm } from "../quiet-dream/daily-rhythm-scheduler.js";
|
|
33
34
|
// ───────────────────────────────────────────────────────────────
|
|
34
35
|
// Helpers
|
|
35
36
|
// ───────────────────────────────────────────────────────────────
|
|
@@ -43,6 +44,66 @@ async function nextCycleSequence(db) {
|
|
|
43
44
|
function buildCycleId(sequence, now) {
|
|
44
45
|
return `cyc_${now.replace(/[:.]/g, "")}_${sequence}`;
|
|
45
46
|
}
|
|
47
|
+
async function advanceAndRecordDailyRhythm(db, cycleId, cycleSequence, cycleRef, now) {
|
|
48
|
+
try {
|
|
49
|
+
const rhythmResult = await checkDailyRhythm(db, { now });
|
|
50
|
+
if ("status" in rhythmResult && rhythmResult.status === "checked") {
|
|
51
|
+
await recordLoopStageEvent(db, {
|
|
52
|
+
id: `evt_${cycleId}_daily_rhythm`,
|
|
53
|
+
cycleId,
|
|
54
|
+
cycleSequence,
|
|
55
|
+
stage: "quiet",
|
|
56
|
+
status: "completed",
|
|
57
|
+
occurredAt: new Date().toISOString(),
|
|
58
|
+
sourceRefs: [
|
|
59
|
+
cycleRef,
|
|
60
|
+
{
|
|
61
|
+
uri: `sn://rhythm/${rhythmResult.state.day}`,
|
|
62
|
+
family: "dream_run",
|
|
63
|
+
id: `rhythm_${rhythmResult.state.day}`,
|
|
64
|
+
redactionClass: "none",
|
|
65
|
+
resolveStatus: "resolvable",
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
return { rhythmState: rhythmResult.state };
|
|
70
|
+
}
|
|
71
|
+
const degraded = rhythmResult;
|
|
72
|
+
await recordLoopStageEvent(db, {
|
|
73
|
+
id: `evt_${cycleId}_daily_rhythm`,
|
|
74
|
+
cycleId,
|
|
75
|
+
cycleSequence,
|
|
76
|
+
stage: "quiet",
|
|
77
|
+
status: "failed",
|
|
78
|
+
occurredAt: new Date().toISOString(),
|
|
79
|
+
reason: degraded.reason,
|
|
80
|
+
sourceRefs: [cycleRef],
|
|
81
|
+
});
|
|
82
|
+
return { rhythmDegraded: degraded };
|
|
83
|
+
}
|
|
84
|
+
catch (rhythmErr) {
|
|
85
|
+
const errMsg = rhythmErr instanceof Error ? rhythmErr.message : String(rhythmErr);
|
|
86
|
+
const degraded = {
|
|
87
|
+
status: "degraded",
|
|
88
|
+
reason: "state_unreadable",
|
|
89
|
+
ownerStage: "quiet",
|
|
90
|
+
sourceRefs: [cycleRef],
|
|
91
|
+
operatorNextAction: `Daily rhythm check failed: ${errMsg.slice(0, 120)}`,
|
|
92
|
+
retryable: true,
|
|
93
|
+
};
|
|
94
|
+
await recordLoopStageEvent(db, {
|
|
95
|
+
id: `evt_${cycleId}_daily_rhythm`,
|
|
96
|
+
cycleId,
|
|
97
|
+
cycleSequence,
|
|
98
|
+
stage: "quiet",
|
|
99
|
+
status: "failed",
|
|
100
|
+
occurredAt: new Date().toISOString(),
|
|
101
|
+
reason: degraded.reason,
|
|
102
|
+
sourceRefs: [cycleRef],
|
|
103
|
+
});
|
|
104
|
+
return { rhythmDegraded: degraded };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
46
107
|
// ───────────────────────────────────────────────────────────────
|
|
47
108
|
// Public API
|
|
48
109
|
// ───────────────────────────────────────────────────────────────
|
|
@@ -130,6 +191,7 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
130
191
|
reason: degradedReason,
|
|
131
192
|
sourceRefs: degradedClosureRef ? [degradedClosureRef, cycleRef] : [cycleRef],
|
|
132
193
|
});
|
|
194
|
+
const { rhythmState } = await advanceAndRecordDailyRhythm(db, cycleId, cycleSequence, cycleRef, now);
|
|
133
195
|
return {
|
|
134
196
|
cycleId,
|
|
135
197
|
cycleSequence,
|
|
@@ -145,6 +207,7 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
145
207
|
retryable: true,
|
|
146
208
|
}
|
|
147
209
|
: undefined,
|
|
210
|
+
rhythmState,
|
|
148
211
|
};
|
|
149
212
|
}
|
|
150
213
|
const cards = perceptionResult.cards;
|
|
@@ -182,11 +245,13 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
182
245
|
reason: "evidence_batch_empty",
|
|
183
246
|
sourceRefs: emptyClosureRef ? [emptyClosureRef, cycleRef] : [cycleRef],
|
|
184
247
|
});
|
|
248
|
+
const { rhythmState } = await advanceAndRecordDailyRhythm(db, cycleId, cycleSequence, cycleRef, now);
|
|
185
249
|
return {
|
|
186
250
|
cycleId,
|
|
187
251
|
cycleSequence,
|
|
188
252
|
closureRef: emptyClosureRef,
|
|
189
253
|
noActionReason: "evidence_batch_empty",
|
|
254
|
+
rhythmState,
|
|
190
255
|
};
|
|
191
256
|
}
|
|
192
257
|
// ── Context assembly: load accepted projections (T-DQ.R.3) ──
|
|
@@ -350,6 +415,8 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
350
415
|
const closureResult = await recordPolicyOutcomeClosure(db, cycleId, closureStatus, decision.decisionReason, {
|
|
351
416
|
proposalId: proposal.id,
|
|
352
417
|
decisionId: decision.id,
|
|
418
|
+
platformId: proposal.targetPlatformId,
|
|
419
|
+
capabilityId: proposal.targetCapabilityId,
|
|
353
420
|
nextState: "await_next_cycle",
|
|
354
421
|
}, { now });
|
|
355
422
|
if ("closureId" in closureResult) {
|
|
@@ -369,6 +436,8 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
369
436
|
const closureResult = await recordPolicyOutcomeClosure(db, cycleId, "downgraded", "guidance_unavailable", {
|
|
370
437
|
proposalId: proposal.id,
|
|
371
438
|
decisionId: decision.id,
|
|
439
|
+
platformId: proposal.targetPlatformId,
|
|
440
|
+
capabilityId: proposal.targetCapabilityId,
|
|
372
441
|
downgradedActionKind: dispatchResult.downgradedActionKind,
|
|
373
442
|
nextState: "await_guidance_recovery",
|
|
374
443
|
}, { now });
|
|
@@ -390,6 +459,8 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
390
459
|
const closureResult = await recordExecutionClosure(db, cycleId, "completed", "policy_allowed", {
|
|
391
460
|
proposalId: proposal.id,
|
|
392
461
|
decisionId: decision.id,
|
|
462
|
+
platformId: proposal.targetPlatformId,
|
|
463
|
+
capabilityId: proposal.targetCapabilityId,
|
|
393
464
|
outputSummary: "Guidance draft dispatched (simulated)",
|
|
394
465
|
nextState: "await_next_cycle",
|
|
395
466
|
}, { now });
|
|
@@ -411,6 +482,8 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
411
482
|
const closureResult = await recordExecutionClosure(db, cycleId, "completed", "policy_allowed", {
|
|
412
483
|
proposalId: proposal.id,
|
|
413
484
|
decisionId: decision.id,
|
|
485
|
+
platformId: proposal.targetPlatformId,
|
|
486
|
+
capabilityId: proposal.targetCapabilityId,
|
|
414
487
|
outputSummary: "Connector dispatch prepared (simulated — T-CP.R.2)",
|
|
415
488
|
nextState: "await_real_execution",
|
|
416
489
|
}, { now });
|
|
@@ -466,11 +539,14 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
466
539
|
}
|
|
467
540
|
noActionReason = "proposal_no_action";
|
|
468
541
|
}
|
|
542
|
+
// T-CP.R.3: Advance daily rhythm after closure/no-action
|
|
543
|
+
const { rhythmState } = await advanceAndRecordDailyRhythm(db, cycleId, cycleSequence, cycleRef, now);
|
|
469
544
|
return {
|
|
470
545
|
cycleId,
|
|
471
546
|
cycleSequence,
|
|
472
547
|
closureRef,
|
|
473
548
|
noActionReason,
|
|
474
549
|
degraded: closureDegraded,
|
|
550
|
+
rhythmState,
|
|
475
551
|
};
|
|
476
552
|
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import type { StateDatabase } from "../../../storage/db/index.js";
|
|
19
19
|
import type { SourceRef, DegradedOperationResult, V8ReasonCode } from "../../../shared/types/v8-contracts.js";
|
|
20
|
+
import type { DailyRhythmState } from "../quiet-dream/daily-rhythm-scheduler.js";
|
|
20
21
|
export interface RealRuntimeSpineOptions {
|
|
21
22
|
workspaceRoot: string;
|
|
22
23
|
state: StateDatabase;
|
|
@@ -29,5 +30,6 @@ export interface RealRuntimeSpineResult {
|
|
|
29
30
|
closureRef?: SourceRef;
|
|
30
31
|
noActionReason?: V8ReasonCode;
|
|
31
32
|
degraded?: DegradedOperationResult;
|
|
33
|
+
rhythmState?: DailyRhythmState;
|
|
32
34
|
}
|
|
33
35
|
export declare function runRealRuntimeHeartbeatCycle(options: RealRuntimeSpineOptions): Promise<RealRuntimeSpineResult | DegradedOperationResult>;
|
|
@@ -37,5 +37,6 @@ export async function runRealRuntimeHeartbeatCycle(options) {
|
|
|
37
37
|
closureRef: orchestrationResult.closureRef,
|
|
38
38
|
noActionReason: orchestrationResult.noActionReason,
|
|
39
39
|
degraded: orchestrationResult.degraded,
|
|
40
|
+
rhythmState: orchestrationResult.rhythmState,
|
|
40
41
|
};
|
|
41
42
|
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import type { StateDatabase } from "../../../storage/db/index.js";
|
|
23
23
|
import type { DegradedOperationResult, V8ReasonCode } from "../../../shared/types/v8-contracts.js";
|
|
24
|
-
export type RhythmStatus = "due" | "completed" | "skipped" | "blocked" | "not_due";
|
|
24
|
+
export type RhythmStatus = "due" | "completed" | "scheduled" | "skipped" | "blocked" | "not_due";
|
|
25
25
|
export interface DailyRhythmState {
|
|
26
26
|
day: string;
|
|
27
27
|
quietStatus: RhythmStatus;
|
|
@@ -71,7 +71,6 @@ export async function checkDailyRhythm(db, options) {
|
|
|
71
71
|
else {
|
|
72
72
|
// Closures exist but Quiet not completed → due
|
|
73
73
|
state.quietStatus = "due";
|
|
74
|
-
state.quietReason = "quiet_empty_input";
|
|
75
74
|
// Auto-run Quiet if forced or if not yet attempted
|
|
76
75
|
if (options?.forceQuiet || state.quietStatus === "due") {
|
|
77
76
|
const quietResult = await buildQuietDailyReview(db, { day, now });
|
|
@@ -93,8 +92,10 @@ export async function checkDailyRhythm(db, options) {
|
|
|
93
92
|
}
|
|
94
93
|
// Determine Dream status based on Quiet outcome
|
|
95
94
|
if (state.quietStatus === "completed") {
|
|
96
|
-
if (state.dreamStatus === "completed" ||
|
|
97
|
-
|
|
95
|
+
if (state.dreamStatus === "completed" ||
|
|
96
|
+
state.dreamStatus === "scheduled" ||
|
|
97
|
+
state.dreamStatus === "blocked") {
|
|
98
|
+
// Already handled; do not re-schedule
|
|
98
99
|
}
|
|
99
100
|
else {
|
|
100
101
|
state.dreamStatus = "due";
|
|
@@ -123,10 +124,14 @@ export async function checkDailyRhythm(db, options) {
|
|
|
123
124
|
state.dreamStatus = "not_due";
|
|
124
125
|
state.dreamReason = "quiet_empty_input";
|
|
125
126
|
}
|
|
127
|
+
else if (state.quietStatus === "skipped") {
|
|
128
|
+
state.dreamStatus = "blocked";
|
|
129
|
+
state.dreamReason = state.quietReason ?? "quiet_empty_input";
|
|
130
|
+
}
|
|
126
131
|
else {
|
|
127
|
-
// Quiet blocked
|
|
132
|
+
// Quiet blocked (degraded) → Dream cannot run
|
|
128
133
|
state.dreamStatus = "blocked";
|
|
129
|
-
state.dreamReason = "dream_blocked_redaction";
|
|
134
|
+
state.dreamReason = state.quietReason ?? "dream_blocked_redaction";
|
|
130
135
|
}
|
|
131
136
|
// Persist state
|
|
132
137
|
const writeResult = await writeDailyRhythmState(db, {
|
|
@@ -32,5 +32,5 @@ export interface AcceptMemoryProjectionOptions {
|
|
|
32
32
|
now?: string;
|
|
33
33
|
}
|
|
34
34
|
export declare function acceptMemoryProjection(db: StateDatabase, candidateId: string, topicKey: string, memoryText: string, sourceRefs: SourceRef[], options?: AcceptMemoryProjectionOptions): Promise<ProjectionLifecycleResult | DegradedOperationResult>;
|
|
35
|
-
export declare function rejectMemoryProjection(db: StateDatabase, projectionId: string,
|
|
36
|
-
export declare function retireMemoryProjection(db: StateDatabase, projectionId: string,
|
|
35
|
+
export declare function rejectMemoryProjection(db: StateDatabase, projectionId: string, _candidateId: string, _topicKey: string, _sourceRefs: SourceRef[], reason?: V8ReasonCode, options?: AcceptMemoryProjectionOptions): Promise<ProjectionLifecycleResult | DegradedOperationResult>;
|
|
36
|
+
export declare function retireMemoryProjection(db: StateDatabase, projectionId: string, _candidateId: string, _topicKey: string, _sourceRefs: SourceRef[], options?: AcceptMemoryProjectionOptions): Promise<ProjectionLifecycleResult | DegradedOperationResult>;
|
|
@@ -82,21 +82,12 @@ export async function acceptMemoryProjection(db, candidateId, topicKey, memoryTe
|
|
|
82
82
|
supersedesProjectionId: supersedesId,
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
|
-
export async function rejectMemoryProjection(db, projectionId,
|
|
85
|
+
export async function rejectMemoryProjection(db, projectionId, _candidateId, _topicKey, _sourceRefs, reason = "projection_rejected", options) {
|
|
86
86
|
const now = options?.now ?? new Date().toISOString();
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
topicKey,
|
|
92
|
-
status: "rejected",
|
|
93
|
-
sourceRefs,
|
|
94
|
-
redactionClass: "none",
|
|
95
|
-
lifecycleStatus: "rejected",
|
|
96
|
-
payloadJson: JSON.stringify({ rejectedAt: now, reason }),
|
|
97
|
-
});
|
|
98
|
-
if ("reason" in writeResult) {
|
|
99
|
-
return writeResult;
|
|
87
|
+
// F5: Use UPDATE instead of INSERT to avoid PK conflict on existing projections
|
|
88
|
+
const updateResult = await updateLongTermMemoryProjectionStatus(db, projectionId, "rejected", JSON.stringify({ rejectedAt: now, reason }));
|
|
89
|
+
if ("reason" in updateResult) {
|
|
90
|
+
return updateResult;
|
|
100
91
|
}
|
|
101
92
|
return {
|
|
102
93
|
projectionId,
|
|
@@ -104,21 +95,12 @@ export async function rejectMemoryProjection(db, projectionId, candidateId, topi
|
|
|
104
95
|
reason,
|
|
105
96
|
};
|
|
106
97
|
}
|
|
107
|
-
export async function retireMemoryProjection(db, projectionId,
|
|
98
|
+
export async function retireMemoryProjection(db, projectionId, _candidateId, _topicKey, _sourceRefs, options) {
|
|
108
99
|
const now = options?.now ?? new Date().toISOString();
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
topicKey,
|
|
114
|
-
status: "retired",
|
|
115
|
-
sourceRefs,
|
|
116
|
-
redactionClass: "none",
|
|
117
|
-
lifecycleStatus: "retired",
|
|
118
|
-
payloadJson: JSON.stringify({ retiredAt: now }),
|
|
119
|
-
});
|
|
120
|
-
if ("reason" in writeResult) {
|
|
121
|
-
return writeResult;
|
|
100
|
+
// F5: Use UPDATE instead of INSERT to avoid PK conflict on existing projections
|
|
101
|
+
const updateResult = await updateLongTermMemoryProjectionStatus(db, projectionId, "retired", JSON.stringify({ retiredAt: now }));
|
|
102
|
+
if ("reason" in updateResult) {
|
|
103
|
+
return updateResult;
|
|
122
104
|
}
|
|
123
105
|
return {
|
|
124
106
|
projectionId,
|
|
@@ -5,6 +5,8 @@ export interface ObservabilityDatabase {
|
|
|
5
5
|
sqlite: Database;
|
|
6
6
|
db: ReturnType<typeof drizzle<typeof schema>>;
|
|
7
7
|
schema: typeof schema;
|
|
8
|
+
/** Persist in-memory sql.js state to disk without closing the connection. */
|
|
9
|
+
flush(): void;
|
|
8
10
|
close(): void;
|
|
9
11
|
}
|
|
10
12
|
export declare function createObservabilityDatabase(filename?: string): ObservabilityDatabase;
|
|
@@ -126,6 +126,12 @@ export function createObservabilityDatabase(filename = "observability.db") {
|
|
|
126
126
|
sqlite,
|
|
127
127
|
db,
|
|
128
128
|
schema,
|
|
129
|
+
flush() {
|
|
130
|
+
if (!isMemory) {
|
|
131
|
+
const data = sqlite.export();
|
|
132
|
+
fs.writeFileSync(dbPath, Buffer.from(data));
|
|
133
|
+
}
|
|
134
|
+
},
|
|
129
135
|
close() {
|
|
130
136
|
if (!isMemory) {
|
|
131
137
|
const data = sqlite.export();
|
|
@@ -25,14 +25,18 @@ export interface RealRunHealthGate {
|
|
|
25
25
|
hasQuietArtifact: boolean;
|
|
26
26
|
/** Has a scheduled or completed DreamConsolidationRun */
|
|
27
27
|
hasDreamArtifact: boolean;
|
|
28
|
+
/** Has a fresh impulse context artifact (within 24h) */
|
|
29
|
+
hasFreshImpulseContext: boolean;
|
|
30
|
+
/** Has at least one accepted or active long-term memory projection */
|
|
31
|
+
hasProjectionFeedback: boolean;
|
|
28
32
|
/** True if only contract smoke (cycle traces) but no real artifacts */
|
|
29
33
|
contractSmokeOnly: boolean;
|
|
30
|
-
/** True if closure exists but no runtime-produced cycle trace backs it */
|
|
34
|
+
/** True if closure exists but no runtime-produced cycle trace + stage event backs it */
|
|
31
35
|
seededStateDetected: boolean;
|
|
32
36
|
/** True only when real runtime activity is proven (not seeded, not smoke-only) */
|
|
33
37
|
gatePassed: boolean;
|
|
34
38
|
/** Explicit missing stage reason */
|
|
35
|
-
missingStage?: "closure" | "quiet" | "dream" | "none";
|
|
39
|
+
missingStage?: "closure" | "quiet" | "dream" | "impulse" | "projection" | "none";
|
|
36
40
|
missingReason?: string;
|
|
37
41
|
}
|
|
38
42
|
export type RealRunHealthResult = {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* - Read-only diagnostic; does not modify state.
|
|
17
17
|
* - Reports explicit absence reasons instead of silent zeros.
|
|
18
18
|
*/
|
|
19
|
-
import { readActionClosuresByDay, readDailyRhythmStateByDay, readHeartbeatCycleTraces, } from "../storage/v8-state-stores.js";
|
|
19
|
+
import { readActionClosuresByDay, readDailyRhythmStateByDay, readHeartbeatCycleTraces, readLoopStageEventsByCycle, readImpulseContextArtifact, readMemoryProjectionsByStatus, } from "../storage/v8-state-stores.js";
|
|
20
20
|
// ───────────────────────────────────────────────────────────────
|
|
21
21
|
// Public API
|
|
22
22
|
// ───────────────────────────────────────────────────────────────
|
|
@@ -28,7 +28,7 @@ export async function checkRealRunHealth(db, day) {
|
|
|
28
28
|
return { ok: false, degraded: closureResult.degraded };
|
|
29
29
|
}
|
|
30
30
|
const hasRealClosure = closureResult.rows.length > 0;
|
|
31
|
-
// Check if closures are runtime-produced (backed by cycle trace + stage
|
|
31
|
+
// Check if closures are runtime-produced (backed by cycle trace + closure stage event + source refs)
|
|
32
32
|
let seededStateDetected = false;
|
|
33
33
|
if (hasRealClosure) {
|
|
34
34
|
const traces = await readHeartbeatCycleTraces(db, 1000);
|
|
@@ -41,6 +41,29 @@ export async function checkRealRunHealth(db, day) {
|
|
|
41
41
|
seededStateDetected = true;
|
|
42
42
|
break;
|
|
43
43
|
}
|
|
44
|
+
// F3: verify closure has corresponding loop_stage_event with stage="closure" and status="completed"
|
|
45
|
+
const stageEvents = await readLoopStageEventsByCycle(db, closure.cycleId);
|
|
46
|
+
if (stageEvents.degraded) {
|
|
47
|
+
return { ok: false, degraded: stageEvents.degraded };
|
|
48
|
+
}
|
|
49
|
+
const hasClosureStageEvent = stageEvents.rows.some((e) => e.stage === "closure" && e.status === "completed");
|
|
50
|
+
if (!hasClosureStageEvent) {
|
|
51
|
+
seededStateDetected = true;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
// F3: verify closure has non-empty source refs
|
|
55
|
+
const sourceRefsJson = closure.sourceRefsJson ?? "[]";
|
|
56
|
+
let sourceRefs = [];
|
|
57
|
+
try {
|
|
58
|
+
sourceRefs = JSON.parse(sourceRefsJson);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
sourceRefs = [];
|
|
62
|
+
}
|
|
63
|
+
if (!Array.isArray(sourceRefs) || sourceRefs.length === 0) {
|
|
64
|
+
seededStateDetected = true;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
44
67
|
}
|
|
45
68
|
}
|
|
46
69
|
// Check daily rhythm state for Quiet/Dream
|
|
@@ -51,10 +74,24 @@ export async function checkRealRunHealth(db, day) {
|
|
|
51
74
|
const rhythm = rhythmResult.row;
|
|
52
75
|
const hasQuietArtifact = rhythm?.quietStatus === "completed";
|
|
53
76
|
const hasDreamArtifact = rhythm?.dreamStatus === "scheduled" || rhythm?.dreamStatus === "completed";
|
|
77
|
+
// Check impulse context artifact freshness
|
|
78
|
+
const impulseResult = await readImpulseContextArtifact(db, "heartbeat");
|
|
79
|
+
let hasFreshImpulseContext = false;
|
|
80
|
+
if (!impulseResult.degraded && impulseResult.row) {
|
|
81
|
+
const updatedAt = new Date(impulseResult.row.updatedAt).getTime();
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
84
|
+
hasFreshImpulseContext = now - updatedAt <= ONE_DAY_MS;
|
|
85
|
+
}
|
|
86
|
+
// Check accepted/active memory projections
|
|
87
|
+
const activeProjections = await readMemoryProjectionsByStatus(db, "active");
|
|
88
|
+
const acceptedProjections = await readMemoryProjectionsByStatus(db, "accepted");
|
|
89
|
+
const hasProjectionFeedback = (!activeProjections.degraded && activeProjections.rows.length > 0) ||
|
|
90
|
+
(!acceptedProjections.degraded && acceptedProjections.rows.length > 0);
|
|
54
91
|
// Determine if only contract smoke
|
|
55
|
-
const contractSmokeOnly = !hasRealClosure && !hasQuietArtifact && !hasDreamArtifact;
|
|
92
|
+
const contractSmokeOnly = !hasRealClosure && !hasQuietArtifact && !hasDreamArtifact && !hasFreshImpulseContext && !hasProjectionFeedback;
|
|
56
93
|
// Gate passes only when all real runtime stages have evidence
|
|
57
|
-
const gatePassed = !contractSmokeOnly && !seededStateDetected && hasRealClosure && hasQuietArtifact && hasDreamArtifact;
|
|
94
|
+
const gatePassed = !contractSmokeOnly && !seededStateDetected && hasRealClosure && hasQuietArtifact && hasDreamArtifact && hasFreshImpulseContext && hasProjectionFeedback;
|
|
58
95
|
// Identify missing stage
|
|
59
96
|
let missingStage;
|
|
60
97
|
let missingReason;
|
|
@@ -64,7 +101,7 @@ export async function checkRealRunHealth(db, day) {
|
|
|
64
101
|
}
|
|
65
102
|
else if (seededStateDetected) {
|
|
66
103
|
missingStage = "closure";
|
|
67
|
-
missingReason = "ActionClosureRecord exists but lacks runtime-produced cycle trace. Seeded state detected — not valid runtime proof.";
|
|
104
|
+
missingReason = "ActionClosureRecord exists but lacks runtime-produced cycle trace, closure stage event, or source refs. Seeded state detected — not valid runtime proof.";
|
|
68
105
|
}
|
|
69
106
|
else if (!hasQuietArtifact) {
|
|
70
107
|
missingStage = "quiet";
|
|
@@ -74,6 +111,14 @@ export async function checkRealRunHealth(db, day) {
|
|
|
74
111
|
missingStage = "dream";
|
|
75
112
|
missingReason = "QuietDailyReview completed but no DreamConsolidationRun. Dream scheduler may be unavailable.";
|
|
76
113
|
}
|
|
114
|
+
else if (!hasFreshImpulseContext) {
|
|
115
|
+
missingStage = "impulse";
|
|
116
|
+
missingReason = "Heartbeat produces closure but impulse context artifact is missing or stale (>24h). Run guidance_payload to refresh.";
|
|
117
|
+
}
|
|
118
|
+
else if (!hasProjectionFeedback) {
|
|
119
|
+
missingStage = "projection";
|
|
120
|
+
missingReason = "Living loop active but no accepted/active memory projections. Quiet/Dream may not have produced accepted memory yet.";
|
|
121
|
+
}
|
|
77
122
|
else {
|
|
78
123
|
missingStage = "none";
|
|
79
124
|
missingReason = "All living-loop stages have real artifacts.";
|
|
@@ -84,6 +129,8 @@ export async function checkRealRunHealth(db, day) {
|
|
|
84
129
|
hasRealClosure,
|
|
85
130
|
hasQuietArtifact,
|
|
86
131
|
hasDreamArtifact,
|
|
132
|
+
hasFreshImpulseContext,
|
|
133
|
+
hasProjectionFeedback,
|
|
87
134
|
contractSmokeOnly,
|
|
88
135
|
seededStateDetected,
|
|
89
136
|
gatePassed,
|