@haaaiawd/second-nature 0.1.6 → 0.1.8

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/index.js CHANGED
@@ -1,26 +1,322 @@
1
- function createFallbackCommands() {
2
- const commandNames = ["status", "policy", "credential", "quiet", "report", "session", "audit", "explain"];
3
- return commandNames.map((name) => ({
4
- name,
5
- description: `Fallback command shell for ${name}`,
6
- execute: async (_input) => ({
1
+ /**
2
+ * Host-safe Second Nature plugin surface.
3
+ *
4
+ * Core logic:
5
+ * - keep register(api) synchronous so OpenClaw captures services/command/tool before return
6
+ * - avoid importing CLI/runtime DB modules at module-evaluation time because the packaged
7
+ * runtime graph currently contains async sql.js bootstrap that breaks vm sandbox loading
8
+ * - expose a minimal in-memory activation spine so status/lifecycle stay truthful even when
9
+ * the full workspace runtime is not loaded inside the host
10
+ *
11
+ * Dependencies:
12
+ * - only imports runtime lifecycle/service modules that are synchronous at load time
13
+ *
14
+ * Boundaries:
15
+ * - read-only operator flows stay available through command/tool surface
16
+ * - structured mutating flows such as policy set / credential verify remain unavailable here
17
+ * - full evidence-backed workspace runtime can be reintroduced later behind a host-safe boundary
18
+ *
19
+ * Test coverage:
20
+ * - tests/integration/cli/plugin-runtime-registration.test.ts
21
+ * - tests/integration/cli/plugin-packaging-walkthrough.test.ts
22
+ */
23
+ import { startRuntimeService, } from "./runtime/core/second-nature/runtime/service-entry.js";
24
+ import { getLifecycleState, recordRegistration, } from "./runtime/core/second-nature/runtime/lifecycle-service.js";
25
+ const INTERNAL_RUNTIME_TRACE_PREFIX = "sn-runtime-";
26
+ const HOST_SAFE_LIMITATION_MESSAGE = "Host-safe plugin package keeps synchronous register/load semantics, but mutating workspace runtime flows remain unavailable here.";
27
+ let activationSpine = null;
28
+ function trimRuntimeEvidence(spine) {
29
+ if (spine.runtimeEvidence.length > 12) {
30
+ spine.runtimeEvidence.splice(0, spine.runtimeEvidence.length - 12);
31
+ }
32
+ }
33
+ function latestRuntimeEvidence(spine) {
34
+ return spine.runtimeEvidence[spine.runtimeEvidence.length - 1];
35
+ }
36
+ function createUnavailableActionError(code, message, requiredUserInput, nextStep) {
37
+ return {
38
+ ok: false,
39
+ error: {
40
+ code,
41
+ message,
42
+ requiredUserInput,
43
+ nextStep,
44
+ },
45
+ message: HOST_SAFE_LIMITATION_MESSAGE,
46
+ };
47
+ }
48
+ function parseExplainSubject(subjectRaw) {
49
+ const trimmed = subjectRaw.trim();
50
+ if (!trimmed) {
51
+ throw new Error("explain_subject_invalid");
52
+ }
53
+ const separatorIndex = trimmed.indexOf(":");
54
+ if (separatorIndex === -1) {
55
+ throw new Error("explain_subject_requires_id");
56
+ }
57
+ const kind = trimmed.slice(0, separatorIndex).trim();
58
+ const id = trimmed.slice(separatorIndex + 1).trim();
59
+ if (!id) {
60
+ throw new Error("explain_subject_requires_id");
61
+ }
62
+ switch (kind) {
63
+ case "decision":
64
+ return { subjectType: "decision", subjectId: id };
65
+ case "platform":
66
+ case "platform-selection":
67
+ return { subjectType: "platform-selection", subjectId: id };
68
+ case "outreach":
69
+ return { subjectType: "outreach", subjectId: id };
70
+ case "soul":
71
+ case "soul-change":
72
+ return { subjectType: "soul-change", subjectId: id };
73
+ default:
74
+ throw new Error("explain_subject_unsupported");
75
+ }
76
+ }
77
+ function buildStatusPayload(spine) {
78
+ const runtimeEvidence = latestRuntimeEvidence(spine);
79
+ const updatedAt = runtimeEvidence?.createdAt ?? new Date(spine.lifecycleState.lastChangedAt).toISOString();
80
+ return {
81
+ ok: true,
82
+ data: {
83
+ runtime: {
84
+ host: "openclaw-plugin",
85
+ serviceStatus: spine.runtimeHandle.ready ? "running" : "idle",
86
+ updatedAt,
87
+ },
88
+ rhythm: {
89
+ mode: "active",
90
+ windowId: undefined,
91
+ },
92
+ quiet: {
93
+ mode: "unknown",
94
+ lastEvent: runtimeEvidence?.traceId,
95
+ interrupted: undefined,
96
+ },
97
+ connectors: [],
98
+ credentials: [],
99
+ risk: {
100
+ level: "low",
101
+ flags: [],
102
+ },
103
+ },
104
+ };
105
+ }
106
+ function buildQuietPayload(scope) {
107
+ return {
108
+ ok: true,
109
+ data: {
110
+ scope,
111
+ mode: "unknown",
112
+ sourceCount: 0,
113
+ reportCount: 0,
114
+ recentJournalCount: 0,
115
+ },
116
+ };
117
+ }
118
+ function buildReportPayload(day) {
119
+ return {
120
+ ok: true,
121
+ data: {
122
+ day: day && day.trim() ? day : new Date().toISOString().slice(0, 10),
123
+ summary: "",
124
+ highlights: [],
125
+ sourceRefs: [],
126
+ },
127
+ };
128
+ }
129
+ function buildSessionPayload(sessionId) {
130
+ if (!sessionId) {
131
+ return {
7
132
  ok: false,
8
- command: name,
9
- message: "Plugin loaded in packaging fallback mode; reinstall full workspace build for command runtime.",
10
- }),
11
- }));
133
+ error: {
134
+ code: "MISSING_SESSION_ID",
135
+ message: "session show requires sessionId",
136
+ requiredUserInput: ["session_id"],
137
+ nextStep: "reinvoke_session_with_session_id",
138
+ },
139
+ };
140
+ }
141
+ return {
142
+ ok: true,
143
+ data: {
144
+ requestedSessionId: sessionId,
145
+ traceId: sessionId,
146
+ decisionCount: 0,
147
+ attemptCount: 0,
148
+ governanceCount: 0,
149
+ keyFactors: [],
150
+ evidenceRefs: [],
151
+ },
152
+ };
153
+ }
154
+ function buildCredentialPayload(platformId) {
155
+ return {
156
+ ok: true,
157
+ data: {
158
+ platformId: platformId && platformId.trim() ? platformId : "unknown",
159
+ status: "missing",
160
+ nextStep: "provide_credential_context",
161
+ },
162
+ };
12
163
  }
13
- async function resolveCommandRouterSafe() {
164
+ function buildExplainPayload(spine, subjectRaw) {
165
+ if (!subjectRaw?.trim()) {
166
+ return {
167
+ ok: false,
168
+ error: {
169
+ code: "MISSING_EXPLAIN_SUBJECT",
170
+ message: "explain requires subject",
171
+ requiredUserInput: ["subject"],
172
+ nextStep: "reinvoke_explain_with_subject",
173
+ },
174
+ };
175
+ }
176
+ let subject;
14
177
  try {
15
- const mod = await import("./runtime/cli/index.js");
16
- if (mod?.createCommandRouter) {
17
- return mod.createCommandRouter();
18
- }
178
+ subject = parseExplainSubject(subjectRaw);
19
179
  }
20
- catch {
21
- // fall through to fallback router
180
+ catch (error) {
181
+ const code = error.message;
182
+ if (code === "explain_subject_requires_id") {
183
+ return createUnavailableActionError("EXPLAIN_SUBJECT_REQUIRES_ID", "subject must include identifier", ["subject"], "reinvoke_explain_with_supported_subject");
184
+ }
185
+ if (code === "explain_subject_unsupported") {
186
+ return createUnavailableActionError("EXPLAIN_SUBJECT_UNSUPPORTED", "supported subjects are decision:<id>, platform:<id>, outreach:<id>, soul:<id>", ["subject"], "reinvoke_explain_with_supported_subject");
187
+ }
188
+ return createUnavailableActionError("EXPLAIN_SUBJECT_INVALID", "invalid explain subject", ["subject"], "reinvoke_explain_with_supported_subject");
22
189
  }
23
- const commands = createFallbackCommands();
190
+ const runtimeEvidence = latestRuntimeEvidence(spine);
191
+ return {
192
+ ok: true,
193
+ data: {
194
+ subjectType: subject.subjectType,
195
+ conclusion: "Plugin surface is loaded in host-safe mode with a minimal activation spine.",
196
+ keyFactors: [
197
+ "synchronous_register",
198
+ `subject:${subject.subjectId}`,
199
+ runtimeEvidence?.capability ?? "runtime.activate",
200
+ ],
201
+ evidenceRefs: [
202
+ runtimeEvidence?.traceId ?? `${INTERNAL_RUNTIME_TRACE_PREFIX}none`,
203
+ `subject:${subjectRaw.trim()}`,
204
+ "host_safe_mode",
205
+ ],
206
+ nextStep: "use full workspace runtime for evidence-backed explain details",
207
+ },
208
+ };
209
+ }
210
+ function buildHeartbeatCheckPayload(spine, input) {
211
+ const runtimeEvidence = latestRuntimeEvidence(spine);
212
+ const updatedAt = runtimeEvidence?.createdAt ?? new Date(spine.lifecycleState.lastChangedAt).toISOString();
213
+ const timestamp = typeof input?.timestamp === "string" && input.timestamp.trim().length > 0 ? input.timestamp : updatedAt;
214
+ return {
215
+ ok: true,
216
+ status: "heartbeat_ok",
217
+ heartbeat: "HEARTBEAT_OK",
218
+ scope: "rhythm",
219
+ trigger: "heartbeat_bridge",
220
+ reasons: ["host_safe_bridge_ready"],
221
+ nextAction: "continue",
222
+ message: "Host-safe heartbeat bridge acknowledged the round. No additional action is required from this surface.",
223
+ data: {
224
+ runtime: {
225
+ host: "openclaw-plugin",
226
+ serviceStatus: spine.runtimeHandle.ready ? "running" : "idle",
227
+ updatedAt,
228
+ },
229
+ surface: {
230
+ tool: "second_nature_ops",
231
+ command: "second-nature heartbeat_check",
232
+ },
233
+ bridge: {
234
+ timestamp,
235
+ sessionContextProvided: typeof input?.sessionContext === "string" && input.sessionContext.trim().length > 0,
236
+ heartbeatChecklistProvided: typeof input?.heartbeatChecklist === "string" && input.heartbeatChecklist.trim().length > 0,
237
+ serviceEntryMode: "runtime_carrier_only",
238
+ },
239
+ },
240
+ };
241
+ }
242
+ function createHostSafeRouter(spine) {
243
+ const notImplemented = async (command) => ({
244
+ ok: false,
245
+ command,
246
+ message: HOST_SAFE_LIMITATION_MESSAGE,
247
+ });
248
+ const commands = [
249
+ {
250
+ name: "status",
251
+ description: "Show aggregated Second Nature status",
252
+ execute: async () => buildStatusPayload(spine),
253
+ },
254
+ {
255
+ name: "policy",
256
+ description: "Write or inspect policy state",
257
+ execute: async (input) => {
258
+ const action = typeof input?.action === "string" ? input.action : "show";
259
+ if (action === "set") {
260
+ return createUnavailableActionError("HOST_SAFE_POLICY_SET_UNAVAILABLE", "policy set is unavailable in the host-safe plugin package", ["social_daily_limit", "quiet_enabled"], "run_workspace_runtime_or_reinstall_full_build");
261
+ }
262
+ return notImplemented("policy");
263
+ },
264
+ },
265
+ {
266
+ name: "credential",
267
+ description: "Inspect or recover credential state",
268
+ execute: async (input) => {
269
+ const action = typeof input?.action === "string" ? input.action : "show";
270
+ if (action === "verify") {
271
+ return createUnavailableActionError("HOST_SAFE_CREDENTIAL_VERIFY_UNAVAILABLE", "credential verify is unavailable in the host-safe plugin package", ["verification_answer"], "run_workspace_runtime_or_reinstall_full_build");
272
+ }
273
+ const platformId = typeof input?.platformId === "string" ? input.platformId : undefined;
274
+ return buildCredentialPayload(platformId);
275
+ },
276
+ },
277
+ {
278
+ name: "quiet",
279
+ description: "Inspect Quiet lifecycle state",
280
+ execute: async (input) => {
281
+ const scope = typeof input?.scope === "string" ? input.scope : undefined;
282
+ return buildQuietPayload(scope);
283
+ },
284
+ },
285
+ {
286
+ name: "report",
287
+ description: "Show daily report artifacts",
288
+ execute: async (input) => {
289
+ const day = typeof input?.day === "string" ? input.day : undefined;
290
+ return buildReportPayload(day);
291
+ },
292
+ },
293
+ {
294
+ name: "session",
295
+ description: "Inspect continuity session details",
296
+ execute: async (input) => {
297
+ const sessionId = typeof input?.sessionId === "string" ? input.sessionId : undefined;
298
+ return buildSessionPayload(sessionId);
299
+ },
300
+ },
301
+ {
302
+ name: "audit",
303
+ description: "Inspect audit and evidence views",
304
+ execute: async () => notImplemented("audit"),
305
+ },
306
+ {
307
+ name: "explain",
308
+ description: "Answer why-question explain requests",
309
+ execute: async (input) => {
310
+ const subject = typeof input?.subject === "string" ? input.subject : undefined;
311
+ return buildExplainPayload(spine, subject);
312
+ },
313
+ },
314
+ {
315
+ name: "heartbeat_check",
316
+ description: "Acknowledge the shipping heartbeat bridge round",
317
+ execute: async (input) => buildHeartbeatCheckPayload(spine, input),
318
+ },
319
+ ];
24
320
  return {
25
321
  commands,
26
322
  resolve(name) {
@@ -28,51 +324,155 @@ async function resolveCommandRouterSafe() {
28
324
  },
29
325
  };
30
326
  }
31
- async function createRuntimeService() {
32
- try {
33
- const runtimeMod = await import("./runtime/core/second-nature/runtime/service-entry.js");
34
- if (runtimeMod?.startRuntimeService) {
35
- const handle = runtimeMod.startRuntimeService();
327
+ function createActivationSpine() {
328
+ const spine = {
329
+ router: undefined,
330
+ runtimeHandle: startRuntimeService({ workspaceRoot: process.cwd() }),
331
+ lifecycleState: getLifecycleState(),
332
+ serviceStartRecorded: false,
333
+ runtimeEvidence: [],
334
+ };
335
+ spine.router = createHostSafeRouter(spine);
336
+ return spine;
337
+ }
338
+ function ensureActivationSpine() {
339
+ if (activationSpine) {
340
+ return activationSpine;
341
+ }
342
+ activationSpine = createActivationSpine();
343
+ return activationSpine;
344
+ }
345
+ function recordRuntimeEvidence(spine, origin) {
346
+ if (origin === "service_start" && spine.serviceStartRecorded) {
347
+ return;
348
+ }
349
+ if (origin === "service_start") {
350
+ spine.serviceStartRecorded = true;
351
+ }
352
+ spine.runtimeEvidence.push({
353
+ traceId: `${INTERNAL_RUNTIME_TRACE_PREFIX}${origin}-${spine.lifecycleState.registerCount}-${Date.now()}`,
354
+ capability: origin === "register"
355
+ ? spine.lifecycleState.registerCount === 1
356
+ ? "runtime.activate"
357
+ : "runtime.reload"
358
+ : "runtime.heartbeat",
359
+ origin,
360
+ createdAt: new Date().toISOString(),
361
+ status: "succeeded",
362
+ });
363
+ trimRuntimeEvidence(spine);
364
+ }
365
+ function refreshRegistrationState() {
366
+ const spine = ensureActivationSpine();
367
+ spine.runtimeHandle = startRuntimeService({ workspaceRoot: process.cwd() });
368
+ spine.lifecycleState = recordRegistration();
369
+ spine.serviceStartRecorded = false;
370
+ recordRuntimeEvidence(spine, "register");
371
+ return spine;
372
+ }
373
+ function parseCommandInput(rawArgs) {
374
+ const tokens = rawArgs?.trim().split(/\s+/).filter(Boolean) ?? [];
375
+ if (tokens.length === 0) {
376
+ return {
377
+ ok: false,
378
+ result: { ok: false, message: "Missing command argument." },
379
+ };
380
+ }
381
+ const [command, ...rest] = tokens;
382
+ if (command === "policy" && rest[0] === "set") {
383
+ return {
384
+ ok: false,
385
+ result: {
386
+ ok: false,
387
+ command,
388
+ message: "policy set requires structured args; use second_nature_ops instead.",
389
+ },
390
+ };
391
+ }
392
+ if (command === "credential" && rest[0] === "verify") {
393
+ return {
394
+ ok: false,
395
+ result: {
396
+ ok: false,
397
+ command,
398
+ message: "credential verify requires structured args; use second_nature_ops instead.",
399
+ },
400
+ };
401
+ }
402
+ switch (command) {
403
+ case "status":
404
+ case "quiet":
36
405
  return {
37
- id: "second-nature-runtime",
38
- start() {
39
- return { ready: handle.ready, version: handle.version };
40
- },
406
+ ok: true,
407
+ command,
408
+ input: rest.length > 0 ? { scope: rest.join(" ") } : undefined,
409
+ };
410
+ case "report":
411
+ return {
412
+ ok: true,
413
+ command,
414
+ input: rest[0] ? { day: rest[0] } : undefined,
415
+ };
416
+ case "session":
417
+ return {
418
+ ok: true,
419
+ command,
420
+ input: rest[0] ? { sessionId: rest[0] } : undefined,
421
+ };
422
+ case "credential":
423
+ return {
424
+ ok: true,
425
+ command,
426
+ input: rest[0] ? { platformId: rest[0] } : undefined,
427
+ };
428
+ case "heartbeat_check":
429
+ return {
430
+ ok: true,
431
+ command,
432
+ input: rest.length > 0
433
+ ? {
434
+ timestamp: rest[0],
435
+ sessionContext: rest.length > 1 ? rest.slice(1).join(" ") : undefined,
436
+ }
437
+ : undefined,
438
+ };
439
+ case "explain":
440
+ return {
441
+ ok: true,
442
+ command,
443
+ input: rest.length > 0 ? { subject: rest.join(" ") } : undefined,
444
+ };
445
+ default:
446
+ return {
447
+ ok: true,
448
+ command,
449
+ input: undefined,
41
450
  };
42
- }
43
- }
44
- catch {
45
- // fall through to minimal service shell
46
451
  }
452
+ }
453
+ function createRuntimeService() {
47
454
  return {
48
455
  id: "second-nature-runtime",
49
456
  start() {
50
- return { ready: true, version: "0.1.6-minimal" };
457
+ const spine = ensureActivationSpine();
458
+ recordRuntimeEvidence(spine, "service_start");
459
+ return {
460
+ ready: spine.runtimeHandle.ready,
461
+ version: spine.runtimeHandle.version,
462
+ };
51
463
  },
52
464
  };
53
465
  }
54
- async function createLifecycleService() {
55
- try {
56
- const lifecycleMod = await import("./runtime/core/second-nature/runtime/lifecycle-service.js");
57
- if (lifecycleMod?.recordRegistration) {
58
- return {
59
- id: "second-nature-lifecycle",
60
- start() {
61
- const state = lifecycleMod.recordRegistration();
62
- return { phase: state.phase, registerCount: state.registerCount };
63
- },
64
- };
65
- }
66
- }
67
- catch {
68
- // fall through to minimal lifecycle shell
69
- }
70
- let registerCount = 0;
466
+ function createLifecycleService() {
71
467
  return {
72
468
  id: "second-nature-lifecycle",
73
469
  start() {
74
- registerCount += 1;
75
- return { phase: registerCount === 1 ? "loading" : "reloading", registerCount };
470
+ const spine = ensureActivationSpine();
471
+ return {
472
+ phase: spine.lifecycleState.phase,
473
+ registerCount: spine.lifecycleState.registerCount,
474
+ lastChangedAt: spine.lifecycleState.lastChangedAt,
475
+ };
76
476
  },
77
477
  };
78
478
  }
@@ -80,33 +480,30 @@ export default {
80
480
  id: "second-nature",
81
481
  name: "Second Nature",
82
482
  description: "Registers command/tool/service surface with load-reload lifecycle semantics.",
83
- async register(api) {
84
- const router = await resolveCommandRouterSafe();
85
- const runtimeService = await createRuntimeService();
86
- const lifecycleService = await createLifecycleService();
483
+ register(api) {
484
+ const runtimeService = createRuntimeService();
485
+ const lifecycleService = createLifecycleService();
87
486
  api.registerService(runtimeService);
88
487
  api.registerService(lifecycleService);
89
- api.registerCli(({ program }) => {
90
- void program;
91
- }, { commands: ["second-nature"] });
92
488
  api.registerCommand({
93
489
  name: "second-nature",
94
490
  description: "Route Agent-facing operational commands for Second Nature.",
95
491
  acceptsArgs: true,
96
492
  handler: async (ctx) => {
97
- const command = ctx.args?.trim();
98
- if (!command) {
493
+ const spine = ensureActivationSpine();
494
+ const parsed = parseCommandInput(ctx.args);
495
+ if (!parsed.ok) {
99
496
  return {
100
- text: JSON.stringify({ ok: false, message: "Missing command argument." }),
497
+ text: JSON.stringify(parsed.result),
101
498
  };
102
499
  }
103
- const resolved = router.resolve(command);
500
+ const resolved = spine.router.resolve(parsed.command);
104
501
  if (!resolved) {
105
502
  return {
106
- text: JSON.stringify({ ok: false, command, message: "Unknown Second Nature command." }),
503
+ text: JSON.stringify({ ok: false, command: parsed.command, message: "Unknown Second Nature command." }),
107
504
  };
108
505
  }
109
- const result = await resolved.execute();
506
+ const result = await resolved.execute(parsed.input);
110
507
  return {
111
508
  text: JSON.stringify(result),
112
509
  };
@@ -120,12 +517,13 @@ export default {
120
517
  additionalProperties: false,
121
518
  properties: {
122
519
  command: { type: "string" },
123
- args: { type: "object", additionalProperties: true }
520
+ args: { type: "object", additionalProperties: true },
124
521
  },
125
- required: ["command"]
522
+ required: ["command"],
126
523
  },
127
524
  async execute(_id, params) {
128
- const resolved = router.resolve(params.command);
525
+ const spine = ensureActivationSpine();
526
+ const resolved = spine.router.resolve(params.command);
129
527
  if (!resolved) {
130
528
  return {
131
529
  content: [
@@ -1,13 +1,20 @@
1
1
  {
2
2
  "id": "second-nature",
3
3
  "name": "Second Nature",
4
- "version": "0.1.6",
4
+ "version": "0.1.8",
5
5
  "entry": "./index.js",
6
- "description": "OpenClaw native plugin package for continuity, explain, and recovery operations.",
6
+ "description": "OpenClaw native plugin package with synchronous surface registration and a bundled runtime spine.",
7
7
  "capabilities": {
8
- "commands": ["second-nature"],
9
- "tools": ["second_nature_ops"],
10
- "services": ["second-nature-runtime", "second-nature-lifecycle"]
8
+ "commands": [
9
+ "second-nature"
10
+ ],
11
+ "tools": [
12
+ "second_nature_ops"
13
+ ],
14
+ "services": [
15
+ "second-nature-runtime",
16
+ "second-nature-lifecycle"
17
+ ]
11
18
  },
12
19
  "configSchema": {
13
20
  "type": "object",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@haaaiawd/second-nature",
3
- "version": "0.1.6",
4
- "description": "OpenClaw native plugin for long-running agent continuity, Quiet memory curation, and explainable operator flows.",
3
+ "version": "0.1.8",
4
+ "description": "OpenClaw native plugin with synchronous registration, a packaged runtime artifact, and operator-facing status/explain flows.",
5
5
  "keywords": [
6
6
  "openclaw",
7
7
  "plugin",
@@ -1,9 +1,11 @@
1
+ import { desc } from "drizzle-orm";
1
2
  import { createQuietInputLoader } from "../../storage/services/quiet-input-loader.js";
2
3
  import { AssetRepository } from "../../storage/repositories/asset-repository.js";
3
4
  import { CredentialRepository } from "../../storage/repositories/credential-repository.js";
4
5
  import { EvidenceQueryEngine } from "../../observability/query/evidence-query-engine.js";
5
- import { executionAttempts } from "../../observability/db/schema/index.js";
6
- import { desc } from "drizzle-orm";
6
+ import { decisionLedger, executionAttempts } from "../../observability/db/schema/index.js";
7
+ const INTERNAL_RUNTIME_PLATFORM_ID = "second-nature-runtime";
8
+ const INTERNAL_RUNTIME_TRACE_PREFIX = "sn-runtime-";
7
9
  function buildCredentialNextStep(status) {
8
10
  if (status === "pending_verification")
9
11
  return "submit_verification_answer";
@@ -11,6 +13,24 @@ function buildCredentialNextStep(status) {
11
13
  return "refresh_credential_context";
12
14
  return undefined;
13
15
  }
16
+ function mapRuntimeStatus(attempt) {
17
+ if (!attempt) {
18
+ return "unknown";
19
+ }
20
+ if (attempt.failureClass || attempt.status === "failed") {
21
+ return "degraded";
22
+ }
23
+ return "running";
24
+ }
25
+ function mapConnectorStatus(attempt) {
26
+ if (!attempt) {
27
+ return "unknown";
28
+ }
29
+ if (attempt.failureClass || attempt.status === "failed") {
30
+ return "degraded";
31
+ }
32
+ return "healthy";
33
+ }
14
34
  export function createCliReadModels(deps) {
15
35
  const assetRepository = new AssetRepository(deps.stateDb);
16
36
  const credentialRepository = new CredentialRepository(deps.stateDb);
@@ -18,15 +38,28 @@ export function createCliReadModels(deps) {
18
38
  const evidenceQuery = new EvidenceQueryEngine(deps.observabilityDb);
19
39
  return {
20
40
  async loadStatus(_scope) {
21
- let latestAttempt;
41
+ let recentAttempts = [];
42
+ let recentDecisions = [];
22
43
  let credentials = [];
23
44
  try {
24
- latestAttempt = await deps.observabilityDb.db.query.executionAttempts.findFirst({
25
- orderBy: [desc(executionAttempts.startedAt)],
26
- });
45
+ recentAttempts = await deps.observabilityDb.db
46
+ .select()
47
+ .from(executionAttempts)
48
+ .orderBy(desc(executionAttempts.startedAt), desc(executionAttempts.finishedAt))
49
+ .limit(50);
50
+ }
51
+ catch {
52
+ recentAttempts = [];
53
+ }
54
+ try {
55
+ recentDecisions = await deps.observabilityDb.db
56
+ .select()
57
+ .from(decisionLedger)
58
+ .orderBy(desc(decisionLedger.createdAt))
59
+ .limit(50);
27
60
  }
28
61
  catch {
29
- latestAttempt = undefined;
62
+ recentDecisions = [];
30
63
  }
31
64
  try {
32
65
  credentials = await deps.stateDb.db.query.credentialRecords.findMany();
@@ -34,28 +67,40 @@ export function createCliReadModels(deps) {
34
67
  catch {
35
68
  credentials = [];
36
69
  }
37
- const connectorSummary = latestAttempt
38
- ? [{
39
- platformId: latestAttempt.platformId,
40
- status: latestAttempt.failureClass ? "degraded" : "healthy",
41
- channel: latestAttempt.channel,
42
- failureClass: latestAttempt.failureClass ?? undefined,
43
- }]
70
+ const latestRuntimeAttempt = recentAttempts.find((attempt) => attempt.platformId === INTERNAL_RUNTIME_PLATFORM_ID);
71
+ const latestConnectorAttempt = recentAttempts.find((attempt) => attempt.platformId !== INTERNAL_RUNTIME_PLATFORM_ID);
72
+ const latestRuntimeDecision = recentDecisions.find((decision) => decision.traceId.startsWith(INTERNAL_RUNTIME_TRACE_PREFIX));
73
+ const runtimeUpdatedAt = latestRuntimeAttempt?.finishedAt ?? latestRuntimeAttempt?.startedAt ?? latestRuntimeDecision?.createdAt ?? "";
74
+ const quietMode = latestRuntimeDecision?.mode === "quiet" ||
75
+ latestRuntimeDecision?.mode === "maintenance_only" ||
76
+ latestRuntimeDecision?.mode === "paused_for_interrupt"
77
+ ? latestRuntimeDecision.mode
78
+ : "unknown";
79
+ const riskFlags = [latestRuntimeAttempt?.failureClass, latestConnectorAttempt?.failureClass].filter((value) => Boolean(value));
80
+ const connectorSummary = latestConnectorAttempt
81
+ ? [
82
+ {
83
+ platformId: latestConnectorAttempt.platformId,
84
+ status: mapConnectorStatus(latestConnectorAttempt),
85
+ channel: latestConnectorAttempt.channel,
86
+ failureClass: latestConnectorAttempt.failureClass ?? undefined,
87
+ },
88
+ ]
44
89
  : [];
45
90
  return {
46
91
  runtime: {
47
92
  host: "openclaw-plugin",
48
- serviceStatus: latestAttempt ? (latestAttempt.failureClass ? "degraded" : "running") : "unknown",
49
- updatedAt: new Date().toISOString(),
93
+ serviceStatus: mapRuntimeStatus(latestRuntimeAttempt),
94
+ updatedAt: runtimeUpdatedAt,
50
95
  },
51
96
  rhythm: {
52
- mode: "unknown",
97
+ mode: latestRuntimeDecision?.mode ?? "unknown",
53
98
  windowId: undefined,
54
99
  },
55
100
  quiet: {
56
- mode: "unknown",
57
- lastEvent: undefined,
58
- interrupted: undefined,
101
+ mode: quietMode,
102
+ lastEvent: latestRuntimeDecision?.traceId,
103
+ interrupted: latestRuntimeDecision?.mode === "paused_for_interrupt" ? true : undefined,
59
104
  },
60
105
  connectors: connectorSummary,
61
106
  credentials: credentials.map((item) => ({
@@ -64,8 +109,8 @@ export function createCliReadModels(deps) {
64
109
  nextStep: buildCredentialNextStep(item.status),
65
110
  })),
66
111
  risk: {
67
- level: latestAttempt?.failureClass ? "medium" : "low",
68
- flags: latestAttempt?.failureClass ? [latestAttempt.failureClass] : [],
112
+ level: riskFlags.length > 0 ? "medium" : "low",
113
+ flags: riskFlags,
69
114
  },
70
115
  };
71
116
  },
@@ -108,40 +108,23 @@ export function createConnectorPolicyLayer(ctx) {
108
108
  }
109
109
  let lastFailure;
110
110
  for (let attempt = 1; attempt <= retryPolicy.maxRetries; attempt += 1) {
111
- const traceId = makeTraceId(request, plan);
112
- const attemptId = `attempt-${traceId}-${attempt}`;
111
+ const traceId = `${makeTraceId(request, plan)}:${attempt}`;
113
112
  if (ctx.telemetry) {
114
- await ctx.telemetry.recordExecutionAttempt({
115
- id: attemptId,
113
+ await ctx.telemetry.startAttempt({
116
114
  traceId,
117
115
  decisionId: identity.decisionId,
118
116
  intentId: identity.intentId,
119
117
  platformId: request.platformId,
120
118
  capability: request.intent,
121
119
  channel: plan.channel,
122
- status: "started",
123
120
  retryPolicy: JSON.stringify(retryPolicy),
124
121
  idempotencyKey: request.idempotencyKey,
125
- startedAt: new Date().toISOString(),
126
122
  });
127
123
  }
128
124
  const raw = await ctx.executionRunner.run(plan, request);
129
125
  if (raw.success) {
130
126
  if (ctx.telemetry) {
131
- await ctx.telemetry.recordExecutionAttempt({
132
- id: `${attemptId}-done`,
133
- traceId,
134
- decisionId: identity.decisionId,
135
- intentId: identity.intentId,
136
- platformId: request.platformId,
137
- capability: request.intent,
138
- channel: plan.channel,
139
- status: "succeeded",
140
- retryPolicy: JSON.stringify(retryPolicy),
141
- idempotencyKey: request.idempotencyKey,
142
- startedAt: new Date().toISOString(),
143
- finishedAt: new Date().toISOString(),
144
- });
127
+ await ctx.telemetry.completeAttempt(traceId, "succeeded");
145
128
  }
146
129
  return {
147
130
  status: "success",
@@ -161,21 +144,7 @@ export function createConnectorPolicyLayer(ctx) {
161
144
  channel: raw.channel,
162
145
  };
163
146
  if (ctx.telemetry) {
164
- await ctx.telemetry.recordExecutionAttempt({
165
- id: `${attemptId}-failed`,
166
- traceId,
167
- decisionId: identity.decisionId,
168
- intentId: identity.intentId,
169
- platformId: request.platformId,
170
- capability: request.intent,
171
- channel: plan.channel,
172
- status: "failed",
173
- failureClass: classified.class,
174
- retryPolicy: JSON.stringify(retryPolicy),
175
- idempotencyKey: request.idempotencyKey,
176
- startedAt: new Date().toISOString(),
177
- finishedAt: new Date().toISOString(),
178
- });
147
+ await ctx.telemetry.completeAttempt(traceId, "failed", undefined, classified.class);
179
148
  }
180
149
  if (ctx.cooldownPort) {
181
150
  await ctx.cooldownPort.markFailure(request.platformId, intent, classified.class, classified.retryAfterMs);
@@ -6,6 +6,73 @@ import { fileURLToPath } from "node:url";
6
6
  import * as schema from "./schema/index.js";
7
7
  // Pre-initialize sql.js WASM at module load time
8
8
  const SQL = await initSqlJs();
9
+ const OBSERVABILITY_SCHEMA_SQL = `
10
+ CREATE TABLE IF NOT EXISTS decision_ledger (
11
+ id TEXT PRIMARY KEY,
12
+ tick_id TEXT NOT NULL,
13
+ trace_id TEXT NOT NULL,
14
+ intent_id TEXT,
15
+ platform_id TEXT,
16
+ verdict TEXT NOT NULL,
17
+ mode TEXT NOT NULL,
18
+ reasons TEXT NOT NULL,
19
+ reason_codes TEXT NOT NULL,
20
+ decision_basis TEXT NOT NULL,
21
+ evidence_refs TEXT NOT NULL,
22
+ model_eval_ref TEXT,
23
+ created_at TEXT NOT NULL
24
+ );
25
+ CREATE UNIQUE INDEX IF NOT EXISTS decision_trace_idx ON decision_ledger(trace_id);
26
+ CREATE INDEX IF NOT EXISTS decision_tick_idx ON decision_ledger(tick_id);
27
+ CREATE TABLE IF NOT EXISTS execution_attempts (
28
+ id TEXT PRIMARY KEY,
29
+ trace_id TEXT NOT NULL,
30
+ decision_id TEXT NOT NULL,
31
+ intent_id TEXT NOT NULL,
32
+ platform_id TEXT NOT NULL,
33
+ capability TEXT NOT NULL,
34
+ channel TEXT NOT NULL,
35
+ status TEXT NOT NULL,
36
+ commit_state TEXT,
37
+ failure_class TEXT,
38
+ retry_policy TEXT,
39
+ idempotency_key TEXT,
40
+ started_at TEXT,
41
+ finished_at TEXT
42
+ );
43
+ CREATE UNIQUE INDEX IF NOT EXISTS attempt_trace_idx ON execution_attempts(trace_id);
44
+ CREATE INDEX IF NOT EXISTS attempt_decision_idx ON execution_attempts(decision_id);
45
+ CREATE INDEX IF NOT EXISTS attempt_platform_idx ON execution_attempts(platform_id);
46
+ CREATE TABLE IF NOT EXISTS governance_audit (
47
+ id TEXT PRIMARY KEY,
48
+ event_type TEXT NOT NULL,
49
+ proposal_id TEXT,
50
+ target_asset_id TEXT,
51
+ asset_path TEXT,
52
+ status_from TEXT,
53
+ status_to TEXT NOT NULL,
54
+ before_hash TEXT,
55
+ after_hash TEXT,
56
+ supporting_sources TEXT,
57
+ reason TEXT,
58
+ verification_deadline TEXT,
59
+ attempts_remaining INTEGER,
60
+ created_at TEXT NOT NULL
61
+ );
62
+ CREATE INDEX IF NOT EXISTS audit_proposal_idx ON governance_audit(proposal_id);
63
+ CREATE INDEX IF NOT EXISTS audit_asset_idx ON governance_audit(target_asset_id);
64
+ CREATE INDEX IF NOT EXISTS audit_event_idx ON governance_audit(event_type);
65
+ CREATE TABLE IF NOT EXISTS redaction_manifest (
66
+ id TEXT PRIMARY KEY,
67
+ event_id TEXT NOT NULL,
68
+ event_type TEXT NOT NULL,
69
+ field_name TEXT NOT NULL,
70
+ action TEXT NOT NULL,
71
+ original_value_hash TEXT,
72
+ created_at TEXT NOT NULL
73
+ );
74
+ CREATE INDEX IF NOT EXISTS redact_event_idx ON redaction_manifest(event_id);
75
+ `;
9
76
  function resolveDbPath(filename) {
10
77
  if (path.isAbsolute(filename) || filename === ":memory:") {
11
78
  return filename;
@@ -18,6 +85,9 @@ function resolveDbPath(filename) {
18
85
  }
19
86
  return path.join(dataDir, filename);
20
87
  }
88
+ function bootstrapObservabilitySchema(sqlite) {
89
+ sqlite.exec(OBSERVABILITY_SCHEMA_SQL);
90
+ }
21
91
  export function createObservabilityDatabase(filename = "observability.db") {
22
92
  const dbPath = resolveDbPath(filename);
23
93
  const isMemory = filename === ":memory:";
@@ -26,6 +96,7 @@ export function createObservabilityDatabase(filename = "observability.db") {
26
96
  dbBuffer = fs.readFileSync(dbPath);
27
97
  }
28
98
  const sqlite = new SQL.Database(dbBuffer);
99
+ bootstrapObservabilitySchema(sqlite);
29
100
  const db = drizzle(sqlite, { schema });
30
101
  return {
31
102
  sqlite,
@@ -7,7 +7,6 @@ export interface ExecutionAttemptInput {
7
7
  platformId: string;
8
8
  capability: string;
9
9
  channel: string;
10
- status: ExecutionAttempt["status"];
11
10
  commitState?: IntentCommitState;
12
11
  failureClass?: string;
13
12
  retryPolicy?: string;
@@ -6,6 +6,65 @@ import { fileURLToPath } from "node:url";
6
6
  import * as schema from "./schema/index.js";
7
7
  // Pre-initialize sql.js WASM at module load time
8
8
  const SQL = await initSqlJs();
9
+ const STATE_SCHEMA_SQL = `
10
+ CREATE TABLE IF NOT EXISTS credential_records (
11
+ platform_id TEXT PRIMARY KEY,
12
+ credential_type TEXT NOT NULL,
13
+ encrypted_value TEXT NOT NULL,
14
+ status TEXT NOT NULL,
15
+ verification_code TEXT,
16
+ challenge_text TEXT,
17
+ expires_at TEXT,
18
+ attempts_remaining INTEGER,
19
+ updated_at TEXT NOT NULL
20
+ );
21
+ CREATE TABLE IF NOT EXISTS policy_records (
22
+ platform_id TEXT PRIMARY KEY,
23
+ social_daily_limit INTEGER NOT NULL,
24
+ quiet_enabled INTEGER NOT NULL,
25
+ updated_at TEXT NOT NULL
26
+ );
27
+ CREATE TABLE IF NOT EXISTS asset_registry (
28
+ id TEXT PRIMARY KEY,
29
+ kind TEXT NOT NULL,
30
+ path TEXT NOT NULL,
31
+ hash TEXT NOT NULL,
32
+ version INTEGER NOT NULL DEFAULT 1,
33
+ layer TEXT NOT NULL,
34
+ last_indexed_at TEXT NOT NULL
35
+ );
36
+ CREATE UNIQUE INDEX IF NOT EXISTS asset_registry_path_idx ON asset_registry(path);
37
+ CREATE TABLE IF NOT EXISTS intent_commit_records (
38
+ id TEXT PRIMARY KEY,
39
+ intent_id TEXT NOT NULL,
40
+ decision_id TEXT NOT NULL,
41
+ checkpoint_id TEXT,
42
+ state TEXT NOT NULL,
43
+ outcome_ref TEXT,
44
+ metadata_json TEXT,
45
+ updated_at TEXT NOT NULL
46
+ );
47
+ CREATE TABLE IF NOT EXISTS proposal_records (
48
+ id TEXT PRIMARY KEY,
49
+ target_asset_id TEXT NOT NULL,
50
+ before_hash TEXT,
51
+ after_hash TEXT,
52
+ status TEXT NOT NULL,
53
+ proposed_diff TEXT NOT NULL,
54
+ reason TEXT NOT NULL,
55
+ supporting_sources TEXT NOT NULL,
56
+ confidence REAL NOT NULL,
57
+ created_at TEXT NOT NULL,
58
+ applied_at TEXT
59
+ );
60
+ CREATE TABLE IF NOT EXISTS provenance_edges (
61
+ id TEXT PRIMARY KEY,
62
+ from_id TEXT NOT NULL,
63
+ to_id TEXT NOT NULL,
64
+ kind TEXT NOT NULL,
65
+ created_at TEXT NOT NULL
66
+ );
67
+ `;
9
68
  function resolveDbPath(filename) {
10
69
  if (path.isAbsolute(filename) || filename === ":memory:") {
11
70
  return filename;
@@ -18,6 +77,9 @@ function resolveDbPath(filename) {
18
77
  }
19
78
  return path.join(dataDir, filename);
20
79
  }
80
+ function bootstrapStateSchema(sqlite) {
81
+ sqlite.exec(STATE_SCHEMA_SQL);
82
+ }
21
83
  export function createStateDatabase(filename = "state.db") {
22
84
  const dbPath = resolveDbPath(filename);
23
85
  const isMemory = filename === ":memory:";
@@ -26,6 +88,7 @@ export function createStateDatabase(filename = "state.db") {
26
88
  dbBuffer = fs.readFileSync(dbPath);
27
89
  }
28
90
  const sqlite = new SQL.Database(dbBuffer);
91
+ bootstrapStateSchema(sqlite);
29
92
  const db = drizzle(sqlite, { schema });
30
93
  return {
31
94
  sqlite,
@@ -1,11 +0,0 @@
1
- import type { StateAPI } from "../storage/state-api.js";
2
- export interface PolicyWriteInput {
3
- platformId: string;
4
- socialDailyLimit: number;
5
- quietEnabled: boolean;
6
- }
7
- export interface ActionBridge {
8
- savePolicy(input: PolicyWriteInput): Promise<void>;
9
- verifyCredential(platformId: string, answer: string): Promise<void>;
10
- }
11
- export declare function createActionBridge(stateApi: StateAPI): ActionBridge;
@@ -1,25 +0,0 @@
1
- import { type ObservabilityDatabase } from "../observability/db/index.js";
2
- import { type StateDatabase, type StateAPI } from "../storage/index.js";
3
- import { type ActionBridge } from "./action-bridge.js";
4
- import { type CliCommandDefinition } from "./commands/index.js";
5
- import { type CliReadModels } from "./read-models/index.js";
6
- export interface CommandRouter {
7
- commands: CliCommandDefinition[];
8
- resolve(name: string): CliCommandDefinition | undefined;
9
- }
10
- export interface CommandRouterDeps {
11
- commands: CliCommandDefinition[];
12
- }
13
- export interface CliRuntimeDeps {
14
- stateDb: StateDatabase;
15
- observabilityDb: ObservabilityDatabase;
16
- stateApi: StateAPI;
17
- readModels: CliReadModels;
18
- actionBridge: ActionBridge;
19
- }
20
- export interface CreateCommandRouterOptions {
21
- deps?: Partial<CliRuntimeDeps>;
22
- }
23
- export declare function createCliRuntimeDeps(overrides?: Partial<CliRuntimeDeps>): CliRuntimeDeps;
24
- export declare function createCommandRouter(options?: CreateCommandRouterOptions): CommandRouter;
25
- export declare function closeCliRuntimeDeps(deps: Pick<CliRuntimeDeps, "stateDb" | "observabilityDb">): void;