@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,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>;
|