@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 +6 -3
- package/openclaw.plugin.json +2 -2
- package/package.json +1 -1
- package/runtime/cli/ops/manual-run-dispatcher.d.ts +10 -0
- package/runtime/cli/ops/manual-run-dispatcher.js +49 -0
- package/runtime/cli/ops/ops-router.js +2 -0
- package/runtime/connectors/base/normalized-evidence-content.js +15 -2
- package/runtime/connectors/services/connector-executor-adapter.js +4 -0
- package/runtime/core/second-nature/quiet-dream/dream-consolidation-runner.d.ts +1 -0
- package/runtime/core/second-nature/quiet-dream/dream-consolidation-runner.js +11 -19
- package/workspace-ops-bridge.js +29 -1
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(
|
|
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(
|
|
170
|
+
const payload = (await bridge.dispatch(normalizedCommand, input));
|
|
168
171
|
return withSetupNudge(spine, command, payload);
|
|
169
172
|
}
|
|
170
|
-
const def = spine.router.resolve(
|
|
173
|
+
const def = spine.router.resolve(normalizedCommand);
|
|
171
174
|
if (!def) {
|
|
172
175
|
return { ok: false, message: `Unknown Second Nature command: ${command}` };
|
|
173
176
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "second-nature",
|
|
3
3
|
"name": "Second Nature",
|
|
4
|
-
"version": "0.2.
|
|
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
|
@@ -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
|
-
|
|
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
|
}
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
*
|
|
21
21
|
* Test coverage: tests/unit/dream/dream-consolidation-runner.test.ts
|
|
22
22
|
*/
|
|
23
|
-
import { readDreamConsolidationRunById, readQuietDailyReviewById,
|
|
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
|
-
//
|
|
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
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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:
|
|
162
|
+
reason: acceptResult.reason,
|
|
171
163
|
};
|
|
172
164
|
}
|
|
173
165
|
}
|
package/workspace-ops-bridge.js
CHANGED
|
@@ -99,7 +99,35 @@ export async function openWorkspaceOpsBridge(workspaceRoot) {
|
|
|
99
99
|
const prevCwd = process.cwd();
|
|
100
100
|
try {
|
|
101
101
|
process.chdir(resolvedRoot);
|
|
102
|
-
|
|
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);
|