@haaaiawd/second-nature 0.2.2 → 0.2.5
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/ops/heartbeat-surface.d.ts +20 -0
- package/runtime/cli/ops/heartbeat-surface.js +72 -1
- package/runtime/cli/ops/ops-router.js +119 -31
- package/runtime/connectors/base/contract.d.ts +11 -0
- package/runtime/connectors/base/failure-taxonomy.js +45 -26
- 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/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 +412 -25
- package/runtime/core/second-nature/control-plane/real-runtime-spine.d.ts +35 -0
- package/runtime/core/second-nature/control-plane/real-runtime-spine.js +42 -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/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-dream/daily-rhythm-scheduler.d.ts +43 -0
- package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.js +162 -0
- 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 +27 -44
- 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 +49 -0
- package/runtime/observability/living-loop-health-gate.js +141 -0
- package/runtime/observability/loop-status.d.ts +30 -0
- package/runtime/observability/loop-status.js +167 -7
- package/runtime/observability/services/heartbeat-digest-assembler.d.ts +21 -0
- package/runtime/observability/services/heartbeat-digest-assembler.js +44 -0
- package/runtime/shared/types/v8-contracts.d.ts +2 -2
- package/runtime/storage/db/index.js +60 -6
- 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 +874 -0
- package/runtime/storage/db/schema/v8-entities.js +62 -1
- package/runtime/storage/v8-state-stores.d.ts +41 -2
- package/runtime/storage/v8-state-stores.js +206 -2
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LivingLoopHealthGate — Distinguish contract-smoke from real runtime activity.
|
|
3
|
+
*
|
|
4
|
+
* Core logic: Check for persisted ActionClosureRecord, QuietDailyReview,
|
|
5
|
+
* and DreamConsolidationRun to determine if the living loop has real
|
|
6
|
+
* evidence or is only passing contract smoke tests.
|
|
7
|
+
*
|
|
8
|
+
* Design authority:
|
|
9
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/observability-health-system.md §4.2`
|
|
10
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/control-plane-system.md §4`
|
|
11
|
+
*
|
|
12
|
+
* Dependencies:
|
|
13
|
+
* - `src/storage/v8-state-stores.js` (readActionClosuresByDay, readDailyRhythmStateByDay)
|
|
14
|
+
*
|
|
15
|
+
* Boundary:
|
|
16
|
+
* - Read-only diagnostic; does not modify state.
|
|
17
|
+
* - Reports explicit absence reasons instead of silent zeros.
|
|
18
|
+
*/
|
|
19
|
+
import type { StateDatabase } from "../storage/db/index.js";
|
|
20
|
+
import type { DegradedOperationResult } from "../shared/types/v8-contracts.js";
|
|
21
|
+
export interface RealRunHealthGate {
|
|
22
|
+
/** Has at least one real ActionClosureRecord */
|
|
23
|
+
hasRealClosure: boolean;
|
|
24
|
+
/** Has a completed QuietDailyReview */
|
|
25
|
+
hasQuietArtifact: boolean;
|
|
26
|
+
/** Has a scheduled or completed DreamConsolidationRun */
|
|
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;
|
|
32
|
+
/** True if only contract smoke (cycle traces) but no real artifacts */
|
|
33
|
+
contractSmokeOnly: boolean;
|
|
34
|
+
/** True if closure exists but no runtime-produced cycle trace + stage event backs it */
|
|
35
|
+
seededStateDetected: boolean;
|
|
36
|
+
/** True only when real runtime activity is proven (not seeded, not smoke-only) */
|
|
37
|
+
gatePassed: boolean;
|
|
38
|
+
/** Explicit missing stage reason */
|
|
39
|
+
missingStage?: "closure" | "quiet" | "dream" | "impulse" | "projection" | "none";
|
|
40
|
+
missingReason?: string;
|
|
41
|
+
}
|
|
42
|
+
export type RealRunHealthResult = {
|
|
43
|
+
ok: true;
|
|
44
|
+
gate: RealRunHealthGate;
|
|
45
|
+
} | {
|
|
46
|
+
ok: false;
|
|
47
|
+
degraded: DegradedOperationResult;
|
|
48
|
+
};
|
|
49
|
+
export declare function checkRealRunHealth(db: StateDatabase, day?: string): Promise<RealRunHealthResult>;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LivingLoopHealthGate — Distinguish contract-smoke from real runtime activity.
|
|
3
|
+
*
|
|
4
|
+
* Core logic: Check for persisted ActionClosureRecord, QuietDailyReview,
|
|
5
|
+
* and DreamConsolidationRun to determine if the living loop has real
|
|
6
|
+
* evidence or is only passing contract smoke tests.
|
|
7
|
+
*
|
|
8
|
+
* Design authority:
|
|
9
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/observability-health-system.md §4.2`
|
|
10
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/control-plane-system.md §4`
|
|
11
|
+
*
|
|
12
|
+
* Dependencies:
|
|
13
|
+
* - `src/storage/v8-state-stores.js` (readActionClosuresByDay, readDailyRhythmStateByDay)
|
|
14
|
+
*
|
|
15
|
+
* Boundary:
|
|
16
|
+
* - Read-only diagnostic; does not modify state.
|
|
17
|
+
* - Reports explicit absence reasons instead of silent zeros.
|
|
18
|
+
*/
|
|
19
|
+
import { readActionClosuresByDay, readDailyRhythmStateByDay, readHeartbeatCycleTraces, readLoopStageEventsByCycle, readImpulseContextArtifact, readMemoryProjectionsByStatus, } from "../storage/v8-state-stores.js";
|
|
20
|
+
// ───────────────────────────────────────────────────────────────
|
|
21
|
+
// Public API
|
|
22
|
+
// ───────────────────────────────────────────────────────────────
|
|
23
|
+
export async function checkRealRunHealth(db, day) {
|
|
24
|
+
const targetDay = day ?? new Date().toISOString().slice(0, 10);
|
|
25
|
+
// Check closures
|
|
26
|
+
const closureResult = await readActionClosuresByDay(db, targetDay);
|
|
27
|
+
if (closureResult.degraded) {
|
|
28
|
+
return { ok: false, degraded: closureResult.degraded };
|
|
29
|
+
}
|
|
30
|
+
const hasRealClosure = closureResult.rows.length > 0;
|
|
31
|
+
// Check if closures are runtime-produced (backed by cycle trace + closure stage event + source refs)
|
|
32
|
+
let seededStateDetected = false;
|
|
33
|
+
if (hasRealClosure) {
|
|
34
|
+
const traces = await readHeartbeatCycleTraces(db, 1000);
|
|
35
|
+
if (traces.degraded) {
|
|
36
|
+
return { ok: false, degraded: traces.degraded };
|
|
37
|
+
}
|
|
38
|
+
for (const closure of closureResult.rows) {
|
|
39
|
+
const hasCycleTrace = traces.rows.some((t) => t.id === closure.cycleId);
|
|
40
|
+
if (!hasCycleTrace) {
|
|
41
|
+
seededStateDetected = true;
|
|
42
|
+
break;
|
|
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
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Check daily rhythm state for Quiet/Dream
|
|
70
|
+
const rhythmResult = await readDailyRhythmStateByDay(db, targetDay);
|
|
71
|
+
if (rhythmResult.degraded) {
|
|
72
|
+
return { ok: false, degraded: rhythmResult.degraded };
|
|
73
|
+
}
|
|
74
|
+
const rhythm = rhythmResult.row;
|
|
75
|
+
const hasQuietArtifact = rhythm?.quietStatus === "completed";
|
|
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);
|
|
91
|
+
// Determine if only contract smoke
|
|
92
|
+
const contractSmokeOnly = !hasRealClosure && !hasQuietArtifact && !hasDreamArtifact && !hasFreshImpulseContext && !hasProjectionFeedback;
|
|
93
|
+
// Gate passes only when all real runtime stages have evidence
|
|
94
|
+
const gatePassed = !contractSmokeOnly && !seededStateDetected && hasRealClosure && hasQuietArtifact && hasDreamArtifact && hasFreshImpulseContext && hasProjectionFeedback;
|
|
95
|
+
// Identify missing stage
|
|
96
|
+
let missingStage;
|
|
97
|
+
let missingReason;
|
|
98
|
+
if (!hasRealClosure) {
|
|
99
|
+
missingStage = "closure";
|
|
100
|
+
missingReason = "No ActionClosureRecord for today. Heartbeat may be running contract smoke without real action closure.";
|
|
101
|
+
}
|
|
102
|
+
else if (seededStateDetected) {
|
|
103
|
+
missingStage = "closure";
|
|
104
|
+
missingReason = "ActionClosureRecord exists but lacks runtime-produced cycle trace, closure stage event, or source refs. Seeded state detected — not valid runtime proof.";
|
|
105
|
+
}
|
|
106
|
+
else if (!hasQuietArtifact) {
|
|
107
|
+
missingStage = "quiet";
|
|
108
|
+
missingReason = "ActionClosureRecord exists but no QuietDailyReview. Daily review may be due or skipped.";
|
|
109
|
+
}
|
|
110
|
+
else if (!hasDreamArtifact) {
|
|
111
|
+
missingStage = "dream";
|
|
112
|
+
missingReason = "QuietDailyReview completed but no DreamConsolidationRun. Dream scheduler may be unavailable.";
|
|
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
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
missingStage = "none";
|
|
124
|
+
missingReason = "All living-loop stages have real artifacts.";
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
ok: true,
|
|
128
|
+
gate: {
|
|
129
|
+
hasRealClosure,
|
|
130
|
+
hasQuietArtifact,
|
|
131
|
+
hasDreamArtifact,
|
|
132
|
+
hasFreshImpulseContext,
|
|
133
|
+
hasProjectionFeedback,
|
|
134
|
+
contractSmokeOnly,
|
|
135
|
+
seededStateDetected,
|
|
136
|
+
gatePassed,
|
|
137
|
+
missingStage,
|
|
138
|
+
missingReason,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -20,6 +20,18 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import type { StateDatabase } from "../storage/db/index.js";
|
|
22
22
|
import type { DegradedOperationResult } from "../shared/types/v8-contracts.js";
|
|
23
|
+
export interface RealRunHealthProjection {
|
|
24
|
+
gatePassed: boolean;
|
|
25
|
+
contractSmokeOnly: boolean;
|
|
26
|
+
seededStateDetected: boolean;
|
|
27
|
+
hasRealClosure: boolean;
|
|
28
|
+
hasQuietArtifact: boolean;
|
|
29
|
+
hasDreamArtifact: boolean;
|
|
30
|
+
hasFreshImpulseContext: boolean;
|
|
31
|
+
hasProjectionFeedback: boolean;
|
|
32
|
+
missingStage?: string;
|
|
33
|
+
missingReason?: string;
|
|
34
|
+
}
|
|
23
35
|
export interface LoopStatusReadModel {
|
|
24
36
|
ok: true;
|
|
25
37
|
overallStatus: string;
|
|
@@ -28,7 +40,13 @@ export interface LoopStatusReadModel {
|
|
|
28
40
|
lastHeartbeatAt?: string;
|
|
29
41
|
stageSummaries: StageSummary[];
|
|
30
42
|
policyDeniedCount: number;
|
|
43
|
+
hardGuardDeniedCount: number;
|
|
44
|
+
cooldownReplayCount: number;
|
|
45
|
+
sourceAbsenceCount: number;
|
|
46
|
+
quietSuppressionCount: number;
|
|
47
|
+
connectorTerminalCount: number;
|
|
31
48
|
nextAction: string;
|
|
49
|
+
realRunHealth: RealRunHealthProjection;
|
|
32
50
|
}
|
|
33
51
|
export interface StageSummary {
|
|
34
52
|
stage: string;
|
|
@@ -43,4 +61,16 @@ export type LoopStatusResult = {
|
|
|
43
61
|
ok: false;
|
|
44
62
|
degraded: DegradedOperationResult;
|
|
45
63
|
};
|
|
64
|
+
export interface DenialAttribution {
|
|
65
|
+
policyDeniedCount: number;
|
|
66
|
+
hardGuardDeniedCount: number;
|
|
67
|
+
cooldownReplayCount: number;
|
|
68
|
+
sourceAbsenceCount: number;
|
|
69
|
+
quietSuppressionCount: number;
|
|
70
|
+
connectorTerminalCount: number;
|
|
71
|
+
}
|
|
72
|
+
export declare function attributeDenials(db: StateDatabase, options?: {
|
|
73
|
+
day?: string;
|
|
74
|
+
cycleWindowHours?: number;
|
|
75
|
+
}): Promise<DenialAttribution>;
|
|
46
76
|
export declare function readLoopStatus(db: StateDatabase): Promise<LoopStatusResult>;
|
|
@@ -19,10 +19,16 @@
|
|
|
19
19
|
* Test coverage: tests/unit/observability/loop-status.test.ts
|
|
20
20
|
*/
|
|
21
21
|
import { assembleLoopStatus } from "./causal-loop-health.js";
|
|
22
|
+
import { checkRealRunHealth } from "./living-loop-health-gate.js";
|
|
23
|
+
import { readActionClosuresByDay, readConnectorCooldownState, } from "../storage/v8-state-stores.js";
|
|
22
24
|
// ───────────────────────────────────────────────────────────────
|
|
23
25
|
// Helpers
|
|
24
26
|
// ───────────────────────────────────────────────────────────────
|
|
25
|
-
function computeNextAction(overallStatus, stalledAt) {
|
|
27
|
+
function computeNextAction(overallStatus, stalledAt, realRunMissingStage, realRunMissingReason, attribution) {
|
|
28
|
+
// Real-run health takes precedence over generic causal health
|
|
29
|
+
if (realRunMissingStage && realRunMissingStage !== "none") {
|
|
30
|
+
return `Real-run health degraded: ${realRunMissingReason ?? `missing stage: ${realRunMissingStage}`}. Run a real heartbeat cycle or verify daily rhythm state.`;
|
|
31
|
+
}
|
|
26
32
|
if (overallStatus === "healthy") {
|
|
27
33
|
return "No operator action required. Loop is progressing normally.";
|
|
28
34
|
}
|
|
@@ -49,6 +55,110 @@ function computeNextAction(overallStatus, stalledAt) {
|
|
|
49
55
|
return "Review loop stage events and state database health.";
|
|
50
56
|
}
|
|
51
57
|
// ───────────────────────────────────────────────────────────────
|
|
58
|
+
// Denial / replay attribution (T-OBS.R.4)
|
|
59
|
+
// ───────────────────────────────────────────────────────────────
|
|
60
|
+
const CONNECTOR_TERMINAL_REASONS = new Set([
|
|
61
|
+
"auth_failure",
|
|
62
|
+
"credential_expired",
|
|
63
|
+
"verification_required",
|
|
64
|
+
"configuration_missing",
|
|
65
|
+
"platform_unavailable",
|
|
66
|
+
"transport_failure",
|
|
67
|
+
"rate_limited",
|
|
68
|
+
"timeout",
|
|
69
|
+
"script_error",
|
|
70
|
+
"parse_failure",
|
|
71
|
+
"protocol_mismatch",
|
|
72
|
+
"semantic_rejection",
|
|
73
|
+
"permanent_input_error",
|
|
74
|
+
"unknown_platform_change",
|
|
75
|
+
]);
|
|
76
|
+
function classifyReasonToTerminal(reason) {
|
|
77
|
+
return CONNECTOR_TERMINAL_REASONS.has(reason);
|
|
78
|
+
}
|
|
79
|
+
function emptyAttribution() {
|
|
80
|
+
return {
|
|
81
|
+
policyDeniedCount: 0,
|
|
82
|
+
hardGuardDeniedCount: 0,
|
|
83
|
+
cooldownReplayCount: 0,
|
|
84
|
+
sourceAbsenceCount: 0,
|
|
85
|
+
quietSuppressionCount: 0,
|
|
86
|
+
connectorTerminalCount: 0,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export async function attributeDenials(db, options) {
|
|
90
|
+
const targetDay = options?.day ?? new Date().toISOString().slice(0, 10);
|
|
91
|
+
const readResult = await readActionClosuresByDay(db, targetDay);
|
|
92
|
+
if (readResult.degraded) {
|
|
93
|
+
return emptyAttribution();
|
|
94
|
+
}
|
|
95
|
+
const attribution = emptyAttribution();
|
|
96
|
+
for (const closure of readResult.rows) {
|
|
97
|
+
const status = closure.status;
|
|
98
|
+
const reason = closure.reason ?? "";
|
|
99
|
+
// Cooldown/replay is determined from durable cooldown state per platform/capability.
|
|
100
|
+
if ((status === "denied" || status === "downgraded" || status === "deferred") &&
|
|
101
|
+
closure.platformId &&
|
|
102
|
+
closure.capabilityId) {
|
|
103
|
+
const cooldownResult = await readConnectorCooldownState(db, closure.platformId, closure.capabilityId);
|
|
104
|
+
if (cooldownResult.row?.blockedUntil && new Date(cooldownResult.row.blockedUntil) > new Date()) {
|
|
105
|
+
attribution.cooldownReplayCount += 1;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const terminalClass = classifyReasonToTerminal(reason);
|
|
110
|
+
const isCooldownReplay = reason === "cooldown_blocked" || reason === "replay_suppressed";
|
|
111
|
+
const isQuietSuppression = reason === "guidance_unavailable" || reason === "quiet_empty_input" || reason === "quiet_suppression";
|
|
112
|
+
if (isCooldownReplay) {
|
|
113
|
+
attribution.cooldownReplayCount += 1;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (status === "denied") {
|
|
117
|
+
if (reason.startsWith("policy_denied")) {
|
|
118
|
+
attribution.policyDeniedCount += 1;
|
|
119
|
+
}
|
|
120
|
+
else if (terminalClass) {
|
|
121
|
+
attribution.connectorTerminalCount += 1;
|
|
122
|
+
}
|
|
123
|
+
else if (reason === "source_refs_missing" ||
|
|
124
|
+
reason === "affordance_unavailable" ||
|
|
125
|
+
reason === "awaiting_user" ||
|
|
126
|
+
reason === "permission_missing") {
|
|
127
|
+
attribution.hardGuardDeniedCount += 1;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
attribution.policyDeniedCount += 1;
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (status === "downgraded" || status === "deferred") {
|
|
135
|
+
if (isQuietSuppression) {
|
|
136
|
+
attribution.quietSuppressionCount += 1;
|
|
137
|
+
}
|
|
138
|
+
else if (terminalClass) {
|
|
139
|
+
attribution.connectorTerminalCount += 1;
|
|
140
|
+
}
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (status === "no_action") {
|
|
144
|
+
if (reason === "evidence_batch_empty" || reason === "quiet_empty_input") {
|
|
145
|
+
attribution.sourceAbsenceCount += 1;
|
|
146
|
+
}
|
|
147
|
+
else if (terminalClass) {
|
|
148
|
+
attribution.connectorTerminalCount += 1;
|
|
149
|
+
}
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (status === "completed" || status === "failed") {
|
|
153
|
+
if (terminalClass) {
|
|
154
|
+
attribution.connectorTerminalCount += 1;
|
|
155
|
+
}
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return attribution;
|
|
160
|
+
}
|
|
161
|
+
// ───────────────────────────────────────────────────────────────
|
|
52
162
|
// Public API
|
|
53
163
|
// ───────────────────────────────────────────────────────────────
|
|
54
164
|
export async function readLoopStatus(db) {
|
|
@@ -60,26 +170,76 @@ export async function readLoopStatus(db) {
|
|
|
60
170
|
};
|
|
61
171
|
}
|
|
62
172
|
const snapshot = health;
|
|
173
|
+
// T-OBS.R.3: Consume real-run health gate
|
|
174
|
+
const realRunResult = await checkRealRunHealth(db);
|
|
175
|
+
let realRunHealth;
|
|
176
|
+
if (realRunResult.ok) {
|
|
177
|
+
realRunHealth = {
|
|
178
|
+
gatePassed: realRunResult.gate.gatePassed,
|
|
179
|
+
contractSmokeOnly: realRunResult.gate.contractSmokeOnly,
|
|
180
|
+
seededStateDetected: realRunResult.gate.seededStateDetected,
|
|
181
|
+
hasRealClosure: realRunResult.gate.hasRealClosure,
|
|
182
|
+
hasQuietArtifact: realRunResult.gate.hasQuietArtifact,
|
|
183
|
+
hasDreamArtifact: realRunResult.gate.hasDreamArtifact,
|
|
184
|
+
hasFreshImpulseContext: realRunResult.gate.hasFreshImpulseContext,
|
|
185
|
+
hasProjectionFeedback: realRunResult.gate.hasProjectionFeedback,
|
|
186
|
+
missingStage: realRunResult.gate.missingStage,
|
|
187
|
+
missingReason: realRunResult.gate.missingReason,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
realRunHealth = {
|
|
192
|
+
gatePassed: false,
|
|
193
|
+
contractSmokeOnly: false,
|
|
194
|
+
seededStateDetected: false,
|
|
195
|
+
hasRealClosure: false,
|
|
196
|
+
hasQuietArtifact: false,
|
|
197
|
+
hasDreamArtifact: false,
|
|
198
|
+
hasFreshImpulseContext: false,
|
|
199
|
+
hasProjectionFeedback: false,
|
|
200
|
+
missingReason: "Real-run health check degraded: " + (realRunResult.degraded.operatorNextAction || "unknown"),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
// Override overallStatus based on real-run health parity
|
|
204
|
+
let overallStatus = snapshot.overallStatus;
|
|
205
|
+
let stalledAt = snapshot.stalledAt;
|
|
206
|
+
if (!realRunHealth.gatePassed) {
|
|
207
|
+
// Real-run gate fails → cannot report healthy
|
|
208
|
+
if (overallStatus === "healthy") {
|
|
209
|
+
overallStatus = "degraded";
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
// Real-run gate passes → all stages have evidence, ignore staged-event-only stall
|
|
214
|
+
overallStatus = "healthy";
|
|
215
|
+
stalledAt = undefined;
|
|
216
|
+
}
|
|
63
217
|
const stageSummaries = snapshot.stages.map((s) => ({
|
|
64
218
|
stage: s.stage,
|
|
65
219
|
eventCount: s.eventCount,
|
|
66
220
|
stalled: s.stalled,
|
|
67
221
|
lastEventAt: s.lastEventAt,
|
|
68
222
|
}));
|
|
69
|
-
//
|
|
70
|
-
const
|
|
71
|
-
const nextAction = computeNextAction(
|
|
223
|
+
// T-OBS.R.4: Attribute denials and connector replay root causes
|
|
224
|
+
const attribution = await attributeDenials(db);
|
|
225
|
+
const nextAction = computeNextAction(overallStatus, stalledAt, realRunHealth.missingStage, realRunHealth.missingReason, attribution);
|
|
72
226
|
return {
|
|
73
227
|
ok: true,
|
|
74
228
|
status: {
|
|
75
229
|
ok: true,
|
|
76
|
-
overallStatus
|
|
77
|
-
stalledAt
|
|
230
|
+
overallStatus,
|
|
231
|
+
stalledAt,
|
|
78
232
|
lastCycleSequence: snapshot.lastCycleSequence,
|
|
79
233
|
lastHeartbeatAt: snapshot.lastHeartbeatAt,
|
|
80
234
|
stageSummaries,
|
|
81
|
-
policyDeniedCount,
|
|
235
|
+
policyDeniedCount: attribution.policyDeniedCount,
|
|
236
|
+
hardGuardDeniedCount: attribution.hardGuardDeniedCount,
|
|
237
|
+
cooldownReplayCount: attribution.cooldownReplayCount,
|
|
238
|
+
sourceAbsenceCount: attribution.sourceAbsenceCount,
|
|
239
|
+
quietSuppressionCount: attribution.quietSuppressionCount,
|
|
240
|
+
connectorTerminalCount: attribution.connectorTerminalCount,
|
|
82
241
|
nextAction,
|
|
242
|
+
realRunHealth,
|
|
83
243
|
},
|
|
84
244
|
};
|
|
85
245
|
}
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
* tests/integration/observability/digest-delivery.test.ts (T-OBS.C.4)
|
|
32
32
|
*/
|
|
33
33
|
import type { AppendOnlyAuditStore } from "../audit/append-only-audit-store.js";
|
|
34
|
+
import type { StateDatabase } from "../../storage/db/index.js";
|
|
34
35
|
export interface ConnectorDaySummary {
|
|
35
36
|
platformId: string;
|
|
36
37
|
capability: string;
|
|
@@ -68,6 +69,18 @@ export interface DeliveryProofRef {
|
|
|
68
69
|
channelId: string;
|
|
69
70
|
messageHash: string;
|
|
70
71
|
}
|
|
72
|
+
export interface RealRunHealthDigestProjection {
|
|
73
|
+
gatePassed: boolean;
|
|
74
|
+
contractSmokeOnly: boolean;
|
|
75
|
+
seededStateDetected: boolean;
|
|
76
|
+
hasRealClosure: boolean;
|
|
77
|
+
hasQuietArtifact: boolean;
|
|
78
|
+
hasDreamArtifact: boolean;
|
|
79
|
+
hasFreshImpulseContext: boolean;
|
|
80
|
+
hasProjectionFeedback: boolean;
|
|
81
|
+
missingStage?: string;
|
|
82
|
+
missingReason?: string;
|
|
83
|
+
}
|
|
71
84
|
export interface HeartbeatDigest {
|
|
72
85
|
date: string;
|
|
73
86
|
generatedAt: string;
|
|
@@ -76,6 +89,8 @@ export interface HeartbeatDigest {
|
|
|
76
89
|
goalSummary: GoalDaySummary;
|
|
77
90
|
quietDreamSummary: QuietDreamDaySummary;
|
|
78
91
|
healthSummary: HealthDaySummary;
|
|
92
|
+
/** Real-run health gate result (T-OBS.R.3) */
|
|
93
|
+
realRunHealth: RealRunHealthDigestProjection;
|
|
79
94
|
/** Set when delivery succeeded */
|
|
80
95
|
deliveredAt?: string;
|
|
81
96
|
/** Proof of successful delivery (channel + message hash, no raw content) */
|
|
@@ -124,6 +139,12 @@ export interface StateMemoryDigestPort {
|
|
|
124
139
|
export interface HeartbeatDigestAssemblerDeps {
|
|
125
140
|
auditStore: AppendOnlyAuditStore;
|
|
126
141
|
stateMemoryPort?: StateMemoryDigestPort;
|
|
142
|
+
/**
|
|
143
|
+
* Optional state database for real-run health evaluation (F6).
|
|
144
|
+
* When provided, generateHeartbeatDigest calls checkRealRunHealth automatically
|
|
145
|
+
* and embeds the result into digest.realRunHealth.
|
|
146
|
+
*/
|
|
147
|
+
db?: StateDatabase;
|
|
127
148
|
/**
|
|
128
149
|
* Optional delivery adapter (T-OBS.C.4).
|
|
129
150
|
* When provided, the assembled digest is passed to adapter.deliver() after assembly.
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
* tests/unit/observability/heartbeat-digest-assembler.test.ts (T-OBS.C.3)
|
|
31
31
|
* tests/integration/observability/digest-delivery.test.ts (T-OBS.C.4)
|
|
32
32
|
*/
|
|
33
|
+
import { checkRealRunHealth } from "../living-loop-health-gate.js";
|
|
33
34
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
34
35
|
function isSameDayUtc(isoTimestamp, dateStr) {
|
|
35
36
|
// dateStr: "YYYY-MM-DD"
|
|
@@ -219,6 +220,48 @@ export async function generateHeartbeatDigest(date, deps) {
|
|
|
219
220
|
quietDreamSummary = aggregateQuietDreamFromAudit(events, date);
|
|
220
221
|
}
|
|
221
222
|
const nothingSignificant = isNothingSignificant(connectorSummary, goalSummary, quietDreamSummary, healthSummary);
|
|
223
|
+
// F6: Auto-evaluate real-run health when db is provided
|
|
224
|
+
let realRunHealth = {
|
|
225
|
+
gatePassed: false,
|
|
226
|
+
contractSmokeOnly: true,
|
|
227
|
+
seededStateDetected: false,
|
|
228
|
+
hasRealClosure: false,
|
|
229
|
+
hasQuietArtifact: false,
|
|
230
|
+
hasDreamArtifact: false,
|
|
231
|
+
hasFreshImpulseContext: false,
|
|
232
|
+
hasProjectionFeedback: false,
|
|
233
|
+
missingReason: "Real-run health not evaluated — no state DB wired to digest assembler",
|
|
234
|
+
};
|
|
235
|
+
if (deps.db) {
|
|
236
|
+
const realRunResult = await checkRealRunHealth(deps.db, date);
|
|
237
|
+
if (realRunResult.ok) {
|
|
238
|
+
realRunHealth = {
|
|
239
|
+
gatePassed: realRunResult.gate.gatePassed,
|
|
240
|
+
contractSmokeOnly: realRunResult.gate.contractSmokeOnly,
|
|
241
|
+
seededStateDetected: realRunResult.gate.seededStateDetected,
|
|
242
|
+
hasRealClosure: realRunResult.gate.hasRealClosure,
|
|
243
|
+
hasQuietArtifact: realRunResult.gate.hasQuietArtifact,
|
|
244
|
+
hasDreamArtifact: realRunResult.gate.hasDreamArtifact,
|
|
245
|
+
hasFreshImpulseContext: realRunResult.gate.hasFreshImpulseContext,
|
|
246
|
+
hasProjectionFeedback: realRunResult.gate.hasProjectionFeedback,
|
|
247
|
+
missingStage: realRunResult.gate.missingStage,
|
|
248
|
+
missingReason: realRunResult.gate.missingReason,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
realRunHealth = {
|
|
253
|
+
gatePassed: false,
|
|
254
|
+
contractSmokeOnly: false,
|
|
255
|
+
seededStateDetected: false,
|
|
256
|
+
hasRealClosure: false,
|
|
257
|
+
hasQuietArtifact: false,
|
|
258
|
+
hasDreamArtifact: false,
|
|
259
|
+
hasFreshImpulseContext: false,
|
|
260
|
+
hasProjectionFeedback: false,
|
|
261
|
+
missingReason: "Real-run health check degraded: " + (realRunResult.degraded.operatorNextAction ?? "unknown"),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
222
265
|
const digest = {
|
|
223
266
|
date,
|
|
224
267
|
generatedAt,
|
|
@@ -227,6 +270,7 @@ export async function generateHeartbeatDigest(date, deps) {
|
|
|
227
270
|
goalSummary,
|
|
228
271
|
quietDreamSummary,
|
|
229
272
|
healthSummary,
|
|
273
|
+
realRunHealth,
|
|
230
274
|
};
|
|
231
275
|
// T-OBS.C.4: delivery hook — attempt delivery if adapter is provided
|
|
232
276
|
if (deliveryAdapter) {
|
|
@@ -25,7 +25,7 @@ export interface ActionKindMetadata {
|
|
|
25
25
|
allowedDowngrades: PlatformNeutralActionKind[];
|
|
26
26
|
}
|
|
27
27
|
export type ConnectorCapabilitySideEffect = "external_read" | "external_write" | "local_state" | "unknown";
|
|
28
|
-
export type SourceRefFamily = "evidence" | "perception" | "judgment" | "action_closure" | "quiet_review" | "dream_run" | "memory_projection" | "tool_experience" | "connector_result" | "audit";
|
|
28
|
+
export type SourceRefFamily = "evidence" | "perception" | "judgment" | "action_closure" | "quiet_review" | "dream_run" | "memory_projection" | "projection" | "tool_experience" | "connector_result" | "audit";
|
|
29
29
|
export type RedactionClass = "none" | "redacted" | "blocked";
|
|
30
30
|
export type SensitivityClass = "public_technical" | "public_general" | "private_context" | "sensitive";
|
|
31
31
|
export type SourceResolveStatus = "resolvable" | "missing" | "redacted" | "permission_denied";
|
|
@@ -82,5 +82,5 @@ export interface DegradedOperationResult {
|
|
|
82
82
|
operatorNextAction: string;
|
|
83
83
|
retryable: boolean;
|
|
84
84
|
}
|
|
85
|
-
export type V8ReasonCode = "quiet_completed" | "quiet_empty_input" | "quiet_state_unreadable" | "quiet_validation_failed" | "dream_scheduled" | "dream_scheduler_unavailable" | "dream_started" | "dream_completed" | "dream_failed" | "dream_blocked_redaction" | "projection_candidate_created" | "projection_accepted" | "projection_rejected" | "projection_superseded" | "proposal_created" | "proposal_no_action" | "proposal_missing_source_refs" | "proposal_risk_blocked" | "policy_allowed" | "policy_deferred_owner_confirmation" | "policy_downgraded_to_draft" | "policy_denied_missing_permission" | "policy_denied_high_risk" | "policy_denied_breaker_open" | "guidance_unavailable" | "closure_completed" | "closure_no_action" | "closure_denied" | "closure_deferred" | "closure_downgraded" | "closure_downgraded_without_draft" | "closure_failed" | "perception_rules_only" | "evidence_batch_empty" | "evidence_batch_truncated" | "judgment_low_confidence" | "judgment_missing_source_refs" | "source_refs_unresolved" | "state_unreadable" | "stage_event_missing" | "ingestion_no_data" | "ingestion_empty" | "ingestion_state_unreadable" | "ingestion_connector_failed" | "execution_completed" | "execution_failed" | "execution_timeout" | "execution_unavailable";
|
|
85
|
+
export type V8ReasonCode = "quiet_completed" | "quiet_empty_input" | "quiet_state_unreadable" | "quiet_validation_failed" | "dream_scheduled" | "dream_scheduler_unavailable" | "dream_started" | "dream_completed" | "dream_failed" | "dream_blocked_redaction" | "projection_candidate_created" | "projection_accepted" | "projection_rejected" | "projection_superseded" | "projection_topic_matched" | "proposal_created" | "proposal_no_action" | "proposal_missing_source_refs" | "proposal_risk_blocked" | "policy_allowed" | "policy_deferred_owner_confirmation" | "policy_downgraded_to_draft" | "policy_denied_missing_permission" | "policy_denied_high_risk" | "policy_denied_breaker_open" | "guidance_unavailable" | "closure_completed" | "closure_no_action" | "closure_denied" | "closure_deferred" | "closure_downgraded" | "closure_downgraded_without_draft" | "closure_failed" | "perception_rules_only" | "perception_contract_drift" | "evidence_batch_empty" | "evidence_batch_truncated" | "judgment_low_confidence" | "judgment_missing_source_refs" | "source_refs_unresolved" | "state_unreadable" | "stage_event_missing" | "ingestion_no_data" | "ingestion_empty" | "ingestion_state_unreadable" | "ingestion_connector_failed" | "execution_completed" | "execution_failed" | "execution_timeout" | "execution_unavailable";
|
|
86
86
|
export declare const ACTION_KIND_REGISTRY: Readonly<Record<PlatformNeutralActionKind, ActionKindMetadata>>;
|
|
@@ -220,12 +220,14 @@ const STATE_SCHEMA_SQL = `
|
|
|
220
220
|
payload_json TEXT,
|
|
221
221
|
lifecycle_status TEXT NOT NULL DEFAULT 'pending'
|
|
222
222
|
);
|
|
223
|
-
CREATE TABLE IF NOT EXISTS action_closure_record (
|
|
224
|
-
id TEXT PRIMARY KEY,
|
|
225
|
-
created_at TEXT NOT NULL,
|
|
226
|
-
cycle_id TEXT NOT NULL,
|
|
227
|
-
|
|
228
|
-
|
|
223
|
+
CREATE TABLE IF NOT EXISTS action_closure_record (
|
|
224
|
+
id TEXT PRIMARY KEY,
|
|
225
|
+
created_at TEXT NOT NULL,
|
|
226
|
+
cycle_id TEXT NOT NULL,
|
|
227
|
+
platform_id TEXT,
|
|
228
|
+
capability_id TEXT,
|
|
229
|
+
proposal_id TEXT,
|
|
230
|
+
decision_id TEXT,
|
|
229
231
|
status TEXT NOT NULL,
|
|
230
232
|
reason TEXT,
|
|
231
233
|
next_state TEXT,
|
|
@@ -295,6 +297,54 @@ const STATE_SCHEMA_SQL = `
|
|
|
295
297
|
payload_json TEXT,
|
|
296
298
|
lifecycle_status TEXT NOT NULL DEFAULT 'started'
|
|
297
299
|
);
|
|
300
|
+
CREATE TABLE IF NOT EXISTS impulse_context_artifact (
|
|
301
|
+
id TEXT PRIMARY KEY,
|
|
302
|
+
created_at TEXT NOT NULL,
|
|
303
|
+
updated_at TEXT NOT NULL,
|
|
304
|
+
scene_type TEXT NOT NULL,
|
|
305
|
+
capability_intent TEXT,
|
|
306
|
+
platform_id TEXT,
|
|
307
|
+
capability_class TEXT,
|
|
308
|
+
impulse_source TEXT NOT NULL,
|
|
309
|
+
impulse_text TEXT,
|
|
310
|
+
atmosphere_text TEXT,
|
|
311
|
+
expression_boundary_constraints_json TEXT,
|
|
312
|
+
expression_boundary_style TEXT,
|
|
313
|
+
freshness_version INTEGER NOT NULL DEFAULT 1,
|
|
314
|
+
source_refs_json TEXT NOT NULL,
|
|
315
|
+
redaction_class TEXT NOT NULL DEFAULT 'none',
|
|
316
|
+
payload_json TEXT,
|
|
317
|
+
lifecycle_status TEXT NOT NULL DEFAULT 'active'
|
|
318
|
+
);
|
|
319
|
+
CREATE TABLE IF NOT EXISTS daily_rhythm_state (
|
|
320
|
+
id TEXT PRIMARY KEY,
|
|
321
|
+
day TEXT NOT NULL,
|
|
322
|
+
quiet_status TEXT NOT NULL DEFAULT 'not_due',
|
|
323
|
+
dream_status TEXT NOT NULL DEFAULT 'not_due',
|
|
324
|
+
quiet_reason TEXT,
|
|
325
|
+
dream_reason TEXT,
|
|
326
|
+
quiet_completed_at TEXT,
|
|
327
|
+
dream_completed_at TEXT,
|
|
328
|
+
source_refs_json TEXT NOT NULL,
|
|
329
|
+
payload_json TEXT,
|
|
330
|
+
updated_at TEXT NOT NULL
|
|
331
|
+
);
|
|
332
|
+
CREATE TABLE IF NOT EXISTS connector_cooldown_state (
|
|
333
|
+
id TEXT PRIMARY KEY,
|
|
334
|
+
platform_id TEXT NOT NULL,
|
|
335
|
+
capability_id TEXT NOT NULL,
|
|
336
|
+
failure_class TEXT NOT NULL,
|
|
337
|
+
retry_after_ms INTEGER,
|
|
338
|
+
blocked_until TEXT NOT NULL,
|
|
339
|
+
failure_count INTEGER NOT NULL DEFAULT 1,
|
|
340
|
+
terminal_count INTEGER NOT NULL DEFAULT 0,
|
|
341
|
+
source_refs_json TEXT NOT NULL,
|
|
342
|
+
redaction_class TEXT NOT NULL DEFAULT 'none',
|
|
343
|
+
payload_json TEXT,
|
|
344
|
+
created_at TEXT NOT NULL,
|
|
345
|
+
updated_at TEXT NOT NULL
|
|
346
|
+
);
|
|
347
|
+
CREATE INDEX IF NOT EXISTS connector_cooldown_state_platform_capability_idx ON connector_cooldown_state(platform_id, capability_id);
|
|
298
348
|
`;
|
|
299
349
|
function resolveDbPath(filename) {
|
|
300
350
|
if (path.isAbsolute(filename) || filename === ":memory:") {
|
|
@@ -316,6 +366,10 @@ function bootstrapStateSchema(sqlite) {
|
|
|
316
366
|
function applyStateSchemaMigrations(sqlite) {
|
|
317
367
|
const migrations = [
|
|
318
368
|
"ALTER TABLE policy_records ADD COLUMN outreach_daily_budget INTEGER NOT NULL DEFAULT 2",
|
|
369
|
+
"ALTER TABLE action_closure_record ADD COLUMN platform_id TEXT",
|
|
370
|
+
"ALTER TABLE action_closure_record ADD COLUMN capability_id TEXT",
|
|
371
|
+
"ALTER TABLE connector_cooldown_state ADD COLUMN terminal_count INTEGER NOT NULL DEFAULT 0",
|
|
372
|
+
"CREATE INDEX IF NOT EXISTS connector_cooldown_state_platform_capability_idx ON connector_cooldown_state(platform_id, capability_id)",
|
|
319
373
|
];
|
|
320
374
|
for (const sql of migrations) {
|
|
321
375
|
try {
|