@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.
Files changed (39) hide show
  1. package/openclaw.plugin.json +1 -1
  2. package/package.json +1 -1
  3. package/runtime/cli/commands/index.d.ts +4 -0
  4. package/runtime/cli/commands/index.js +179 -5
  5. package/runtime/cli/index.js +2 -0
  6. package/runtime/cli/ops/ops-router.js +27 -17
  7. package/runtime/connectors/base/contract.d.ts +1 -0
  8. package/runtime/connectors/base/failure-taxonomy.js +45 -26
  9. package/runtime/connectors/services/connector-cooldown-port.d.ts +22 -0
  10. package/runtime/connectors/services/connector-cooldown-port.js +123 -0
  11. package/runtime/connectors/services/connector-executor-adapter.js +10 -4
  12. package/runtime/connectors/services/credential-route-context.d.ts +3 -2
  13. package/runtime/connectors/services/credential-route-context.js +19 -3
  14. package/runtime/core/second-nature/action/action-closure-recorder.d.ts +4 -0
  15. package/runtime/core/second-nature/action/action-closure-recorder.js +5 -0
  16. package/runtime/core/second-nature/action/action-proposal-builder.js +1 -0
  17. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.d.ts +2 -0
  18. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.js +76 -0
  19. package/runtime/core/second-nature/control-plane/real-runtime-spine.d.ts +2 -0
  20. package/runtime/core/second-nature/control-plane/real-runtime-spine.js +1 -0
  21. package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.d.ts +1 -1
  22. package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.js +10 -5
  23. package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.d.ts +2 -2
  24. package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.js +10 -28
  25. package/runtime/observability/db/index.d.ts +2 -0
  26. package/runtime/observability/db/index.js +6 -0
  27. package/runtime/observability/living-loop-health-gate.d.ts +6 -2
  28. package/runtime/observability/living-loop-health-gate.js +52 -5
  29. package/runtime/observability/loop-status.d.ts +19 -0
  30. package/runtime/observability/loop-status.js +121 -7
  31. package/runtime/observability/services/heartbeat-digest-assembler.d.ts +9 -0
  32. package/runtime/observability/services/heartbeat-digest-assembler.js +44 -9
  33. package/runtime/shared/types/v8-contracts.d.ts +1 -1
  34. package/runtime/storage/db/index.d.ts +2 -0
  35. package/runtime/storage/db/index.js +28 -2
  36. package/runtime/storage/db/schema/v8-entities.d.ts +288 -0
  37. package/runtime/storage/db/schema/v8-entities.js +23 -1
  38. package/runtime/storage/v8-state-stores.d.ts +10 -1
  39. 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
- detail: `HTTP ${resp.status}: ${await resp.text().catch(() => "")}`,
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 routeContextPort = createCredentialRouteContextPort(vault);
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 stubbed (always unblocked) until a cooldown ledger is modeled.
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
- export declare function createCredentialRouteContextPort(vault: CredentialVault): RouteContextPort;
10
+ import type { StateDatabase } from "../../storage/db/index.js";
11
+ export declare function createCredentialRouteContextPort(vault: CredentialVault, db: StateDatabase): RouteContextPort;
@@ -1,4 +1,5 @@
1
- export function createCredentialRouteContextPort(vault) {
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
- return { blocked: false };
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" || state.dreamStatus === "blocked") {
97
- // Already handled
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/skipped → Dream cannot run
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, 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>;
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, candidateId, topicKey, sourceRefs, reason = "projection_rejected", options) {
85
+ export async function rejectMemoryProjection(db, projectionId, _candidateId, _topicKey, _sourceRefs, reason = "projection_rejected", options) {
86
86
  const now = options?.now ?? new Date().toISOString();
87
- const writeResult = await writeLongTermMemoryProjection(db, {
88
- id: projectionId,
89
- createdAt: now,
90
- candidateId,
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, candidateId, topicKey, sourceRefs, options) {
98
+ export async function retireMemoryProjection(db, projectionId, _candidateId, _topicKey, _sourceRefs, options) {
108
99
  const now = options?.now ?? new Date().toISOString();
109
- const writeResult = await writeLongTermMemoryProjection(db, {
110
- id: projectionId,
111
- createdAt: now,
112
- candidateId,
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 events)
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,