@haaaiawd/second-nature 0.1.7 → 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,27 +1,322 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
+
*/
|
|
5
23
|
import { startRuntimeService, } from "./runtime/core/second-nature/runtime/service-entry.js";
|
|
6
24
|
import { getLifecycleState, recordRegistration, } from "./runtime/core/second-nature/runtime/lifecycle-service.js";
|
|
7
|
-
const INTERNAL_RUNTIME_PLATFORM_ID = "second-nature-runtime";
|
|
8
25
|
const INTERNAL_RUNTIME_TRACE_PREFIX = "sn-runtime-";
|
|
9
|
-
const
|
|
26
|
+
const HOST_SAFE_LIMITATION_MESSAGE = "Host-safe plugin package keeps synchronous register/load semantics, but mutating workspace runtime flows remain unavailable here.";
|
|
10
27
|
let activationSpine = null;
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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 {
|
|
17
132
|
ok: false,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
+
};
|
|
22
153
|
}
|
|
23
|
-
function
|
|
24
|
-
|
|
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
|
+
};
|
|
163
|
+
}
|
|
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;
|
|
177
|
+
try {
|
|
178
|
+
subject = parseExplainSubject(subjectRaw);
|
|
179
|
+
}
|
|
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");
|
|
189
|
+
}
|
|
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
|
+
];
|
|
25
320
|
return {
|
|
26
321
|
commands,
|
|
27
322
|
resolve(name) {
|
|
@@ -29,33 +324,43 @@ function createFallbackRouter() {
|
|
|
29
324
|
},
|
|
30
325
|
};
|
|
31
326
|
}
|
|
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
|
+
}
|
|
32
338
|
function ensureActivationSpine() {
|
|
33
339
|
if (activationSpine) {
|
|
34
340
|
return activationSpine;
|
|
35
341
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
decisionLedger: new DecisionLedger(runtimeDeps.observabilityDb),
|
|
43
|
-
executionTelemetry: new ExecutionTelemetry(runtimeDeps.observabilityDb),
|
|
44
|
-
runtimeHandle: startRuntimeService({ workspaceRoot: process.cwd() }),
|
|
45
|
-
lifecycleState: getLifecycleState(),
|
|
46
|
-
serviceStartRecorded: false,
|
|
47
|
-
};
|
|
48
|
-
return activationSpine;
|
|
342
|
+
activationSpine = createActivationSpine();
|
|
343
|
+
return activationSpine;
|
|
344
|
+
}
|
|
345
|
+
function recordRuntimeEvidence(spine, origin) {
|
|
346
|
+
if (origin === "service_start" && spine.serviceStartRecorded) {
|
|
347
|
+
return;
|
|
49
348
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
router: createFallbackRouter(),
|
|
53
|
-
runtimeHandle: { ready: true, version: "0.1.7-minimal", close() { } },
|
|
54
|
-
lifecycleState: getLifecycleState(),
|
|
55
|
-
serviceStartRecorded: false,
|
|
56
|
-
};
|
|
57
|
-
return activationSpine;
|
|
349
|
+
if (origin === "service_start") {
|
|
350
|
+
spine.serviceStartRecorded = true;
|
|
58
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);
|
|
59
364
|
}
|
|
60
365
|
function refreshRegistrationState() {
|
|
61
366
|
const spine = ensureActivationSpine();
|
|
@@ -65,62 +370,6 @@ function refreshRegistrationState() {
|
|
|
65
370
|
recordRuntimeEvidence(spine, "register");
|
|
66
371
|
return spine;
|
|
67
372
|
}
|
|
68
|
-
function recordRuntimeEvidence(spine, origin) {
|
|
69
|
-
const decisionLedger = spine.decisionLedger;
|
|
70
|
-
const executionTelemetry = spine.executionTelemetry;
|
|
71
|
-
if (!decisionLedger || !executionTelemetry) {
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
if (origin === "service_start" && spine.serviceStartRecorded) {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
if (origin === "service_start") {
|
|
78
|
-
spine.serviceStartRecorded = true;
|
|
79
|
-
}
|
|
80
|
-
const now = new Date().toISOString();
|
|
81
|
-
const traceId = `${INTERNAL_RUNTIME_TRACE_PREFIX}${origin}-${spine.lifecycleState.registerCount}-${Date.now()}`;
|
|
82
|
-
const decisionId = `decision-${traceId}`;
|
|
83
|
-
const tickId = `tick-${traceId}`;
|
|
84
|
-
const intentId = `intent-${traceId}`;
|
|
85
|
-
const capability = origin === "register"
|
|
86
|
-
? spine.lifecycleState.registerCount === 1
|
|
87
|
-
? "runtime.activate"
|
|
88
|
-
: "runtime.reload"
|
|
89
|
-
: "runtime.heartbeat";
|
|
90
|
-
void (async () => {
|
|
91
|
-
await decisionLedger.recordHeartbeatDecision({
|
|
92
|
-
id: decisionId,
|
|
93
|
-
tickId,
|
|
94
|
-
traceId,
|
|
95
|
-
intentId,
|
|
96
|
-
runtimeScope: "rhythm",
|
|
97
|
-
triggerSource: "heartbeat_bridge",
|
|
98
|
-
decisionStatus: "heartbeat_ok",
|
|
99
|
-
reasons: [
|
|
100
|
-
`origin:${origin}`,
|
|
101
|
-
`phase:${spine.lifecycleState.phase}`,
|
|
102
|
-
`registrations:${spine.lifecycleState.registerCount}`,
|
|
103
|
-
],
|
|
104
|
-
mode: "active",
|
|
105
|
-
createdAt: now,
|
|
106
|
-
});
|
|
107
|
-
await executionTelemetry.startAttempt({
|
|
108
|
-
traceId,
|
|
109
|
-
decisionId,
|
|
110
|
-
intentId,
|
|
111
|
-
platformId: INTERNAL_RUNTIME_PLATFORM_ID,
|
|
112
|
-
capability,
|
|
113
|
-
channel: INTERNAL_RUNTIME_CHANNEL,
|
|
114
|
-
status: "started",
|
|
115
|
-
startedAt: now,
|
|
116
|
-
});
|
|
117
|
-
await executionTelemetry.completeAttempt(traceId, "succeeded", "committed");
|
|
118
|
-
})().catch(() => {
|
|
119
|
-
if (origin === "service_start") {
|
|
120
|
-
spine.serviceStartRecorded = false;
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
373
|
function parseCommandInput(rawArgs) {
|
|
125
374
|
const tokens = rawArgs?.trim().split(/\s+/).filter(Boolean) ?? [];
|
|
126
375
|
if (tokens.length === 0) {
|
|
@@ -176,6 +425,17 @@ function parseCommandInput(rawArgs) {
|
|
|
176
425
|
command,
|
|
177
426
|
input: rest[0] ? { platformId: rest[0] } : undefined,
|
|
178
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
|
+
};
|
|
179
439
|
case "explain":
|
|
180
440
|
return {
|
|
181
441
|
ok: true,
|
|
@@ -221,7 +481,6 @@ export default {
|
|
|
221
481
|
name: "Second Nature",
|
|
222
482
|
description: "Registers command/tool/service surface with load-reload lifecycle semantics.",
|
|
223
483
|
register(api) {
|
|
224
|
-
const spine = refreshRegistrationState();
|
|
225
484
|
const runtimeService = createRuntimeService();
|
|
226
485
|
const lifecycleService = createLifecycleService();
|
|
227
486
|
api.registerService(runtimeService);
|
|
@@ -231,6 +490,7 @@ export default {
|
|
|
231
490
|
description: "Route Agent-facing operational commands for Second Nature.",
|
|
232
491
|
acceptsArgs: true,
|
|
233
492
|
handler: async (ctx) => {
|
|
493
|
+
const spine = ensureActivationSpine();
|
|
234
494
|
const parsed = parseCommandInput(ctx.args);
|
|
235
495
|
if (!parsed.ok) {
|
|
236
496
|
return {
|
|
@@ -262,6 +522,7 @@ export default {
|
|
|
262
522
|
required: ["command"],
|
|
263
523
|
},
|
|
264
524
|
async execute(_id, params) {
|
|
525
|
+
const spine = ensureActivationSpine();
|
|
265
526
|
const resolved = spine.router.resolve(params.command);
|
|
266
527
|
if (!resolved) {
|
|
267
528
|
return {
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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);
|