@haaaiawd/second-nature 0.2.4 → 0.2.6

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 (39) hide show
  1. package/openclaw.plugin.json +1 -1
  2. package/package.json +1 -1
  3. package/runtime/cli/commands/index.d.ts +4 -0
  4. package/runtime/cli/commands/index.js +179 -5
  5. package/runtime/cli/index.js +2 -0
  6. package/runtime/cli/ops/ops-router.js +27 -17
  7. package/runtime/connectors/base/contract.d.ts +1 -0
  8. package/runtime/connectors/base/failure-taxonomy.js +45 -26
  9. package/runtime/connectors/services/connector-cooldown-port.d.ts +22 -0
  10. package/runtime/connectors/services/connector-cooldown-port.js +123 -0
  11. package/runtime/connectors/services/connector-executor-adapter.js +10 -4
  12. package/runtime/connectors/services/credential-route-context.d.ts +3 -2
  13. package/runtime/connectors/services/credential-route-context.js +19 -3
  14. package/runtime/core/second-nature/action/action-closure-recorder.d.ts +4 -0
  15. package/runtime/core/second-nature/action/action-closure-recorder.js +5 -0
  16. package/runtime/core/second-nature/action/action-proposal-builder.js +1 -0
  17. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.d.ts +2 -0
  18. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.js +76 -0
  19. package/runtime/core/second-nature/control-plane/real-runtime-spine.d.ts +2 -0
  20. package/runtime/core/second-nature/control-plane/real-runtime-spine.js +1 -0
  21. package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.d.ts +1 -1
  22. package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.js +10 -5
  23. package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.d.ts +2 -2
  24. package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.js +10 -28
  25. package/runtime/observability/db/index.d.ts +2 -0
  26. package/runtime/observability/db/index.js +6 -0
  27. package/runtime/observability/living-loop-health-gate.d.ts +6 -2
  28. package/runtime/observability/living-loop-health-gate.js +52 -5
  29. package/runtime/observability/loop-status.d.ts +19 -0
  30. package/runtime/observability/loop-status.js +121 -7
  31. package/runtime/observability/services/heartbeat-digest-assembler.d.ts +9 -0
  32. package/runtime/observability/services/heartbeat-digest-assembler.js +44 -9
  33. package/runtime/shared/types/v8-contracts.d.ts +1 -1
  34. package/runtime/storage/db/index.d.ts +2 -0
  35. package/runtime/storage/db/index.js +28 -2
  36. package/runtime/storage/db/schema/v8-entities.d.ts +288 -0
  37. package/runtime/storage/db/schema/v8-entities.js +23 -1
  38. package/runtime/storage/v8-state-stores.d.ts +10 -1
  39. package/runtime/storage/v8-state-stores.js +86 -1
@@ -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
- missingReason: "Real-run health check degraded",
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
- // Policy denied count is a placeholder; real implementation would query action closures
115
- const policyDeniedCount = 0;
116
- const nextAction = computeNextAction(overallStatus, snapshot.stalledAt, realRunHealth.missingStage, realRunHealth.missingReason);
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: snapshot.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>>;
@@ -5,6 +5,8 @@ export interface StateDatabase {
5
5
  sqlite: Database;
6
6
  db: ReturnType<typeof drizzle<typeof schema>>;
7
7
  schema: typeof schema;
8
+ /** Persist in-memory sql.js state to disk without closing the connection. */
9
+ flush(): void;
8
10
  close(): void;
9
11
  }
10
12
  export declare function createStateDatabase(filename?: string): StateDatabase;
@@ -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,
@@ -225,6 +224,8 @@ const STATE_SCHEMA_SQL = `
225
224
  id TEXT PRIMARY KEY,
226
225
  created_at TEXT NOT NULL,
227
226
  cycle_id TEXT NOT NULL,
227
+ platform_id TEXT,
228
+ capability_id TEXT,
228
229
  proposal_id TEXT,
229
230
  decision_id TEXT,
230
231
  status TEXT NOT NULL,
@@ -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 {
@@ -374,6 +394,12 @@ export function createStateDatabase(filename = "state.db") {
374
394
  sqlite,
375
395
  db,
376
396
  schema,
397
+ flush() {
398
+ if (!isMemory) {
399
+ const data = sqlite.export();
400
+ fs.writeFileSync(dbPath, Buffer.from(data));
401
+ }
402
+ },
377
403
  close() {
378
404
  if (!isMemory) {
379
405
  const data = sqlite.export();