@haaaiawd/second-nature 0.2.8 → 0.2.9

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
@@ -91,6 +91,7 @@ const WORKSPACE_BRIDGE_COMMANDS = new Set([
91
91
  "session",
92
92
  "explain",
93
93
  "heartbeat_check",
94
+ "heartbeat_run",
94
95
  "fallback",
95
96
  "storage_smoke",
96
97
  // T1.2.8 (SN-CODE-03): capability probe surface via workspace bridge
@@ -146,8 +147,10 @@ async function ensureWorkspaceOpsBridge(spine) {
146
147
  return { ok: true, dispatch: opened.dispatch };
147
148
  }
148
149
  async function routeSecondNatureCommand(spine, command, input) {
150
+ // T-ROS.C.1-followup: heartbeat_run is an alias for heartbeat_check on the plugin surface.
151
+ const normalizedCommand = command === "heartbeat_run" ? "heartbeat_check" : command;
149
152
  const wr = spine.workspaceRootContext;
150
- const useBridge = wr.resolution !== "unknown" && isWorkspaceBridgeCommand(command, input);
153
+ const useBridge = wr.resolution !== "unknown" && isWorkspaceBridgeCommand(normalizedCommand, input);
151
154
  if (useBridge) {
152
155
  const bridge = await ensureWorkspaceOpsBridge(spine);
153
156
  if (!bridge.ok) {
@@ -164,10 +167,10 @@ async function routeSecondNatureCommand(spine, command, input) {
164
167
  },
165
168
  };
166
169
  }
167
- const payload = (await bridge.dispatch(command, input));
170
+ const payload = (await bridge.dispatch(normalizedCommand, input));
168
171
  return withSetupNudge(spine, command, payload);
169
172
  }
170
- const def = spine.router.resolve(command);
173
+ const def = spine.router.resolve(normalizedCommand);
171
174
  if (!def) {
172
175
  return { ok: false, message: `Unknown Second Nature command: ${command}` };
173
176
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "id": "second-nature",
3
3
  "name": "Second Nature",
4
- "version": "0.2.8",
5
- "description": "OpenClaw native plugin with synchronous surface registration and bundled runtime spine. Set SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot to the same path as the agent workspace. Agent inner guide is packaged as agent-inner-guide.md. Ops surface: loop_status, self_health, tool_affordance, heartbeat_digest, snapshot:capture, narrative:diff, timeline, restore, runtime_secret_bootstrap, connector:run, guidance_payload.",
4
+ "version": "0.2.9",
5
+ "description": "OpenClaw native plugin with synchronous surface registration and bundled runtime spine. Set SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot to the same path as the agent workspace. Agent inner guide is packaged as agent-inner-guide.md. Ops surface: loop_status, self_health, tool_affordance, heartbeat_check, heartbeat_run, heartbeat_digest, snapshot:capture, narrative:diff, timeline, restore, runtime_secret_bootstrap, connector:run, guidance_payload.",
6
6
  "activation": {
7
7
  "onStartup": true,
8
8
  "onCapabilities": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haaaiawd/second-nature",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "OpenClaw native plugin with synchronous registration, a packaged runtime artifact, and operator-facing status/explain flows.",
5
5
  "keywords": [
6
6
  "openclaw",
@@ -29,6 +29,7 @@ import type { CapabilityContractRegistryV7 } from "../../connectors/base/manifes
29
29
  import type { AppendOnlyAuditStore } from "../../observability/audit/append-only-audit-store.js";
30
30
  import { type HeartbeatCheckInput, type HeartbeatSurfaceResult } from "./heartbeat-surface.js";
31
31
  import type { RuntimeOpsEnvelope } from "./ops-router.js";
32
+ import type { StateDatabase } from "../../storage/db/index.js";
32
33
  export interface ManualTriggerContext {
33
34
  triggerSource: "manual_run";
34
35
  affectsHeartbeatCadence: false;
@@ -47,6 +48,11 @@ export interface ConnectorRunResult {
47
48
  experienceId: string;
48
49
  triggerSource: "manual_run";
49
50
  affectsHeartbeatCadence: false;
51
+ evidence?: {
52
+ v7EvidenceId?: string;
53
+ v8EvidenceIds: string[];
54
+ emptyReason?: string;
55
+ };
50
56
  }
51
57
  export interface WetProbeRunInput {
52
58
  platformId: string;
@@ -77,5 +83,9 @@ export interface ManualRunDispatcherDeps {
77
83
  wetProbeRunner: WetProbeRunner;
78
84
  registryV7: CapabilityContractRegistryV7;
79
85
  auditStore?: AppendOnlyAuditStore;
86
+ /** Workspace state database for evidence persistence (v7 life_evidence + v8 EvidenceItem). */
87
+ state?: StateDatabase;
88
+ /** Workspace root required for v7 life evidence JSON artifacts. */
89
+ workspaceRoot?: string;
80
90
  }
81
91
  export declare function createManualRunDispatcher(deps: ManualRunDispatcherDeps): ManualRunDispatcher;
@@ -25,6 +25,9 @@
25
25
  import * as crypto from "node:crypto";
26
26
  import { recordConnectorAttemptAudit } from "../../observability/services/audit-closure-recorders.js";
27
27
  import { heartbeatCheck, } from "./heartbeat-surface.js";
28
+ import { appendLifeEvidence } from "../../storage/life-evidence/append-life-evidence.js";
29
+ import { mapLifeEvidence } from "../../connectors/base/map-life-evidence.js";
30
+ import { normalizeConnectorEvidence } from "../../connectors/evidence-normalizer.js";
28
31
  function buildManualContext(input) {
29
32
  return {
30
33
  triggerSource: "manual_run",
@@ -40,6 +43,7 @@ export function createManualRunDispatcher(deps) {
40
43
  const decisionId = `manual:${crypto.randomUUID()}`;
41
44
  const intentId = `manual-run:${input.platformId}:${input.capabilityId}`;
42
45
  const idempotencyKey = `idem:manual:${crypto.randomUUID()}`;
46
+ const now = new Date().toISOString();
43
47
  const connectorResult = await deps.connectorExecutor.executeEffect({
44
48
  platformId: input.platformId,
45
49
  intent: input.capabilityId,
@@ -64,11 +68,56 @@ export function createManualRunDispatcher(deps) {
64
68
  decisionId,
65
69
  intentId,
66
70
  });
71
+ const evidenceSummary = {
72
+ v8EvidenceIds: [],
73
+ };
74
+ if (connectorResult.status === "success" && deps.state) {
75
+ const capabilityIntent = input.capabilityId;
76
+ // v7 life evidence double-write (parity with heartbeat loop)
77
+ try {
78
+ if (deps.workspaceRoot) {
79
+ const lifeCandidate = mapLifeEvidence({
80
+ platformId: input.platformId,
81
+ intent: capabilityIntent,
82
+ result: connectorResult,
83
+ observedAt: now,
84
+ });
85
+ if (lifeCandidate) {
86
+ const lifeAck = await appendLifeEvidence(deps.state, deps.workspaceRoot, lifeCandidate);
87
+ evidenceSummary.v7EvidenceId = lifeAck.evidenceId;
88
+ }
89
+ }
90
+ }
91
+ catch (v7Err) {
92
+ const msg = v7Err instanceof Error ? v7Err.message : String(v7Err);
93
+ console.warn(`[connector:run] v7 life evidence append failed for ${input.platformId}: ${msg}`);
94
+ }
95
+ // v8 EvidenceItem content-bearing write
96
+ try {
97
+ const v8Result = await normalizeConnectorEvidence(deps.state, {
98
+ status: "success",
99
+ platformId: input.platformId,
100
+ capabilityId: input.capabilityId,
101
+ data: connectorResult.data,
102
+ observedAt: now,
103
+ });
104
+ evidenceSummary.v8EvidenceIds = v8Result.evidenceIds;
105
+ evidenceSummary.emptyReason = v8Result.emptyReason;
106
+ if (v8Result.degraded) {
107
+ console.warn(`[connector:run] v8 evidence normalization degraded for ${input.platformId}: ${v8Result.degraded.reason}`);
108
+ }
109
+ }
110
+ catch (v8Err) {
111
+ const msg = v8Err instanceof Error ? v8Err.message : String(v8Err);
112
+ console.warn(`[connector:run] v8 evidence normalization failed for ${input.platformId}: ${msg}`);
113
+ }
114
+ }
67
115
  const runResult = {
68
116
  connectorResult,
69
117
  experienceId,
70
118
  triggerSource: ctx.triggerSource,
71
119
  affectsHeartbeatCadence: ctx.affectsHeartbeatCadence,
120
+ evidence: evidenceSummary,
72
121
  };
73
122
  return {
74
123
  ok: connectorResult.status === "success",
@@ -785,6 +785,8 @@ export function createOpsRouter(deps) {
785
785
  wetProbeRunner,
786
786
  registryV7,
787
787
  auditStore: deps.auditStore,
788
+ state: deps.state,
789
+ workspaceRoot: typeof input?.workspaceRoot === "string" ? input.workspaceRoot : process.cwd(),
788
790
  });
789
791
  return dispatcher.runConnector({
790
792
  platformId,
@@ -185,12 +185,15 @@ function extractMetrics(item) {
185
185
  // ───────────────────────────────────────────────────────────────
186
186
  // Item extraction from connector payload
187
187
  // ───────────────────────────────────────────────────────────────
188
- function extractItems(data) {
188
+ function extractItems(data, depth = 4) {
189
+ if (depth <= 0)
190
+ return [];
189
191
  if (Array.isArray(data))
190
192
  return data;
191
193
  if (!isRecord(data))
192
194
  return [];
193
- for (const key of ["items", "data", "results", "posts", "nodes", "agents", "edges", "entries", "feed"]) {
195
+ const ARRAY_KEYS = ["items", "data", "results", "posts", "nodes", "agents", "edges", "entries", "feed"];
196
+ for (const key of ARRAY_KEYS) {
194
197
  const candidate = data[key];
195
198
  if (Array.isArray(candidate))
196
199
  return candidate;
@@ -198,6 +201,16 @@ function extractItems(data) {
198
201
  // If the payload itself looks like a single item, treat it as one-item array.
199
202
  if ("id" in data || "title" in data || "content" in data)
200
203
  return [data];
204
+ // Recurse into nested record-valued fields (e.g. real connector runners wrap
205
+ // the platform response in { capability, channel, data: apiResponse }).
206
+ for (const key of ARRAY_KEYS) {
207
+ const candidate = data[key];
208
+ if (isRecord(candidate)) {
209
+ const nested = extractItems(candidate, depth - 1);
210
+ if (nested.length > 0)
211
+ return nested;
212
+ }
213
+ }
201
214
  return [];
202
215
  }
203
216
  function normalizeSingleItem(item, options) {
@@ -187,6 +187,10 @@ function createMoltbookMockRunner(workspaceRoot) {
187
187
  source: "mock",
188
188
  items: Array.isArray(data.items) ? data.items : [],
189
189
  },
190
+ // Duplicate items at payload top-level so v8 evidence normalizer
191
+ // can extract content-bearing evidence without re-implementing
192
+ // the legacy v7 nested shape.
193
+ items: Array.isArray(data.items) ? data.items : [],
190
194
  },
191
195
  };
192
196
  }
@@ -31,6 +31,7 @@ export interface DreamMemoryCandidate {
31
31
  confidence: number;
32
32
  validationStatus: "valid" | "rejected" | "blocked";
33
33
  validationReason?: string;
34
+ acceptedProjectionId?: string;
34
35
  }
35
36
  export interface RunDreamConsolidationResult {
36
37
  runId: string;
@@ -20,7 +20,8 @@
20
20
  *
21
21
  * Test coverage: tests/unit/dream/dream-consolidation-runner.test.ts
22
22
  */
23
- import { readDreamConsolidationRunById, readQuietDailyReviewById, writeLongTermMemoryProjection, } from "../../../storage/v8-state-stores.js";
23
+ import { readDreamConsolidationRunById, readQuietDailyReviewById, } from "../../../storage/v8-state-stores.js";
24
+ import { acceptMemoryProjection } from "./memory-projection-lifecycle.js";
24
25
  // ───────────────────────────────────────────────────────────────
25
26
  // Helpers
26
27
  // ───────────────────────────────────────────────────────────────
@@ -144,30 +145,21 @@ export async function runDreamConsolidation(db, runId, options) {
144
145
  reason: "dream_blocked_redaction",
145
146
  };
146
147
  }
147
- // Write valid candidates as projections (candidate status)
148
+ // Accept valid candidates as active long-term memory projections.
149
+ // This completes the Dream→memory lifecycle so accepted projections can be
150
+ // loaded by EmbodiedContext in subsequent heartbeats (T-DQ.R.3 followup).
148
151
  const validCandidates = candidates.filter((c) => c.validationStatus === "valid");
149
152
  for (const candidate of validCandidates) {
150
- const projectionResult = await writeLongTermMemoryProjection(db, {
151
- id: `proj_${candidate.id}`,
152
- createdAt: now,
153
- candidateId: candidate.id,
154
- topicKey: `topic_${review.day}`,
155
- status: "candidate",
156
- sourceRefs: candidate.sourceRefs,
157
- redactionClass: "none",
158
- lifecycleStatus: "candidate",
159
- payloadJson: JSON.stringify({
160
- candidateText: candidate.candidateText,
161
- confidence: candidate.confidence,
162
- runId,
163
- }),
164
- });
165
- if ("reason" in projectionResult) {
153
+ const acceptResult = await acceptMemoryProjection(db, candidate.id, `topic_${review.day}`, candidate.candidateText, candidate.sourceRefs, { now });
154
+ if ("projectionId" in acceptResult) {
155
+ candidate.acceptedProjectionId = acceptResult.projectionId;
156
+ }
157
+ else {
166
158
  return {
167
159
  runId,
168
160
  status: "failed",
169
161
  candidates,
170
- reason: projectionResult.reason,
162
+ reason: acceptResult.reason,
171
163
  };
172
164
  }
173
165
  }
@@ -99,7 +99,35 @@ export async function openWorkspaceOpsBridge(workspaceRoot) {
99
99
  const prevCwd = process.cwd();
100
100
  try {
101
101
  process.chdir(resolvedRoot);
102
- return await def.execute(input);
102
+ const result = await def.execute(input);
103
+ // T-ROS.C.3-followup: ensure sql.js in-memory DB is persisted after each
104
+ // mutating tool call so subsequent calls (possibly in a new process) see
105
+ // the latest state. Flush failures are reported as warnings, never fatal.
106
+ try {
107
+ if (typeof stateDb.flush === "function") {
108
+ stateDb.flush();
109
+ }
110
+ }
111
+ catch (flushErr) {
112
+ const warning = flushErr instanceof Error ? flushErr.message : String(flushErr);
113
+ result.warnings = [
114
+ ...(result.warnings ?? []),
115
+ `state_flush_warning:${warning}`,
116
+ ];
117
+ }
118
+ try {
119
+ if (typeof observabilityDb.flush === "function") {
120
+ observabilityDb.flush();
121
+ }
122
+ }
123
+ catch (flushErr) {
124
+ const warning = flushErr instanceof Error ? flushErr.message : String(flushErr);
125
+ result.warnings = [
126
+ ...(result.warnings ?? []),
127
+ `observability_flush_warning:${warning}`,
128
+ ];
129
+ }
130
+ return result;
103
131
  }
104
132
  finally {
105
133
  process.chdir(prevCwd);