@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.
Files changed (53) hide show
  1. package/openclaw.plugin.json +1 -1
  2. package/package.json +1 -1
  3. package/runtime/cli/ops/heartbeat-surface.d.ts +20 -0
  4. package/runtime/cli/ops/heartbeat-surface.js +72 -1
  5. package/runtime/cli/ops/ops-router.js +119 -31
  6. package/runtime/connectors/base/contract.d.ts +11 -0
  7. package/runtime/connectors/base/failure-taxonomy.js +45 -26
  8. package/runtime/connectors/base/policy-bound-write-dispatch.d.ts +29 -0
  9. package/runtime/connectors/base/policy-bound-write-dispatch.js +127 -0
  10. package/runtime/connectors/services/connector-cooldown-port.d.ts +22 -0
  11. package/runtime/connectors/services/connector-cooldown-port.js +123 -0
  12. package/runtime/connectors/services/connector-executor-adapter.js +10 -4
  13. package/runtime/connectors/services/credential-route-context.d.ts +3 -2
  14. package/runtime/connectors/services/credential-route-context.js +19 -3
  15. package/runtime/core/second-nature/action/action-closure-recorder.d.ts +4 -0
  16. package/runtime/core/second-nature/action/action-closure-recorder.js +5 -0
  17. package/runtime/core/second-nature/action/action-proposal-builder.js +1 -0
  18. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.d.ts +2 -0
  19. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.js +412 -25
  20. package/runtime/core/second-nature/control-plane/real-runtime-spine.d.ts +35 -0
  21. package/runtime/core/second-nature/control-plane/real-runtime-spine.js +42 -0
  22. package/runtime/core/second-nature/guidance/impulse-context-reader.d.ts +44 -0
  23. package/runtime/core/second-nature/guidance/impulse-context-reader.js +84 -0
  24. package/runtime/core/second-nature/guidance/impulse-context-writer.d.ts +39 -0
  25. package/runtime/core/second-nature/guidance/impulse-context-writer.js +70 -0
  26. package/runtime/core/second-nature/perception/judgment-engine.d.ts +2 -0
  27. package/runtime/core/second-nature/perception/judgment-engine.js +11 -1
  28. package/runtime/core/second-nature/perception/perception-builder.d.ts +6 -2
  29. package/runtime/core/second-nature/perception/perception-builder.js +18 -7
  30. package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.d.ts +43 -0
  31. package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.js +162 -0
  32. package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.d.ts +2 -2
  33. package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.js +27 -44
  34. package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.d.ts +3 -0
  35. package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.js +4 -0
  36. package/runtime/observability/living-loop-health-gate.d.ts +49 -0
  37. package/runtime/observability/living-loop-health-gate.js +141 -0
  38. package/runtime/observability/loop-status.d.ts +30 -0
  39. package/runtime/observability/loop-status.js +167 -7
  40. package/runtime/observability/services/heartbeat-digest-assembler.d.ts +21 -0
  41. package/runtime/observability/services/heartbeat-digest-assembler.js +44 -0
  42. package/runtime/shared/types/v8-contracts.d.ts +2 -2
  43. package/runtime/storage/db/index.js +60 -6
  44. package/runtime/storage/db/migrations/index.js +4 -0
  45. package/runtime/storage/db/migrations/v8-001-living-perception-loop.js +119 -119
  46. package/runtime/storage/db/migrations/v8-002-perception-contract-alignment.d.ts +12 -0
  47. package/runtime/storage/db/migrations/v8-002-perception-contract-alignment.js +14 -0
  48. package/runtime/storage/db/migrations/v8-003-quiet-closure-refs.d.ts +10 -0
  49. package/runtime/storage/db/migrations/v8-003-quiet-closure-refs.js +12 -0
  50. package/runtime/storage/db/schema/v8-entities.d.ts +874 -0
  51. package/runtime/storage/db/schema/v8-entities.js +62 -1
  52. package/runtime/storage/v8-state-stores.d.ts +41 -2
  53. 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
- detail: `HTTP ${resp.status}: ${await resp.text().catch(() => "")}`,
269
+ status,
270
+ detail: `HTTP ${status}${body ? `: ${body.slice(0, 200)}` : ""}`,
267
271
  },
268
272
  };
269
273
  }
@@ -618,7 +622,8 @@ export function createConnectorExecutorAdapter(options) {
618
622
  registry.register({ ...agentWorldManifest });
619
623
  registry.register({ ...instreetManifest });
620
624
  registerWorkspaceManifests(registry, options.workspaceRoot);
621
- const routeContextPort = createCredentialRouteContextPort(vault);
625
+ const cooldownPort = createConnectorCooldownPort(options.stateDb);
626
+ const routeContextPort = createCredentialRouteContextPort(vault, options.stateDb);
622
627
  const routePlanner = new ConnectorRoutePlanner(registry, routeContextPort, new ChannelHealthStore());
623
628
  const telemetry = new ExecutionTelemetry(options.observabilityDb);
624
629
  const executionRunner = createAdaptiveExecutionRunner(vault, options.workspaceRoot);
@@ -626,6 +631,7 @@ export function createConnectorExecutorAdapter(options) {
626
631
  routePlanner,
627
632
  executionRunner,
628
633
  telemetry,
634
+ cooldownPort,
629
635
  effectCommitLedger: new InMemoryEffectCommitLedger(),
630
636
  retryPolicy: { maxRetries: 2, jitter: true },
631
637
  });
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * Loads decrypted credentials from state DB and maps them to the
5
5
  * CredentialContext shape expected by ConnectorRoutePlanner.
6
- * Cooldown is stubbed (always unblocked) until a cooldown ledger is modeled.
6
+ * Cooldown state is loaded from connector_cooldown_state table.
7
7
  */
8
8
  import type { RouteContextPort } from "../base/contract.js";
9
9
  import type { CredentialVault } from "../../storage/services/credential-vault.js";
10
- export declare function createCredentialRouteContextPort(vault: CredentialVault): RouteContextPort;
10
+ import type { StateDatabase } from "../../storage/db/index.js";
11
+ export declare function createCredentialRouteContextPort(vault: CredentialVault, db: StateDatabase): RouteContextPort;
@@ -1,4 +1,5 @@
1
- export function createCredentialRouteContextPort(vault) {
1
+ import { readConnectorCooldownState } from "../../storage/v8-state-stores.js";
2
+ export function createCredentialRouteContextPort(vault, db) {
2
3
  return {
3
4
  async loadCredentialState(platformId) {
4
5
  const ctx = await vault.loadCredentialContext(platformId);
@@ -12,8 +13,23 @@ export function createCredentialRouteContextPort(vault) {
12
13
  }
13
14
  return ctx;
14
15
  },
15
- async loadCooldownState() {
16
- return { blocked: false };
16
+ async loadCooldownState(platformId, intent) {
17
+ const read = await readConnectorCooldownState(db, platformId, intent);
18
+ if (read.degraded) {
19
+ // Fail-closed on unreadable cooldown state
20
+ return { blocked: true, reason: "cooldown_state_unreadable" };
21
+ }
22
+ if (!read.row) {
23
+ return { blocked: false };
24
+ }
25
+ const now = new Date().toISOString();
26
+ const blocked = new Date(read.row.blockedUntil).getTime() > new Date(now).getTime();
27
+ return {
28
+ blocked,
29
+ retryAfterMs: blocked
30
+ ? Math.max(0, new Date(read.row.blockedUntil).getTime() - new Date(now).getTime())
31
+ : undefined,
32
+ };
17
33
  },
18
34
  };
19
35
  }
@@ -56,6 +56,8 @@ export declare function recordRememberClosure(db: StateDatabase, cycleId: string
56
56
  export declare function recordPolicyOutcomeClosure(db: StateDatabase, cycleId: string, closureStatus: ClosureStatus, reason: V8ReasonCode, params: {
57
57
  proposalId?: string;
58
58
  decisionId?: string;
59
+ platformId?: string;
60
+ capabilityId?: string;
59
61
  downgradedActionKind?: string;
60
62
  postProcessing?: string[];
61
63
  nextState?: string;
@@ -63,6 +65,8 @@ export declare function recordPolicyOutcomeClosure(db: StateDatabase, cycleId: s
63
65
  export declare function recordExecutionClosure(db: StateDatabase, cycleId: string, closureStatus: "completed" | "failed", reason: V8ReasonCode, params: {
64
66
  proposalId: string;
65
67
  decisionId: string;
68
+ platformId?: string;
69
+ capabilityId?: string;
66
70
  executionResultRef?: string;
67
71
  outputSummary?: string;
68
72
  nextState?: string;
@@ -46,6 +46,7 @@ export async function recordNoActionClosure(db, cycleId, noActionReason, options
46
46
  id: closureId,
47
47
  createdAt: now,
48
48
  cycleId,
49
+ platformId: "heartbeat",
49
50
  status: "no_action",
50
51
  reason: noActionReason,
51
52
  nextState: "await_next_cycle",
@@ -122,6 +123,8 @@ export async function recordPolicyOutcomeClosure(db, cycleId, closureStatus, rea
122
123
  id: closureId,
123
124
  createdAt: now,
124
125
  cycleId,
126
+ platformId: params.platformId ?? "heartbeat",
127
+ capabilityId: params.capabilityId,
125
128
  proposalId: params.proposalId,
126
129
  decisionId: params.decisionId,
127
130
  status: closureStatus,
@@ -161,6 +164,8 @@ export async function recordExecutionClosure(db, cycleId, closureStatus, reason,
161
164
  id: closureId,
162
165
  createdAt: now,
163
166
  cycleId,
167
+ platformId: params.platformId ?? "heartbeat",
168
+ capabilityId: params.capabilityId,
164
169
  proposalId: params.proposalId,
165
170
  decisionId: params.decisionId,
166
171
  status: closureStatus,
@@ -136,6 +136,7 @@ export async function buildActionProposal(db, judgmentVerdictId, options) {
136
136
  id: closureId,
137
137
  createdAt: now,
138
138
  cycleId,
139
+ platformId: "heartbeat",
139
140
  status: "completed",
140
141
  reason: "remember_for_review",
141
142
  nextState: "pending_daily_review",
@@ -22,6 +22,7 @@
22
22
  * Test coverage: tests/unit/control-plane/heartbeat-cycle-trace.test.ts
23
23
  */
24
24
  import type { StateDatabase } from "../../../storage/db/index.js";
25
+ import { type DailyRhythmState } from "../quiet-dream/daily-rhythm-scheduler.js";
25
26
  import type { SourceRef, DegradedOperationResult, V8ReasonCode } from "../../../shared/types/v8-contracts.js";
26
27
  export interface HeartbeatOrchestrationRequest {
27
28
  workspaceRoot: string;
@@ -34,5 +35,6 @@ export interface HeartbeatOrchestrationResult {
34
35
  closureRef?: SourceRef;
35
36
  noActionReason?: V8ReasonCode;
36
37
  degraded?: DegradedOperationResult;
38
+ rhythmState?: DailyRhythmState;
37
39
  }
38
40
  export declare function runHeartbeatCycle(db: StateDatabase, request: HeartbeatOrchestrationRequest): Promise<HeartbeatOrchestrationResult | DegradedOperationResult>;