@haaaiawd/second-nature 0.2.7 → 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
@@ -108,6 +109,8 @@ const WORKSPACE_BRIDGE_COMMANDS = new Set([
108
109
  "connector_test",
109
110
  "connector_behavior_add",
110
111
  "cycle:recent",
112
+ // v8 ops surface (T-ROS.C.1): causal loop health must be reachable from Claw.
113
+ "loop_status",
111
114
  // v7 ops surface (T-ROS.C.1 / T-ROS.C.2 / T-ROS.C.3 / T-V7C.C.5): self_health, tool_affordance,
112
115
  // heartbeat_digest, snapshot:capture, narrative:diff, timeline, restore, runtime_secret_bootstrap,
113
116
  // connector:run, guidance_payload
@@ -144,8 +147,10 @@ async function ensureWorkspaceOpsBridge(spine) {
144
147
  return { ok: true, dispatch: opened.dispatch };
145
148
  }
146
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;
147
152
  const wr = spine.workspaceRootContext;
148
- const useBridge = wr.resolution !== "unknown" && isWorkspaceBridgeCommand(command, input);
153
+ const useBridge = wr.resolution !== "unknown" && isWorkspaceBridgeCommand(normalizedCommand, input);
149
154
  if (useBridge) {
150
155
  const bridge = await ensureWorkspaceOpsBridge(spine);
151
156
  if (!bridge.ok) {
@@ -162,10 +167,10 @@ async function routeSecondNatureCommand(spine, command, input) {
162
167
  },
163
168
  };
164
169
  }
165
- const payload = (await bridge.dispatch(command, input));
170
+ const payload = (await bridge.dispatch(normalizedCommand, input));
166
171
  return withSetupNudge(spine, command, payload);
167
172
  }
168
- const def = spine.router.resolve(command);
173
+ const def = spine.router.resolve(normalizedCommand);
169
174
  if (!def) {
170
175
  return { ok: false, message: `Unknown Second Nature command: ${command}` };
171
176
  }
@@ -877,6 +882,11 @@ function createHostSafeRouter(spine) {
877
882
  description: "Show recent cycle summary (workspace runtime required)",
878
883
  execute: async () => createUnavailableActionError("HOST_SAFE_CYCLE_RECENT_UNAVAILABLE", "Cycle recent read requires workspace observability database; host-safe plugin does not load persisted audit events.", [], "run_workspace_second_nature_cli_or_full_runtime_package"),
879
884
  },
885
+ {
886
+ name: "loop_status",
887
+ description: "Show v8 causal loop health (workspace runtime required)",
888
+ execute: async () => createUnavailableActionError("HOST_SAFE_LOOP_STATUS_UNAVAILABLE", "loop_status requires workspace state database; provide workspaceRoot so the full workspace bridge can read persisted v8 loop state.", ["workspaceRoot"], "reinvoke_with_workspaceRoot_or_set_SECOND_NATURE_WORKSPACE_ROOT"),
889
+ },
880
890
  ];
881
891
  return {
882
892
  commands,
@@ -1088,6 +1098,8 @@ function parseCommandInput(rawArgs) {
1088
1098
  command,
1089
1099
  input: rest[0] ? { limit: Number(rest[0]) } : undefined,
1090
1100
  };
1101
+ case "loop_status":
1102
+ return { ok: true, command, input: undefined };
1091
1103
  // v7 ops surface (T-ROS.C.2)
1092
1104
  case "self_health":
1093
1105
  return { ok: true, command, input: undefined };
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "id": "second-nature",
3
3
  "name": "Second Nature",
4
- "version": "0.2.7",
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. v7 ops surface: 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.7",
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",
@@ -112,6 +112,7 @@ export async function heartbeatCheck(input) {
112
112
  else {
113
113
  const spine = v8Result;
114
114
  surfaceResult.v8Spine = spine;
115
+ surfaceResult.livedExperienceLoopClaimed = Boolean(spine.cycleId && (spine.closureRef || spine.noActionReason));
115
116
  surfaceResult.reasons = [
116
117
  ...surfaceResult.reasons,
117
118
  `v8_spine_cycle:${spine.cycleId}`,
@@ -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);