@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
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "second-nature",
|
|
3
3
|
"name": "Second Nature",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.5",
|
|
5
5
|
"description": "OpenClaw native plugin with synchronous surface registration and bundled runtime spine. Set SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot to the same path as the agent workspace. Agent inner guide is packaged as agent-inner-guide.md. v7 ops surface: self_health, tool_affordance, heartbeat_digest, snapshot:capture, narrative:diff, timeline, restore, runtime_secret_bootstrap, connector:run, guidance_payload.",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onStartup": true,
|
package/package.json
CHANGED
|
@@ -1067,6 +1067,8 @@ export function createOpsRouter(deps) {
|
|
|
1067
1067
|
hasRealClosure: realRunResult.gate.hasRealClosure,
|
|
1068
1068
|
hasQuietArtifact: realRunResult.gate.hasQuietArtifact,
|
|
1069
1069
|
hasDreamArtifact: realRunResult.gate.hasDreamArtifact,
|
|
1070
|
+
hasFreshImpulseContext: realRunResult.gate.hasFreshImpulseContext,
|
|
1071
|
+
hasProjectionFeedback: realRunResult.gate.hasProjectionFeedback,
|
|
1070
1072
|
missingStage: realRunResult.gate.missingStage,
|
|
1071
1073
|
missingReason: realRunResult.gate.missingReason,
|
|
1072
1074
|
};
|
|
@@ -1079,6 +1081,8 @@ export function createOpsRouter(deps) {
|
|
|
1079
1081
|
hasRealClosure: false,
|
|
1080
1082
|
hasQuietArtifact: false,
|
|
1081
1083
|
hasDreamArtifact: false,
|
|
1084
|
+
hasFreshImpulseContext: false,
|
|
1085
|
+
hasProjectionFeedback: false,
|
|
1082
1086
|
missingReason: "Real-run health check degraded: " + realRunResult.degraded.reason,
|
|
1083
1087
|
};
|
|
1084
1088
|
}
|
|
@@ -73,6 +73,7 @@ export interface CooldownLedgerPort {
|
|
|
73
73
|
loadCooldownState(platformId: string, intent: CapabilityIntent): Promise<{
|
|
74
74
|
blocked: boolean;
|
|
75
75
|
retryAfterMs?: number;
|
|
76
|
+
reason?: string;
|
|
76
77
|
}>;
|
|
77
78
|
}
|
|
78
79
|
export interface RouteContextPort extends CredentialContextPort, CooldownLedgerPort {
|
|
@@ -61,6 +61,23 @@ function readRetryAfterMs(input) {
|
|
|
61
61
|
}
|
|
62
62
|
return undefined;
|
|
63
63
|
}
|
|
64
|
+
function readStatusCode(record) {
|
|
65
|
+
if (typeof record.status === "number")
|
|
66
|
+
return record.status;
|
|
67
|
+
if (typeof record.statusCode === "number")
|
|
68
|
+
return record.statusCode;
|
|
69
|
+
if (typeof record.status === "string") {
|
|
70
|
+
const parsed = Number.parseInt(record.status, 10);
|
|
71
|
+
if (Number.isFinite(parsed))
|
|
72
|
+
return parsed;
|
|
73
|
+
}
|
|
74
|
+
if (typeof record.statusCode === "string") {
|
|
75
|
+
const parsed = Number.parseInt(record.statusCode, 10);
|
|
76
|
+
if (Number.isFinite(parsed))
|
|
77
|
+
return parsed;
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
64
81
|
export function classifyFailure(error) {
|
|
65
82
|
if (error instanceof ConnectorPolicyError) {
|
|
66
83
|
return {
|
|
@@ -74,6 +91,34 @@ export function classifyFailure(error) {
|
|
|
74
91
|
}
|
|
75
92
|
if (error && typeof error === "object") {
|
|
76
93
|
const record = error;
|
|
94
|
+
const status = readStatusCode(record);
|
|
95
|
+
if (status !== undefined) {
|
|
96
|
+
if (status === 429) {
|
|
97
|
+
return {
|
|
98
|
+
class: "rate_limited",
|
|
99
|
+
retryable: RETRYABLE_BY_CLASS.rate_limited,
|
|
100
|
+
retryAfterMs: readRetryAfterMs(record),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (status === 401 || status === 403) {
|
|
104
|
+
return {
|
|
105
|
+
class: "auth_failure",
|
|
106
|
+
retryable: RETRYABLE_BY_CLASS.auth_failure,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (status === 400 || status === 404 || status === 422) {
|
|
110
|
+
return {
|
|
111
|
+
class: "permanent_input_error",
|
|
112
|
+
retryable: RETRYABLE_BY_CLASS.permanent_input_error,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (status >= 500 && status <= 599) {
|
|
116
|
+
return {
|
|
117
|
+
class: "transport_failure",
|
|
118
|
+
retryable: RETRYABLE_BY_CLASS.transport_failure,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
77
122
|
const code = record.code;
|
|
78
123
|
if (typeof code === "string") {
|
|
79
124
|
if (code === "auth_failure")
|
|
@@ -152,32 +197,6 @@ export function classifyFailure(error) {
|
|
|
152
197
|
retryable: RETRYABLE_BY_CLASS.unknown_platform_change,
|
|
153
198
|
};
|
|
154
199
|
}
|
|
155
|
-
const status = record.status;
|
|
156
|
-
if (status === 429) {
|
|
157
|
-
return {
|
|
158
|
-
class: "rate_limited",
|
|
159
|
-
retryable: RETRYABLE_BY_CLASS.rate_limited,
|
|
160
|
-
retryAfterMs: readRetryAfterMs(record),
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
if (status === 401 || status === 403) {
|
|
164
|
-
return {
|
|
165
|
-
class: "auth_failure",
|
|
166
|
-
retryable: RETRYABLE_BY_CLASS.auth_failure,
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
if (status === 400 || status === 404 || status === 422) {
|
|
170
|
-
return {
|
|
171
|
-
class: "permanent_input_error",
|
|
172
|
-
retryable: RETRYABLE_BY_CLASS.permanent_input_error,
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
if (status === 500 || status === 502 || status === 503 || status === 504) {
|
|
176
|
-
return {
|
|
177
|
-
class: "transport_failure",
|
|
178
|
-
retryable: RETRYABLE_BY_CLASS.transport_failure,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
200
|
}
|
|
182
201
|
return {
|
|
183
202
|
class: "unknown_platform_change",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConnectorCooldownPort — Durable cooldown ledger for repeated terminal failures.
|
|
3
|
+
*
|
|
4
|
+
* Core logic: Track terminal failures per platform/capability and block replay
|
|
5
|
+
* for a bounded window after repeated failures. Successful recovery is allowed
|
|
6
|
+
* to bypass stale cooldown.
|
|
7
|
+
*
|
|
8
|
+
* Design authority:
|
|
9
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/connector-system.md §6`
|
|
10
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/body-tool-system.md §4`
|
|
11
|
+
*
|
|
12
|
+
* Dependencies:
|
|
13
|
+
* - `src/storage/v8-state-stores.js` (read/write connector cooldown state)
|
|
14
|
+
* - `src/connectors/base/failure-taxonomy.js` (FailureClass, retryable lookup)
|
|
15
|
+
*
|
|
16
|
+
* Boundary:
|
|
17
|
+
* - Does not execute connectors; only records/read cooldown state.
|
|
18
|
+
* - Does not permanently blacklist platforms; cooldown expires.
|
|
19
|
+
*/
|
|
20
|
+
import type { StateDatabase } from "../../storage/db/index.js";
|
|
21
|
+
import type { CooldownPort } from "../base/policy-layer.js";
|
|
22
|
+
export declare function createConnectorCooldownPort(db: StateDatabase): CooldownPort;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConnectorCooldownPort — Durable cooldown ledger for repeated terminal failures.
|
|
3
|
+
*
|
|
4
|
+
* Core logic: Track terminal failures per platform/capability and block replay
|
|
5
|
+
* for a bounded window after repeated failures. Successful recovery is allowed
|
|
6
|
+
* to bypass stale cooldown.
|
|
7
|
+
*
|
|
8
|
+
* Design authority:
|
|
9
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/connector-system.md §6`
|
|
10
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/body-tool-system.md §4`
|
|
11
|
+
*
|
|
12
|
+
* Dependencies:
|
|
13
|
+
* - `src/storage/v8-state-stores.js` (read/write connector cooldown state)
|
|
14
|
+
* - `src/connectors/base/failure-taxonomy.js` (FailureClass, retryable lookup)
|
|
15
|
+
*
|
|
16
|
+
* Boundary:
|
|
17
|
+
* - Does not execute connectors; only records/read cooldown state.
|
|
18
|
+
* - Does not permanently blacklist platforms; cooldown expires.
|
|
19
|
+
*/
|
|
20
|
+
import { readConnectorCooldownState, writeConnectorCooldownState, } from "../../storage/v8-state-stores.js";
|
|
21
|
+
// ───────────────────────────────────────────────────────────────
|
|
22
|
+
// Config
|
|
23
|
+
// ───────────────────────────────────────────────────────────────
|
|
24
|
+
const DEFAULT_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes
|
|
25
|
+
const TERMINAL_FAILURE_THRESHOLD = 2;
|
|
26
|
+
const RETRYABLE_FAILURE_CLASSES = new Set([
|
|
27
|
+
"transport_failure",
|
|
28
|
+
"rate_limited",
|
|
29
|
+
"timeout",
|
|
30
|
+
"concurrency_conflict",
|
|
31
|
+
]);
|
|
32
|
+
// ───────────────────────────────────────────────────────────────
|
|
33
|
+
// Helpers
|
|
34
|
+
// ───────────────────────────────────────────────────────────────
|
|
35
|
+
function makeCooldownId(platformId, capabilityId) {
|
|
36
|
+
return `cooldown_${platformId}_${capabilityId}`;
|
|
37
|
+
}
|
|
38
|
+
function addMs(iso, ms) {
|
|
39
|
+
return new Date(new Date(iso).getTime() + ms).toISOString();
|
|
40
|
+
}
|
|
41
|
+
function isAfter(a, b) {
|
|
42
|
+
return new Date(a).getTime() > new Date(b).getTime();
|
|
43
|
+
}
|
|
44
|
+
// ───────────────────────────────────────────────────────────────
|
|
45
|
+
// Public API
|
|
46
|
+
// ───────────────────────────────────────────────────────────────
|
|
47
|
+
export function createConnectorCooldownPort(db) {
|
|
48
|
+
return {
|
|
49
|
+
async isBlocked(platformId, intent) {
|
|
50
|
+
const read = await readConnectorCooldownState(db, platformId, intent);
|
|
51
|
+
if (read.degraded) {
|
|
52
|
+
// Fail-closed: if we cannot read cooldown state, prevent replay to avoid hammering
|
|
53
|
+
return {
|
|
54
|
+
blocked: true,
|
|
55
|
+
reason: "cooldown_state_unreadable",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (!read.row) {
|
|
59
|
+
return { blocked: false };
|
|
60
|
+
}
|
|
61
|
+
const now = new Date().toISOString();
|
|
62
|
+
const blocked = isAfter(read.row.blockedUntil, now);
|
|
63
|
+
return {
|
|
64
|
+
blocked,
|
|
65
|
+
retryAfterMs: blocked
|
|
66
|
+
? Math.max(0, new Date(read.row.blockedUntil).getTime() - new Date(now).getTime())
|
|
67
|
+
: undefined,
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
async markFailure(platformId, intent, failureClass, retryAfterMs) {
|
|
71
|
+
const id = makeCooldownId(platformId, intent);
|
|
72
|
+
const now = new Date().toISOString();
|
|
73
|
+
const existing = await readConnectorCooldownState(db, platformId, intent);
|
|
74
|
+
const isRetryable = RETRYABLE_FAILURE_CLASSES.has(failureClass);
|
|
75
|
+
let failureCount = 1;
|
|
76
|
+
let terminalCount = isRetryable ? 0 : 1;
|
|
77
|
+
let blockedUntil = now;
|
|
78
|
+
if (!existing.degraded && existing.row) {
|
|
79
|
+
failureCount = existing.row.failureCount + 1;
|
|
80
|
+
terminalCount = (existing.row.terminalCount ?? 0) + (isRetryable ? 0 : 1);
|
|
81
|
+
// Extend blocked window if already blocked
|
|
82
|
+
if (isAfter(existing.row.blockedUntil, now)) {
|
|
83
|
+
blockedUntil = existing.row.blockedUntil;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (retryAfterMs && retryAfterMs > 0) {
|
|
87
|
+
// Rate-limit or explicit retry-after takes precedence
|
|
88
|
+
blockedUntil = addMs(now, retryAfterMs);
|
|
89
|
+
}
|
|
90
|
+
else if (!isRetryable && terminalCount >= TERMINAL_FAILURE_THRESHOLD) {
|
|
91
|
+
// Repeated terminal failures enter bounded cooldown
|
|
92
|
+
blockedUntil = addMs(now, DEFAULT_COOLDOWN_MS);
|
|
93
|
+
}
|
|
94
|
+
else if (isRetryable) {
|
|
95
|
+
// Retryable failures do not accumulate terminal cooldown
|
|
96
|
+
blockedUntil = now;
|
|
97
|
+
}
|
|
98
|
+
await writeConnectorCooldownState(db, {
|
|
99
|
+
id,
|
|
100
|
+
platformId,
|
|
101
|
+
capabilityId: intent,
|
|
102
|
+
failureClass,
|
|
103
|
+
retryAfterMs: retryAfterMs ?? null,
|
|
104
|
+
blockedUntil,
|
|
105
|
+
failureCount,
|
|
106
|
+
terminalCount,
|
|
107
|
+
sourceRefs: [
|
|
108
|
+
{
|
|
109
|
+
uri: `sn://cooldown/${platformId}/${intent}`,
|
|
110
|
+
family: "audit",
|
|
111
|
+
id,
|
|
112
|
+
redactionClass: "none",
|
|
113
|
+
resolveStatus: "resolvable",
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
payloadJson: JSON.stringify({ markedAt: now, failureCount, terminalCount, isRetryable }),
|
|
117
|
+
createdAt: existing.row?.createdAt ?? now,
|
|
118
|
+
updatedAt: now,
|
|
119
|
+
redactionClass: "none",
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -19,6 +19,7 @@ import { parseConnectorManifestV6 } from "../manifest/manifest-parser.js";
|
|
|
19
19
|
import fs from "node:fs";
|
|
20
20
|
import path from "node:path";
|
|
21
21
|
import { pathToFileURL } from "node:url";
|
|
22
|
+
import { createConnectorCooldownPort } from "./connector-cooldown-port.js";
|
|
22
23
|
const DEFAULT_AGENT_WORLD_USERNAME = "nyx_ha";
|
|
23
24
|
const DEFAULT_AGENT_WORLD_PROFILE_PATH_TEMPLATE = "/api/agents/profile/{username}";
|
|
24
25
|
function readString(value) {
|
|
@@ -104,7 +105,7 @@ async function fetchAgentWorldJson(input) {
|
|
|
104
105
|
body: input.body === undefined ? undefined : JSON.stringify(input.body),
|
|
105
106
|
});
|
|
106
107
|
if (!resp.ok) {
|
|
107
|
-
throw { code: "api_error", detail: `agent-world ${input.label}: ${resp.status}` };
|
|
108
|
+
throw { code: "api_error", status: resp.status, detail: `agent-world ${input.label}: ${resp.status}` };
|
|
108
109
|
}
|
|
109
110
|
return resp.json();
|
|
110
111
|
}
|
|
@@ -147,7 +148,7 @@ async function fetchEvoMapJson(input) {
|
|
|
147
148
|
body: input.body === undefined ? undefined : JSON.stringify(input.body),
|
|
148
149
|
});
|
|
149
150
|
if (!resp.ok) {
|
|
150
|
-
throw { code: "api_error", detail: `evomap ${input.label}: ${resp.status}` };
|
|
151
|
+
throw { code: "api_error", status: resp.status, detail: `evomap ${input.label}: ${resp.status}` };
|
|
151
152
|
}
|
|
152
153
|
return resp.json();
|
|
153
154
|
}
|
|
@@ -256,6 +257,8 @@ function createDeclarativeHttpRunner(manifest, credential) {
|
|
|
256
257
|
body: method !== "GET" && request.payload ? JSON.stringify(request.payload) : undefined,
|
|
257
258
|
});
|
|
258
259
|
if (!resp.ok) {
|
|
260
|
+
const status = resp.status;
|
|
261
|
+
const body = await resp.text().catch(() => "");
|
|
259
262
|
return {
|
|
260
263
|
platformId: request.platformId,
|
|
261
264
|
channel: plan.channel,
|
|
@@ -263,7 +266,8 @@ function createDeclarativeHttpRunner(manifest, credential) {
|
|
|
263
266
|
success: false,
|
|
264
267
|
error: {
|
|
265
268
|
code: "api_error",
|
|
266
|
-
|
|
269
|
+
status,
|
|
270
|
+
detail: `HTTP ${status}${body ? `: ${body.slice(0, 200)}` : ""}`,
|
|
267
271
|
},
|
|
268
272
|
};
|
|
269
273
|
}
|
|
@@ -618,7 +622,8 @@ export function createConnectorExecutorAdapter(options) {
|
|
|
618
622
|
registry.register({ ...agentWorldManifest });
|
|
619
623
|
registry.register({ ...instreetManifest });
|
|
620
624
|
registerWorkspaceManifests(registry, options.workspaceRoot);
|
|
621
|
-
const
|
|
625
|
+
const cooldownPort = createConnectorCooldownPort(options.stateDb);
|
|
626
|
+
const routeContextPort = createCredentialRouteContextPort(vault, options.stateDb);
|
|
622
627
|
const routePlanner = new ConnectorRoutePlanner(registry, routeContextPort, new ChannelHealthStore());
|
|
623
628
|
const telemetry = new ExecutionTelemetry(options.observabilityDb);
|
|
624
629
|
const executionRunner = createAdaptiveExecutionRunner(vault, options.workspaceRoot);
|
|
@@ -626,6 +631,7 @@ export function createConnectorExecutorAdapter(options) {
|
|
|
626
631
|
routePlanner,
|
|
627
632
|
executionRunner,
|
|
628
633
|
telemetry,
|
|
634
|
+
cooldownPort,
|
|
629
635
|
effectCommitLedger: new InMemoryEffectCommitLedger(),
|
|
630
636
|
retryPolicy: { maxRetries: 2, jitter: true },
|
|
631
637
|
});
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Loads decrypted credentials from state DB and maps them to the
|
|
5
5
|
* CredentialContext shape expected by ConnectorRoutePlanner.
|
|
6
|
-
* Cooldown is
|
|
6
|
+
* Cooldown state is loaded from connector_cooldown_state table.
|
|
7
7
|
*/
|
|
8
8
|
import type { RouteContextPort } from "../base/contract.js";
|
|
9
9
|
import type { CredentialVault } from "../../storage/services/credential-vault.js";
|
|
10
|
-
|
|
10
|
+
import type { StateDatabase } from "../../storage/db/index.js";
|
|
11
|
+
export declare function createCredentialRouteContextPort(vault: CredentialVault, db: StateDatabase): RouteContextPort;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { readConnectorCooldownState } from "../../storage/v8-state-stores.js";
|
|
2
|
+
export function createCredentialRouteContextPort(vault, db) {
|
|
2
3
|
return {
|
|
3
4
|
async loadCredentialState(platformId) {
|
|
4
5
|
const ctx = await vault.loadCredentialContext(platformId);
|
|
@@ -12,8 +13,23 @@ export function createCredentialRouteContextPort(vault) {
|
|
|
12
13
|
}
|
|
13
14
|
return ctx;
|
|
14
15
|
},
|
|
15
|
-
async loadCooldownState() {
|
|
16
|
-
|
|
16
|
+
async loadCooldownState(platformId, intent) {
|
|
17
|
+
const read = await readConnectorCooldownState(db, platformId, intent);
|
|
18
|
+
if (read.degraded) {
|
|
19
|
+
// Fail-closed on unreadable cooldown state
|
|
20
|
+
return { blocked: true, reason: "cooldown_state_unreadable" };
|
|
21
|
+
}
|
|
22
|
+
if (!read.row) {
|
|
23
|
+
return { blocked: false };
|
|
24
|
+
}
|
|
25
|
+
const now = new Date().toISOString();
|
|
26
|
+
const blocked = new Date(read.row.blockedUntil).getTime() > new Date(now).getTime();
|
|
27
|
+
return {
|
|
28
|
+
blocked,
|
|
29
|
+
retryAfterMs: blocked
|
|
30
|
+
? Math.max(0, new Date(read.row.blockedUntil).getTime() - new Date(now).getTime())
|
|
31
|
+
: undefined,
|
|
32
|
+
};
|
|
17
33
|
},
|
|
18
34
|
};
|
|
19
35
|
}
|
|
@@ -56,6 +56,8 @@ export declare function recordRememberClosure(db: StateDatabase, cycleId: string
|
|
|
56
56
|
export declare function recordPolicyOutcomeClosure(db: StateDatabase, cycleId: string, closureStatus: ClosureStatus, reason: V8ReasonCode, params: {
|
|
57
57
|
proposalId?: string;
|
|
58
58
|
decisionId?: string;
|
|
59
|
+
platformId?: string;
|
|
60
|
+
capabilityId?: string;
|
|
59
61
|
downgradedActionKind?: string;
|
|
60
62
|
postProcessing?: string[];
|
|
61
63
|
nextState?: string;
|
|
@@ -63,6 +65,8 @@ export declare function recordPolicyOutcomeClosure(db: StateDatabase, cycleId: s
|
|
|
63
65
|
export declare function recordExecutionClosure(db: StateDatabase, cycleId: string, closureStatus: "completed" | "failed", reason: V8ReasonCode, params: {
|
|
64
66
|
proposalId: string;
|
|
65
67
|
decisionId: string;
|
|
68
|
+
platformId?: string;
|
|
69
|
+
capabilityId?: string;
|
|
66
70
|
executionResultRef?: string;
|
|
67
71
|
outputSummary?: string;
|
|
68
72
|
nextState?: string;
|
|
@@ -46,6 +46,7 @@ export async function recordNoActionClosure(db, cycleId, noActionReason, options
|
|
|
46
46
|
id: closureId,
|
|
47
47
|
createdAt: now,
|
|
48
48
|
cycleId,
|
|
49
|
+
platformId: "heartbeat",
|
|
49
50
|
status: "no_action",
|
|
50
51
|
reason: noActionReason,
|
|
51
52
|
nextState: "await_next_cycle",
|
|
@@ -122,6 +123,8 @@ export async function recordPolicyOutcomeClosure(db, cycleId, closureStatus, rea
|
|
|
122
123
|
id: closureId,
|
|
123
124
|
createdAt: now,
|
|
124
125
|
cycleId,
|
|
126
|
+
platformId: params.platformId ?? "heartbeat",
|
|
127
|
+
capabilityId: params.capabilityId,
|
|
125
128
|
proposalId: params.proposalId,
|
|
126
129
|
decisionId: params.decisionId,
|
|
127
130
|
status: closureStatus,
|
|
@@ -161,6 +164,8 @@ export async function recordExecutionClosure(db, cycleId, closureStatus, reason,
|
|
|
161
164
|
id: closureId,
|
|
162
165
|
createdAt: now,
|
|
163
166
|
cycleId,
|
|
167
|
+
platformId: params.platformId ?? "heartbeat",
|
|
168
|
+
capabilityId: params.capabilityId,
|
|
164
169
|
proposalId: params.proposalId,
|
|
165
170
|
decisionId: params.decisionId,
|
|
166
171
|
status: closureStatus,
|
|
@@ -136,6 +136,7 @@ export async function buildActionProposal(db, judgmentVerdictId, options) {
|
|
|
136
136
|
id: closureId,
|
|
137
137
|
createdAt: now,
|
|
138
138
|
cycleId,
|
|
139
|
+
platformId: "heartbeat",
|
|
139
140
|
status: "completed",
|
|
140
141
|
reason: "remember_for_review",
|
|
141
142
|
nextState: "pending_daily_review",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* Test coverage: tests/unit/control-plane/heartbeat-cycle-trace.test.ts
|
|
23
23
|
*/
|
|
24
24
|
import type { StateDatabase } from "../../../storage/db/index.js";
|
|
25
|
+
import { type DailyRhythmState } from "../quiet-dream/daily-rhythm-scheduler.js";
|
|
25
26
|
import type { SourceRef, DegradedOperationResult, V8ReasonCode } from "../../../shared/types/v8-contracts.js";
|
|
26
27
|
export interface HeartbeatOrchestrationRequest {
|
|
27
28
|
workspaceRoot: string;
|
|
@@ -34,5 +35,6 @@ export interface HeartbeatOrchestrationResult {
|
|
|
34
35
|
closureRef?: SourceRef;
|
|
35
36
|
noActionReason?: V8ReasonCode;
|
|
36
37
|
degraded?: DegradedOperationResult;
|
|
38
|
+
rhythmState?: DailyRhythmState;
|
|
37
39
|
}
|
|
38
40
|
export declare function runHeartbeatCycle(db: StateDatabase, request: HeartbeatOrchestrationRequest): Promise<HeartbeatOrchestrationResult | DegradedOperationResult>;
|
|
@@ -30,6 +30,7 @@ import { buildActionProposal, } from "../action/action-proposal-builder.js";
|
|
|
30
30
|
import { evaluateActionPolicy } from "../action/autonomy-policy-evaluator.js";
|
|
31
31
|
import { dispatchAllowedAction } from "../action/policy-bound-dispatch.js";
|
|
32
32
|
import { recordNoActionClosure, recordRememberClosure, recordPolicyOutcomeClosure, recordExecutionClosure, } from "../action/action-closure-recorder.js";
|
|
33
|
+
import { checkDailyRhythm } from "../quiet-dream/daily-rhythm-scheduler.js";
|
|
33
34
|
// ───────────────────────────────────────────────────────────────
|
|
34
35
|
// Helpers
|
|
35
36
|
// ───────────────────────────────────────────────────────────────
|
|
@@ -43,6 +44,66 @@ async function nextCycleSequence(db) {
|
|
|
43
44
|
function buildCycleId(sequence, now) {
|
|
44
45
|
return `cyc_${now.replace(/[:.]/g, "")}_${sequence}`;
|
|
45
46
|
}
|
|
47
|
+
async function advanceAndRecordDailyRhythm(db, cycleId, cycleSequence, cycleRef, now) {
|
|
48
|
+
try {
|
|
49
|
+
const rhythmResult = await checkDailyRhythm(db, { now });
|
|
50
|
+
if ("status" in rhythmResult && rhythmResult.status === "checked") {
|
|
51
|
+
await recordLoopStageEvent(db, {
|
|
52
|
+
id: `evt_${cycleId}_daily_rhythm`,
|
|
53
|
+
cycleId,
|
|
54
|
+
cycleSequence,
|
|
55
|
+
stage: "quiet",
|
|
56
|
+
status: "completed",
|
|
57
|
+
occurredAt: new Date().toISOString(),
|
|
58
|
+
sourceRefs: [
|
|
59
|
+
cycleRef,
|
|
60
|
+
{
|
|
61
|
+
uri: `sn://rhythm/${rhythmResult.state.day}`,
|
|
62
|
+
family: "dream_run",
|
|
63
|
+
id: `rhythm_${rhythmResult.state.day}`,
|
|
64
|
+
redactionClass: "none",
|
|
65
|
+
resolveStatus: "resolvable",
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
return { rhythmState: rhythmResult.state };
|
|
70
|
+
}
|
|
71
|
+
const degraded = rhythmResult;
|
|
72
|
+
await recordLoopStageEvent(db, {
|
|
73
|
+
id: `evt_${cycleId}_daily_rhythm`,
|
|
74
|
+
cycleId,
|
|
75
|
+
cycleSequence,
|
|
76
|
+
stage: "quiet",
|
|
77
|
+
status: "failed",
|
|
78
|
+
occurredAt: new Date().toISOString(),
|
|
79
|
+
reason: degraded.reason,
|
|
80
|
+
sourceRefs: [cycleRef],
|
|
81
|
+
});
|
|
82
|
+
return { rhythmDegraded: degraded };
|
|
83
|
+
}
|
|
84
|
+
catch (rhythmErr) {
|
|
85
|
+
const errMsg = rhythmErr instanceof Error ? rhythmErr.message : String(rhythmErr);
|
|
86
|
+
const degraded = {
|
|
87
|
+
status: "degraded",
|
|
88
|
+
reason: "state_unreadable",
|
|
89
|
+
ownerStage: "quiet",
|
|
90
|
+
sourceRefs: [cycleRef],
|
|
91
|
+
operatorNextAction: `Daily rhythm check failed: ${errMsg.slice(0, 120)}`,
|
|
92
|
+
retryable: true,
|
|
93
|
+
};
|
|
94
|
+
await recordLoopStageEvent(db, {
|
|
95
|
+
id: `evt_${cycleId}_daily_rhythm`,
|
|
96
|
+
cycleId,
|
|
97
|
+
cycleSequence,
|
|
98
|
+
stage: "quiet",
|
|
99
|
+
status: "failed",
|
|
100
|
+
occurredAt: new Date().toISOString(),
|
|
101
|
+
reason: degraded.reason,
|
|
102
|
+
sourceRefs: [cycleRef],
|
|
103
|
+
});
|
|
104
|
+
return { rhythmDegraded: degraded };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
46
107
|
// ───────────────────────────────────────────────────────────────
|
|
47
108
|
// Public API
|
|
48
109
|
// ───────────────────────────────────────────────────────────────
|
|
@@ -130,6 +191,7 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
130
191
|
reason: degradedReason,
|
|
131
192
|
sourceRefs: degradedClosureRef ? [degradedClosureRef, cycleRef] : [cycleRef],
|
|
132
193
|
});
|
|
194
|
+
const { rhythmState } = await advanceAndRecordDailyRhythm(db, cycleId, cycleSequence, cycleRef, now);
|
|
133
195
|
return {
|
|
134
196
|
cycleId,
|
|
135
197
|
cycleSequence,
|
|
@@ -145,6 +207,7 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
145
207
|
retryable: true,
|
|
146
208
|
}
|
|
147
209
|
: undefined,
|
|
210
|
+
rhythmState,
|
|
148
211
|
};
|
|
149
212
|
}
|
|
150
213
|
const cards = perceptionResult.cards;
|
|
@@ -182,11 +245,13 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
182
245
|
reason: "evidence_batch_empty",
|
|
183
246
|
sourceRefs: emptyClosureRef ? [emptyClosureRef, cycleRef] : [cycleRef],
|
|
184
247
|
});
|
|
248
|
+
const { rhythmState } = await advanceAndRecordDailyRhythm(db, cycleId, cycleSequence, cycleRef, now);
|
|
185
249
|
return {
|
|
186
250
|
cycleId,
|
|
187
251
|
cycleSequence,
|
|
188
252
|
closureRef: emptyClosureRef,
|
|
189
253
|
noActionReason: "evidence_batch_empty",
|
|
254
|
+
rhythmState,
|
|
190
255
|
};
|
|
191
256
|
}
|
|
192
257
|
// ── Context assembly: load accepted projections (T-DQ.R.3) ──
|
|
@@ -350,6 +415,8 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
350
415
|
const closureResult = await recordPolicyOutcomeClosure(db, cycleId, closureStatus, decision.decisionReason, {
|
|
351
416
|
proposalId: proposal.id,
|
|
352
417
|
decisionId: decision.id,
|
|
418
|
+
platformId: proposal.targetPlatformId,
|
|
419
|
+
capabilityId: proposal.targetCapabilityId,
|
|
353
420
|
nextState: "await_next_cycle",
|
|
354
421
|
}, { now });
|
|
355
422
|
if ("closureId" in closureResult) {
|
|
@@ -369,6 +436,8 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
369
436
|
const closureResult = await recordPolicyOutcomeClosure(db, cycleId, "downgraded", "guidance_unavailable", {
|
|
370
437
|
proposalId: proposal.id,
|
|
371
438
|
decisionId: decision.id,
|
|
439
|
+
platformId: proposal.targetPlatformId,
|
|
440
|
+
capabilityId: proposal.targetCapabilityId,
|
|
372
441
|
downgradedActionKind: dispatchResult.downgradedActionKind,
|
|
373
442
|
nextState: "await_guidance_recovery",
|
|
374
443
|
}, { now });
|
|
@@ -390,6 +459,8 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
390
459
|
const closureResult = await recordExecutionClosure(db, cycleId, "completed", "policy_allowed", {
|
|
391
460
|
proposalId: proposal.id,
|
|
392
461
|
decisionId: decision.id,
|
|
462
|
+
platformId: proposal.targetPlatformId,
|
|
463
|
+
capabilityId: proposal.targetCapabilityId,
|
|
393
464
|
outputSummary: "Guidance draft dispatched (simulated)",
|
|
394
465
|
nextState: "await_next_cycle",
|
|
395
466
|
}, { now });
|
|
@@ -411,6 +482,8 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
411
482
|
const closureResult = await recordExecutionClosure(db, cycleId, "completed", "policy_allowed", {
|
|
412
483
|
proposalId: proposal.id,
|
|
413
484
|
decisionId: decision.id,
|
|
485
|
+
platformId: proposal.targetPlatformId,
|
|
486
|
+
capabilityId: proposal.targetCapabilityId,
|
|
414
487
|
outputSummary: "Connector dispatch prepared (simulated — T-CP.R.2)",
|
|
415
488
|
nextState: "await_real_execution",
|
|
416
489
|
}, { now });
|
|
@@ -466,11 +539,14 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
466
539
|
}
|
|
467
540
|
noActionReason = "proposal_no_action";
|
|
468
541
|
}
|
|
542
|
+
// T-CP.R.3: Advance daily rhythm after closure/no-action
|
|
543
|
+
const { rhythmState } = await advanceAndRecordDailyRhythm(db, cycleId, cycleSequence, cycleRef, now);
|
|
469
544
|
return {
|
|
470
545
|
cycleId,
|
|
471
546
|
cycleSequence,
|
|
472
547
|
closureRef,
|
|
473
548
|
noActionReason,
|
|
474
549
|
degraded: closureDegraded,
|
|
550
|
+
rhythmState,
|
|
475
551
|
};
|
|
476
552
|
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import type { StateDatabase } from "../../../storage/db/index.js";
|
|
19
19
|
import type { SourceRef, DegradedOperationResult, V8ReasonCode } from "../../../shared/types/v8-contracts.js";
|
|
20
|
+
import type { DailyRhythmState } from "../quiet-dream/daily-rhythm-scheduler.js";
|
|
20
21
|
export interface RealRuntimeSpineOptions {
|
|
21
22
|
workspaceRoot: string;
|
|
22
23
|
state: StateDatabase;
|
|
@@ -29,5 +30,6 @@ export interface RealRuntimeSpineResult {
|
|
|
29
30
|
closureRef?: SourceRef;
|
|
30
31
|
noActionReason?: V8ReasonCode;
|
|
31
32
|
degraded?: DegradedOperationResult;
|
|
33
|
+
rhythmState?: DailyRhythmState;
|
|
32
34
|
}
|
|
33
35
|
export declare function runRealRuntimeHeartbeatCycle(options: RealRuntimeSpineOptions): Promise<RealRuntimeSpineResult | DegradedOperationResult>;
|
|
@@ -37,5 +37,6 @@ export async function runRealRuntimeHeartbeatCycle(options) {
|
|
|
37
37
|
closureRef: orchestrationResult.closureRef,
|
|
38
38
|
noActionReason: orchestrationResult.noActionReason,
|
|
39
39
|
degraded: orchestrationResult.degraded,
|
|
40
|
+
rhythmState: orchestrationResult.rhythmState,
|
|
40
41
|
};
|
|
41
42
|
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import type { StateDatabase } from "../../../storage/db/index.js";
|
|
23
23
|
import type { DegradedOperationResult, V8ReasonCode } from "../../../shared/types/v8-contracts.js";
|
|
24
|
-
export type RhythmStatus = "due" | "completed" | "skipped" | "blocked" | "not_due";
|
|
24
|
+
export type RhythmStatus = "due" | "completed" | "scheduled" | "skipped" | "blocked" | "not_due";
|
|
25
25
|
export interface DailyRhythmState {
|
|
26
26
|
day: string;
|
|
27
27
|
quietStatus: RhythmStatus;
|