@haaaiawd/second-nature 0.2.4 → 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/ops-router.js +4 -0
- 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/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.js +28 -8
- 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
|
@@ -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,
|
|
@@ -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,
|
|
@@ -27,6 +27,8 @@ export interface RealRunHealthProjection {
|
|
|
27
27
|
hasRealClosure: boolean;
|
|
28
28
|
hasQuietArtifact: boolean;
|
|
29
29
|
hasDreamArtifact: boolean;
|
|
30
|
+
hasFreshImpulseContext: boolean;
|
|
31
|
+
hasProjectionFeedback: boolean;
|
|
30
32
|
missingStage?: string;
|
|
31
33
|
missingReason?: string;
|
|
32
34
|
}
|
|
@@ -38,6 +40,11 @@ export interface LoopStatusReadModel {
|
|
|
38
40
|
lastHeartbeatAt?: string;
|
|
39
41
|
stageSummaries: StageSummary[];
|
|
40
42
|
policyDeniedCount: number;
|
|
43
|
+
hardGuardDeniedCount: number;
|
|
44
|
+
cooldownReplayCount: number;
|
|
45
|
+
sourceAbsenceCount: number;
|
|
46
|
+
quietSuppressionCount: number;
|
|
47
|
+
connectorTerminalCount: number;
|
|
41
48
|
nextAction: string;
|
|
42
49
|
realRunHealth: RealRunHealthProjection;
|
|
43
50
|
}
|
|
@@ -54,4 +61,16 @@ export type LoopStatusResult = {
|
|
|
54
61
|
ok: false;
|
|
55
62
|
degraded: DegradedOperationResult;
|
|
56
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>;
|
|
57
76
|
export declare function readLoopStatus(db: StateDatabase): Promise<LoopStatusResult>;
|
|
@@ -20,10 +20,11 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { assembleLoopStatus } from "./causal-loop-health.js";
|
|
22
22
|
import { checkRealRunHealth } from "./living-loop-health-gate.js";
|
|
23
|
+
import { readActionClosuresByDay, readConnectorCooldownState, } from "../storage/v8-state-stores.js";
|
|
23
24
|
// ───────────────────────────────────────────────────────────────
|
|
24
25
|
// Helpers
|
|
25
26
|
// ───────────────────────────────────────────────────────────────
|
|
26
|
-
function computeNextAction(overallStatus, stalledAt, realRunMissingStage, realRunMissingReason) {
|
|
27
|
+
function computeNextAction(overallStatus, stalledAt, realRunMissingStage, realRunMissingReason, attribution) {
|
|
27
28
|
// Real-run health takes precedence over generic causal health
|
|
28
29
|
if (realRunMissingStage && realRunMissingStage !== "none") {
|
|
29
30
|
return `Real-run health degraded: ${realRunMissingReason ?? `missing stage: ${realRunMissingStage}`}. Run a real heartbeat cycle or verify daily rhythm state.`;
|
|
@@ -54,6 +55,110 @@ function computeNextAction(overallStatus, stalledAt, realRunMissingStage, realRu
|
|
|
54
55
|
return "Review loop stage events and state database health.";
|
|
55
56
|
}
|
|
56
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
|
+
// ───────────────────────────────────────────────────────────────
|
|
57
162
|
// Public API
|
|
58
163
|
// ───────────────────────────────────────────────────────────────
|
|
59
164
|
export async function readLoopStatus(db) {
|
|
@@ -76,6 +181,8 @@ export async function readLoopStatus(db) {
|
|
|
76
181
|
hasRealClosure: realRunResult.gate.hasRealClosure,
|
|
77
182
|
hasQuietArtifact: realRunResult.gate.hasQuietArtifact,
|
|
78
183
|
hasDreamArtifact: realRunResult.gate.hasDreamArtifact,
|
|
184
|
+
hasFreshImpulseContext: realRunResult.gate.hasFreshImpulseContext,
|
|
185
|
+
hasProjectionFeedback: realRunResult.gate.hasProjectionFeedback,
|
|
79
186
|
missingStage: realRunResult.gate.missingStage,
|
|
80
187
|
missingReason: realRunResult.gate.missingReason,
|
|
81
188
|
};
|
|
@@ -88,7 +195,9 @@ export async function readLoopStatus(db) {
|
|
|
88
195
|
hasRealClosure: false,
|
|
89
196
|
hasQuietArtifact: false,
|
|
90
197
|
hasDreamArtifact: false,
|
|
91
|
-
|
|
198
|
+
hasFreshImpulseContext: false,
|
|
199
|
+
hasProjectionFeedback: false,
|
|
200
|
+
missingReason: "Real-run health check degraded: " + (realRunResult.degraded.operatorNextAction || "unknown"),
|
|
92
201
|
};
|
|
93
202
|
}
|
|
94
203
|
// Override overallStatus based on real-run health parity
|
|
@@ -111,19 +220,24 @@ export async function readLoopStatus(db) {
|
|
|
111
220
|
stalled: s.stalled,
|
|
112
221
|
lastEventAt: s.lastEventAt,
|
|
113
222
|
}));
|
|
114
|
-
//
|
|
115
|
-
const
|
|
116
|
-
const nextAction = computeNextAction(overallStatus,
|
|
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);
|
|
117
226
|
return {
|
|
118
227
|
ok: true,
|
|
119
228
|
status: {
|
|
120
229
|
ok: true,
|
|
121
230
|
overallStatus,
|
|
122
|
-
stalledAt
|
|
231
|
+
stalledAt,
|
|
123
232
|
lastCycleSequence: snapshot.lastCycleSequence,
|
|
124
233
|
lastHeartbeatAt: snapshot.lastHeartbeatAt,
|
|
125
234
|
stageSummaries,
|
|
126
|
-
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,
|
|
127
241
|
nextAction,
|
|
128
242
|
realRunHealth,
|
|
129
243
|
},
|
|
@@ -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;
|
|
@@ -75,6 +76,8 @@ export interface RealRunHealthDigestProjection {
|
|
|
75
76
|
hasRealClosure: boolean;
|
|
76
77
|
hasQuietArtifact: boolean;
|
|
77
78
|
hasDreamArtifact: boolean;
|
|
79
|
+
hasFreshImpulseContext: boolean;
|
|
80
|
+
hasProjectionFeedback: boolean;
|
|
78
81
|
missingStage?: string;
|
|
79
82
|
missingReason?: string;
|
|
80
83
|
}
|
|
@@ -136,6 +139,12 @@ export interface StateMemoryDigestPort {
|
|
|
136
139
|
export interface HeartbeatDigestAssemblerDeps {
|
|
137
140
|
auditStore: AppendOnlyAuditStore;
|
|
138
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;
|
|
139
148
|
/**
|
|
140
149
|
* Optional delivery adapter (T-OBS.C.4).
|
|
141
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,15 +270,7 @@ export async function generateHeartbeatDigest(date, deps) {
|
|
|
227
270
|
goalSummary,
|
|
228
271
|
quietDreamSummary,
|
|
229
272
|
healthSummary,
|
|
230
|
-
realRunHealth
|
|
231
|
-
gatePassed: false,
|
|
232
|
-
contractSmokeOnly: true,
|
|
233
|
-
seededStateDetected: false,
|
|
234
|
-
hasRealClosure: false,
|
|
235
|
-
hasQuietArtifact: false,
|
|
236
|
-
hasDreamArtifact: false,
|
|
237
|
-
missingReason: "Real-run health not evaluated — call checkRealRunHealth before digest generation",
|
|
238
|
-
},
|
|
273
|
+
realRunHealth,
|
|
239
274
|
};
|
|
240
275
|
// T-OBS.C.4: delivery hook — attempt delivery if adapter is provided
|
|
241
276
|
if (deliveryAdapter) {
|
|
@@ -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" | "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" | "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>>;
|
|
@@ -197,7 +197,6 @@ const STATE_SCHEMA_SQL = `
|
|
|
197
197
|
entities_json TEXT,
|
|
198
198
|
novelty TEXT,
|
|
199
199
|
relevance REAL,
|
|
200
|
-
relevance_class TEXT,
|
|
201
200
|
summary TEXT,
|
|
202
201
|
risk_flags_json TEXT,
|
|
203
202
|
confidence REAL,
|
|
@@ -221,12 +220,14 @@ const STATE_SCHEMA_SQL = `
|
|
|
221
220
|
payload_json TEXT,
|
|
222
221
|
lifecycle_status TEXT NOT NULL DEFAULT 'pending'
|
|
223
222
|
);
|
|
224
|
-
CREATE TABLE IF NOT EXISTS action_closure_record (
|
|
225
|
-
id TEXT PRIMARY KEY,
|
|
226
|
-
created_at TEXT NOT NULL,
|
|
227
|
-
cycle_id TEXT NOT NULL,
|
|
228
|
-
|
|
229
|
-
|
|
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,
|
|
230
231
|
status TEXT NOT NULL,
|
|
231
232
|
reason TEXT,
|
|
232
233
|
next_state TEXT,
|
|
@@ -242,7 +243,6 @@ const STATE_SCHEMA_SQL = `
|
|
|
242
243
|
closure_count INTEGER NOT NULL DEFAULT 0,
|
|
243
244
|
memory_candidate_count INTEGER NOT NULL DEFAULT 0,
|
|
244
245
|
source_refs_json TEXT NOT NULL,
|
|
245
|
-
closure_refs_json TEXT,
|
|
246
246
|
redaction_class TEXT NOT NULL DEFAULT 'none',
|
|
247
247
|
payload_json TEXT,
|
|
248
248
|
lifecycle_status TEXT NOT NULL DEFAULT 'pending'
|
|
@@ -329,6 +329,22 @@ const STATE_SCHEMA_SQL = `
|
|
|
329
329
|
payload_json TEXT,
|
|
330
330
|
updated_at TEXT NOT NULL
|
|
331
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);
|
|
332
348
|
`;
|
|
333
349
|
function resolveDbPath(filename) {
|
|
334
350
|
if (path.isAbsolute(filename) || filename === ":memory:") {
|
|
@@ -350,6 +366,10 @@ function bootstrapStateSchema(sqlite) {
|
|
|
350
366
|
function applyStateSchemaMigrations(sqlite) {
|
|
351
367
|
const migrations = [
|
|
352
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)",
|
|
353
373
|
];
|
|
354
374
|
for (const sql of migrations) {
|
|
355
375
|
try {
|