@haaaiawd/second-nature 0.1.19 → 0.1.21
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 +86 -30
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/runtime/cli/commands/index.js +37 -6
- package/runtime/cli/index.js +8 -0
- package/runtime/cli/ops/heartbeat-surface.d.ts +6 -0
- package/runtime/cli/ops/heartbeat-surface.js +1 -0
- package/runtime/cli/ops/ops-router.d.ts +12 -0
- package/runtime/cli/ops/ops-router.js +89 -0
- package/runtime/cli/ops/workspace-heartbeat-runner.d.ts +6 -0
- package/runtime/cli/ops/workspace-heartbeat-runner.js +1 -0
- package/runtime/cli/read-models/index.d.ts +12 -1
- package/runtime/cli/read-models/index.js +26 -0
- package/runtime/cli/read-models/types.d.ts +18 -1
- package/runtime/connectors/base/contract.d.ts +10 -0
- package/runtime/connectors/base/contract.js +10 -2
- package/runtime/connectors/base/failure-taxonomy.js +63 -15
- package/runtime/connectors/services/connector-executor-adapter.d.ts +16 -0
- package/runtime/connectors/services/connector-executor-adapter.js +118 -0
- package/runtime/connectors/services/credential-route-context.d.ts +10 -0
- package/runtime/connectors/services/credential-route-context.js +19 -0
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +7 -1
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +23 -0
- package/runtime/core/second-nature/orchestrator/effect-dispatcher.d.ts +3 -11
- package/runtime/core/second-nature/orchestrator/effect-dispatcher.js +9 -4
- package/runtime/core/second-nature/runtime/service-entry.js +1 -1
- package/runtime/observability/services/observability-retention.d.ts +10 -0
- package/runtime/observability/services/observability-retention.js +37 -0
- package/workspace-ops-bridge.js +11 -2
package/index.js
CHANGED
|
@@ -96,6 +96,13 @@ const WORKSPACE_BRIDGE_COMMANDS = new Set([
|
|
|
96
96
|
"heartbeat_check",
|
|
97
97
|
"fallback",
|
|
98
98
|
"storage_smoke",
|
|
99
|
+
// T1.2.8 (SN-CODE-03): capability probe surface via workspace bridge
|
|
100
|
+
"capability_probe",
|
|
101
|
+
// T1.2.6 / T1.2.7: policy show + audit read surface
|
|
102
|
+
"policy",
|
|
103
|
+
"audit",
|
|
104
|
+
// T3.3.2: near-real connector smoke sentinel
|
|
105
|
+
"near_real_smoke",
|
|
99
106
|
]);
|
|
100
107
|
function isWorkspaceBridgeCommand(command, input) {
|
|
101
108
|
if (command === "credential") {
|
|
@@ -153,18 +160,25 @@ function resolveWorkspaceRoot(toolWorkspaceRoot) {
|
|
|
153
160
|
if (tool) {
|
|
154
161
|
return { resolution: "tool_args", declaredRoot: tool, runtimeRoot: tool };
|
|
155
162
|
}
|
|
156
|
-
return {
|
|
163
|
+
return {
|
|
164
|
+
resolution: "unknown",
|
|
165
|
+
declaredRoot: undefined,
|
|
166
|
+
runtimeRoot: process.cwd(),
|
|
167
|
+
};
|
|
157
168
|
}
|
|
158
169
|
function syncWorkspaceRootFromTool(spine, toolWorkspaceRoot) {
|
|
159
170
|
const next = resolveWorkspaceRoot(toolWorkspaceRoot);
|
|
160
171
|
const prev = spine.workspaceRootContext;
|
|
161
|
-
const changed = next.runtimeRoot !== prev.runtimeRoot ||
|
|
172
|
+
const changed = next.runtimeRoot !== prev.runtimeRoot ||
|
|
173
|
+
next.resolution !== prev.resolution;
|
|
162
174
|
if (changed) {
|
|
163
175
|
disposeWorkspaceOpsBridge();
|
|
164
176
|
}
|
|
165
177
|
spine.workspaceRootContext = next;
|
|
166
178
|
if (changed) {
|
|
167
|
-
spine.runtimeHandle = startRuntimeService({
|
|
179
|
+
spine.runtimeHandle = startRuntimeService({
|
|
180
|
+
workspaceRoot: next.runtimeRoot,
|
|
181
|
+
});
|
|
168
182
|
}
|
|
169
183
|
}
|
|
170
184
|
function trimRuntimeEvidence(spine) {
|
|
@@ -229,7 +243,8 @@ function parseExplainSubject(subjectRaw) {
|
|
|
229
243
|
}
|
|
230
244
|
function buildStatusPayload(spine) {
|
|
231
245
|
const runtimeEvidence = latestRuntimeEvidence(spine);
|
|
232
|
-
const updatedAt = runtimeEvidence?.createdAt ??
|
|
246
|
+
const updatedAt = runtimeEvidence?.createdAt ??
|
|
247
|
+
new Date(spine.lifecycleState.lastChangedAt).toISOString();
|
|
233
248
|
const wr = spine.workspaceRootContext;
|
|
234
249
|
const needsRootHint = wr.resolution === "unknown";
|
|
235
250
|
return {
|
|
@@ -240,7 +255,9 @@ function buildStatusPayload(spine) {
|
|
|
240
255
|
error: {
|
|
241
256
|
code: "WORKSPACE_READ_SURFACE_UNAVAILABLE",
|
|
242
257
|
message: "Aggregated status requires workspace state; the host-safe plugin does not load persisted read models on this surface.",
|
|
243
|
-
requiredUserInput: needsRootHint
|
|
258
|
+
requiredUserInput: needsRootHint
|
|
259
|
+
? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"]
|
|
260
|
+
: [],
|
|
244
261
|
nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
|
|
245
262
|
},
|
|
246
263
|
data: {
|
|
@@ -264,7 +281,9 @@ function buildQuietPayload(spine, scope) {
|
|
|
264
281
|
error: {
|
|
265
282
|
code: "QUIET_READ_SURFACE_UNAVAILABLE",
|
|
266
283
|
message: "Quiet read surface requires workspace runtime; not evaluated in host-safe carrier mode.",
|
|
267
|
-
requiredUserInput: wr.resolution === "unknown"
|
|
284
|
+
requiredUserInput: wr.resolution === "unknown"
|
|
285
|
+
? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"]
|
|
286
|
+
: [],
|
|
268
287
|
nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
|
|
269
288
|
},
|
|
270
289
|
data: {
|
|
@@ -285,7 +304,9 @@ function buildReportPayload(spine, day) {
|
|
|
285
304
|
error: {
|
|
286
305
|
code: "REPORT_READ_SURFACE_UNAVAILABLE",
|
|
287
306
|
message: "Daily report artifacts require workspace runtime.",
|
|
288
|
-
requiredUserInput: wr.resolution === "unknown"
|
|
307
|
+
requiredUserInput: wr.resolution === "unknown"
|
|
308
|
+
? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"]
|
|
309
|
+
: [],
|
|
289
310
|
nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
|
|
290
311
|
},
|
|
291
312
|
data: {
|
|
@@ -317,7 +338,9 @@ function buildSessionPayload(spine, sessionId) {
|
|
|
317
338
|
error: {
|
|
318
339
|
code: "SESSION_READ_SURFACE_UNAVAILABLE",
|
|
319
340
|
message: "Session analytics require workspace state database.",
|
|
320
|
-
requiredUserInput: wr.resolution === "unknown"
|
|
341
|
+
requiredUserInput: wr.resolution === "unknown"
|
|
342
|
+
? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"]
|
|
343
|
+
: [],
|
|
321
344
|
nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
|
|
322
345
|
},
|
|
323
346
|
data: {
|
|
@@ -338,7 +361,9 @@ function buildCredentialPayload(spine, platformId) {
|
|
|
338
361
|
error: {
|
|
339
362
|
code: "CREDENTIAL_READ_SURFACE_UNAVAILABLE",
|
|
340
363
|
message: "Credential inspection requires workspace runtime on this surface.",
|
|
341
|
-
requiredUserInput: wr.resolution === "unknown"
|
|
364
|
+
requiredUserInput: wr.resolution === "unknown"
|
|
365
|
+
? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"]
|
|
366
|
+
: [],
|
|
342
367
|
nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
|
|
343
368
|
},
|
|
344
369
|
data: {
|
|
@@ -384,7 +409,9 @@ function buildExplainPayload(spine, subjectRaw) {
|
|
|
384
409
|
error: {
|
|
385
410
|
code: "EXPLAIN_READ_SURFACE_UNAVAILABLE",
|
|
386
411
|
message: "Evidence-backed explain requires persisted workspace read models; host-safe carrier did not evaluate operator explain (CH-11-02).",
|
|
387
|
-
requiredUserInput: wr.resolution === "unknown"
|
|
412
|
+
requiredUserInput: wr.resolution === "unknown"
|
|
413
|
+
? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"]
|
|
414
|
+
: [],
|
|
388
415
|
nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
|
|
389
416
|
},
|
|
390
417
|
data: {
|
|
@@ -398,8 +425,13 @@ async function buildStorageSmokePayload(input) {
|
|
|
398
425
|
try {
|
|
399
426
|
const mod = await import("./runtime/storage/bootstrap/storage-mode-smoke.js");
|
|
400
427
|
const runRepairFixture = Boolean(input?.runRepairFixture);
|
|
401
|
-
const workspaceRoot = typeof input?.workspaceRoot === "string"
|
|
402
|
-
|
|
428
|
+
const workspaceRoot = typeof input?.workspaceRoot === "string"
|
|
429
|
+
? input.workspaceRoot
|
|
430
|
+
: undefined;
|
|
431
|
+
const data = await mod.runStorageModeSmoke({
|
|
432
|
+
runRepairFixture,
|
|
433
|
+
workspaceRoot,
|
|
434
|
+
});
|
|
403
435
|
return { ok: true, data };
|
|
404
436
|
}
|
|
405
437
|
catch (error) {
|
|
@@ -434,8 +466,11 @@ function isProbeOnlyInput(input) {
|
|
|
434
466
|
}
|
|
435
467
|
function buildHeartbeatCheckPayload(spine, input) {
|
|
436
468
|
const runtimeEvidence = latestRuntimeEvidence(spine);
|
|
437
|
-
const updatedAt = runtimeEvidence?.createdAt ??
|
|
438
|
-
|
|
469
|
+
const updatedAt = runtimeEvidence?.createdAt ??
|
|
470
|
+
new Date(spine.lifecycleState.lastChangedAt).toISOString();
|
|
471
|
+
const timestamp = typeof input?.timestamp === "string" && input.timestamp.trim().length > 0
|
|
472
|
+
? input.timestamp
|
|
473
|
+
: updatedAt;
|
|
439
474
|
const wr = spine.workspaceRootContext;
|
|
440
475
|
if (isProbeOnlyInput(input)) {
|
|
441
476
|
return {
|
|
@@ -461,8 +496,10 @@ function buildHeartbeatCheckPayload(spine, input) {
|
|
|
461
496
|
bridge: {
|
|
462
497
|
timestamp,
|
|
463
498
|
probeOnly: true,
|
|
464
|
-
sessionContextProvided: typeof input?.sessionContext === "string" &&
|
|
465
|
-
|
|
499
|
+
sessionContextProvided: typeof input?.sessionContext === "string" &&
|
|
500
|
+
input.sessionContext.trim().length > 0,
|
|
501
|
+
heartbeatChecklistProvided: typeof input?.heartbeatChecklist === "string" &&
|
|
502
|
+
input.heartbeatChecklist.trim().length > 0,
|
|
466
503
|
serviceEntryMode: "capability_probe",
|
|
467
504
|
},
|
|
468
505
|
},
|
|
@@ -491,19 +528,16 @@ function buildHeartbeatCheckPayload(spine, input) {
|
|
|
491
528
|
},
|
|
492
529
|
bridge: {
|
|
493
530
|
timestamp,
|
|
494
|
-
sessionContextProvided: typeof input?.sessionContext === "string" &&
|
|
495
|
-
|
|
531
|
+
sessionContextProvided: typeof input?.sessionContext === "string" &&
|
|
532
|
+
input.sessionContext.trim().length > 0,
|
|
533
|
+
heartbeatChecklistProvided: typeof input?.heartbeatChecklist === "string" &&
|
|
534
|
+
input.heartbeatChecklist.trim().length > 0,
|
|
496
535
|
serviceEntryMode: "runtime_carrier_only",
|
|
497
536
|
},
|
|
498
537
|
},
|
|
499
538
|
};
|
|
500
539
|
}
|
|
501
540
|
function createHostSafeRouter(spine) {
|
|
502
|
-
const notImplemented = async (command) => ({
|
|
503
|
-
ok: false,
|
|
504
|
-
command,
|
|
505
|
-
message: HOST_SAFE_LIMITATION_MESSAGE,
|
|
506
|
-
});
|
|
507
541
|
const commands = [
|
|
508
542
|
{
|
|
509
543
|
name: "status",
|
|
@@ -518,7 +552,7 @@ function createHostSafeRouter(spine) {
|
|
|
518
552
|
if (action === "set") {
|
|
519
553
|
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");
|
|
520
554
|
}
|
|
521
|
-
return
|
|
555
|
+
return createUnavailableActionError("HOST_SAFE_POLICY_SHOW_UNAVAILABLE", "Policy read requires workspace state database; host-safe plugin does not load persisted policy rows.", [], "run_workspace_second_nature_cli_or_full_runtime_package");
|
|
522
556
|
},
|
|
523
557
|
},
|
|
524
558
|
{
|
|
@@ -560,7 +594,7 @@ function createHostSafeRouter(spine) {
|
|
|
560
594
|
{
|
|
561
595
|
name: "audit",
|
|
562
596
|
description: "Inspect audit and evidence views",
|
|
563
|
-
execute: async () =>
|
|
597
|
+
execute: async () => createUnavailableActionError("HOST_SAFE_AUDIT_UNAVAILABLE", "Audit read requires workspace observability database; host-safe plugin does not load persisted audit events.", [], "run_workspace_second_nature_cli_or_full_runtime_package"),
|
|
564
598
|
},
|
|
565
599
|
{
|
|
566
600
|
name: "explain",
|
|
@@ -588,6 +622,16 @@ function createHostSafeRouter(spine) {
|
|
|
588
622
|
description: "T4.1.4 storage mode smoke report (sql.js vs native probe)",
|
|
589
623
|
execute: async (input) => buildStorageSmokePayload(input),
|
|
590
624
|
},
|
|
625
|
+
{
|
|
626
|
+
name: "capability_probe",
|
|
627
|
+
description: "Probe host capabilities (workspace runtime required for full report)",
|
|
628
|
+
execute: async () => createUnavailableActionError("HOST_SAFE_CAPABILITY_PROBE_UNAVAILABLE", "Full capability probe requires workspace observability database for persistence; host-safe carrier returns static unknown.", [], "run_workspace_second_nature_cli_or_full_runtime_package"),
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
name: "near_real_smoke",
|
|
632
|
+
description: "Run near-real connector smoke (workspace runtime + connectors required)",
|
|
633
|
+
execute: async () => createUnavailableActionError("HOST_SAFE_NEAR_REAL_SMOKE_UNAVAILABLE", "Near-real connector smoke requires workspace state and observability databases; host-safe plugin cannot run connector harness.", [], "run_workspace_second_nature_cli_or_full_runtime_package"),
|
|
634
|
+
},
|
|
591
635
|
];
|
|
592
636
|
return {
|
|
593
637
|
commands,
|
|
@@ -600,7 +644,9 @@ function createActivationSpine() {
|
|
|
600
644
|
const workspaceRootContext = resolveWorkspaceRoot(undefined);
|
|
601
645
|
const spine = {
|
|
602
646
|
router: undefined,
|
|
603
|
-
runtimeHandle: startRuntimeService({
|
|
647
|
+
runtimeHandle: startRuntimeService({
|
|
648
|
+
workspaceRoot: workspaceRootContext.runtimeRoot,
|
|
649
|
+
}),
|
|
604
650
|
lifecycleState: getLifecycleState(),
|
|
605
651
|
serviceStartRecorded: false,
|
|
606
652
|
runtimeEvidence: [],
|
|
@@ -640,12 +686,15 @@ function refreshRegistrationState() {
|
|
|
640
686
|
const spine = ensureActivationSpine();
|
|
641
687
|
const workspaceRootContext = resolveWorkspaceRoot(undefined);
|
|
642
688
|
const prev = spine.workspaceRootContext;
|
|
643
|
-
const changed = workspaceRootContext.runtimeRoot !== prev.runtimeRoot ||
|
|
689
|
+
const changed = workspaceRootContext.runtimeRoot !== prev.runtimeRoot ||
|
|
690
|
+
workspaceRootContext.resolution !== prev.resolution;
|
|
644
691
|
if (changed) {
|
|
645
692
|
disposeWorkspaceOpsBridge();
|
|
646
693
|
}
|
|
647
694
|
spine.workspaceRootContext = workspaceRootContext;
|
|
648
|
-
spine.runtimeHandle = startRuntimeService({
|
|
695
|
+
spine.runtimeHandle = startRuntimeService({
|
|
696
|
+
workspaceRoot: workspaceRootContext.runtimeRoot,
|
|
697
|
+
});
|
|
649
698
|
spine.lifecycleState = recordRegistration();
|
|
650
699
|
spine.serviceStartRecorded = false;
|
|
651
700
|
recordRuntimeEvidence(spine, "register");
|
|
@@ -809,7 +858,11 @@ export default definePluginEntry({
|
|
|
809
858
|
const resolved = spine.router.resolve(parsed.command);
|
|
810
859
|
if (!resolved) {
|
|
811
860
|
return {
|
|
812
|
-
text: JSON.stringify({
|
|
861
|
+
text: JSON.stringify({
|
|
862
|
+
ok: false,
|
|
863
|
+
command: parsed.command,
|
|
864
|
+
message: "Unknown Second Nature command.",
|
|
865
|
+
}),
|
|
813
866
|
};
|
|
814
867
|
}
|
|
815
868
|
const result = await routeSecondNatureCommand(spine, parsed.command, parsed.input);
|
|
@@ -827,7 +880,10 @@ export default definePluginEntry({
|
|
|
827
880
|
content: [
|
|
828
881
|
{
|
|
829
882
|
type: "text",
|
|
830
|
-
text: JSON.stringify({
|
|
883
|
+
text: JSON.stringify({
|
|
884
|
+
ok: false,
|
|
885
|
+
message: "Unknown Second Nature command.",
|
|
886
|
+
}),
|
|
831
887
|
},
|
|
832
888
|
],
|
|
833
889
|
};
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "second-nature",
|
|
3
3
|
"name": "Second Nature",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.21",
|
|
5
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 (see README / T1.1.4 ops norm).",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onStartup": true,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { credentialVerify } from "./credential.js";
|
|
2
2
|
import { formatExplanation } from "../explain/format-explanation.js";
|
|
3
3
|
import { explainSurfaceSubject } from "../explain/explain-surface-subject.js";
|
|
4
|
-
import { showOperatorFallback, OperatorFallbackNotFoundError } from "../ops/show-operator-fallback.js";
|
|
4
|
+
import { showOperatorFallback, OperatorFallbackNotFoundError, } from "../ops/show-operator-fallback.js";
|
|
5
5
|
import { runStorageModeSmoke } from "../../storage/bootstrap/storage-mode-smoke.js";
|
|
6
6
|
import { policySet } from "./policy.js";
|
|
7
7
|
const notImplemented = async (command) => ({
|
|
@@ -40,7 +40,10 @@ export function createCliCommands(deps) {
|
|
|
40
40
|
if (action === "set") {
|
|
41
41
|
return policySet(actionBridge, input);
|
|
42
42
|
}
|
|
43
|
-
|
|
43
|
+
// T1.2.6 (SN-CODE-01): `policy show` (default) returns the current rhythm policy
|
|
44
|
+
// snapshot. Returns workspace defaults when no policy row has been persisted yet.
|
|
45
|
+
const data = await readModels.loadPolicy();
|
|
46
|
+
return { ok: true, data };
|
|
44
47
|
},
|
|
45
48
|
},
|
|
46
49
|
{
|
|
@@ -69,7 +72,9 @@ export function createCliCommands(deps) {
|
|
|
69
72
|
name: "report",
|
|
70
73
|
description: "Show daily report artifacts",
|
|
71
74
|
execute: async (input) => {
|
|
72
|
-
const day = typeof input?.day === "string"
|
|
75
|
+
const day = typeof input?.day === "string"
|
|
76
|
+
? input.day
|
|
77
|
+
: new Date().toISOString().slice(0, 10);
|
|
73
78
|
const data = await readModels.loadDailyReport(day);
|
|
74
79
|
return { ok: true, data };
|
|
75
80
|
},
|
|
@@ -97,7 +102,12 @@ export function createCliCommands(deps) {
|
|
|
97
102
|
{
|
|
98
103
|
name: "audit",
|
|
99
104
|
description: "Inspect audit and evidence views",
|
|
100
|
-
execute: () =>
|
|
105
|
+
execute: async () => {
|
|
106
|
+
// T1.2.7 (SN-CODE-02): minimal read-side view — list all in-memory audit events.
|
|
107
|
+
// Empty store returns { totalEvents: 0, events: [] } (honest empty, not an error).
|
|
108
|
+
const data = await readModels.loadAuditSummary();
|
|
109
|
+
return { ok: true, data };
|
|
110
|
+
},
|
|
101
111
|
},
|
|
102
112
|
{
|
|
103
113
|
name: "explain",
|
|
@@ -148,8 +158,13 @@ export function createCliCommands(deps) {
|
|
|
148
158
|
description: "T4.1.4 — report sql.js vs native SQLite probe and optional artifact→index repair fixture",
|
|
149
159
|
execute: async (input) => {
|
|
150
160
|
const runRepairFixture = Boolean(input?.runRepairFixture);
|
|
151
|
-
const workspaceRoot = typeof input?.workspaceRoot === "string"
|
|
152
|
-
|
|
161
|
+
const workspaceRoot = typeof input?.workspaceRoot === "string"
|
|
162
|
+
? input.workspaceRoot
|
|
163
|
+
: undefined;
|
|
164
|
+
const data = await runStorageModeSmoke({
|
|
165
|
+
runRepairFixture,
|
|
166
|
+
workspaceRoot,
|
|
167
|
+
});
|
|
153
168
|
return { ok: true, data };
|
|
154
169
|
},
|
|
155
170
|
},
|
|
@@ -189,5 +204,21 @@ export function createCliCommands(deps) {
|
|
|
189
204
|
}
|
|
190
205
|
},
|
|
191
206
|
},
|
|
207
|
+
{
|
|
208
|
+
name: "capability_probe",
|
|
209
|
+
description: "T1.2.8 — probe host capabilities and persist report (static unknown adapter in CLI context)",
|
|
210
|
+
execute: async (input) => {
|
|
211
|
+
const surface = await Promise.resolve(opsRouter.dispatch("capability_probe", input));
|
|
212
|
+
return surface;
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: "near_real_smoke",
|
|
217
|
+
description: "T3.3.2 — run near-real connector smoke (sentinel Moltbook + EvoMap, no live HTTP)",
|
|
218
|
+
execute: async (input) => {
|
|
219
|
+
const surface = await Promise.resolve(opsRouter.dispatch("near_real_smoke", input));
|
|
220
|
+
return surface;
|
|
221
|
+
},
|
|
222
|
+
},
|
|
192
223
|
];
|
|
193
224
|
}
|
package/runtime/cli/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { createOpsRouter } from "./ops/ops-router.js";
|
|
|
7
7
|
import { createCliReadModels, } from "./read-models/index.js";
|
|
8
8
|
import { resolvePackagedRuntime } from "./runtime/runtime-artifact-boundary.js";
|
|
9
9
|
import { createRuntimeDecisionRecorder, } from "../observability/services/runtime-decision-recorder.js";
|
|
10
|
+
import { createConnectorExecutorAdapter } from "../connectors/services/connector-executor-adapter.js";
|
|
10
11
|
export function createCliRuntimeDeps(overrides = {}) {
|
|
11
12
|
const stateDb = overrides.stateDb ?? createStateDatabase();
|
|
12
13
|
const observabilityDb = overrides.observabilityDb ?? createObservabilityDatabase();
|
|
@@ -16,6 +17,7 @@ export function createCliRuntimeDeps(overrides = {}) {
|
|
|
16
17
|
stateDb,
|
|
17
18
|
observabilityDb,
|
|
18
19
|
workspaceRoot: process.cwd(),
|
|
20
|
+
livedExperienceAuditStore: overrides.livedExperienceAuditStore,
|
|
19
21
|
});
|
|
20
22
|
const actionBridge = overrides.actionBridge ?? createActionBridge(stateApi);
|
|
21
23
|
const runtimeRecorder = overrides.runtimeRecorder ?? createRuntimeDecisionRecorder(observabilityDb);
|
|
@@ -31,12 +33,18 @@ export function createCliRuntimeDeps(overrides = {}) {
|
|
|
31
33
|
export function createCommandRouter(options = {}) {
|
|
32
34
|
const runtime = createCliRuntimeDeps(options.deps);
|
|
33
35
|
const pluginRoot = path.join(process.cwd(), "plugin");
|
|
36
|
+
const connectorExecutor = createConnectorExecutorAdapter({
|
|
37
|
+
stateDb: runtime.stateDb,
|
|
38
|
+
observabilityDb: runtime.observabilityDb,
|
|
39
|
+
});
|
|
34
40
|
const opsRouter = createOpsRouter({
|
|
35
41
|
runtimeAvailable: resolvePackagedRuntime(pluginRoot).ok,
|
|
36
42
|
readModels: runtime.readModels,
|
|
37
43
|
runtimeRecorder: runtime.runtimeRecorder,
|
|
38
44
|
state: runtime.stateDb,
|
|
39
45
|
workspaceRoot: process.cwd(),
|
|
46
|
+
observabilityDb: runtime.observabilityDb,
|
|
47
|
+
connectorExecutor,
|
|
40
48
|
});
|
|
41
49
|
const commands = createCliCommands({
|
|
42
50
|
readModels: runtime.readModels,
|
|
@@ -9,6 +9,7 @@ import type { HeartbeatSignal } from "../../core/second-nature/heartbeat/signal.
|
|
|
9
9
|
import type { CliReadModels } from "../read-models/index.js";
|
|
10
10
|
import type { RuntimeDecisionRecorder } from "../../observability/services/runtime-decision-recorder.js";
|
|
11
11
|
import type { StateDatabase } from "../../storage/db/index.js";
|
|
12
|
+
import type { ConnectorExecutor } from "../../core/second-nature/orchestrator/effect-dispatcher.js";
|
|
12
13
|
export type HeartbeatSurfaceStatus = "heartbeat_ok" | "intent_selected" | "denied" | "deferred" | "runtime_carrier_only" | "delivery_unavailable";
|
|
13
14
|
export interface HeartbeatSurfaceResult {
|
|
14
15
|
ok: boolean;
|
|
@@ -41,5 +42,10 @@ export interface HeartbeatCheckInput {
|
|
|
41
42
|
timestamp?: string;
|
|
42
43
|
sessionContext?: string;
|
|
43
44
|
scopeHint?: HeartbeatSignal["scopeHint"];
|
|
45
|
+
/**
|
|
46
|
+
* When present, guard-allowed connector_action intents are dispatched through the
|
|
47
|
+
* connector-system instead of returning connector_dispatch_unwired.
|
|
48
|
+
*/
|
|
49
|
+
connectorExecutor?: ConnectorExecutor;
|
|
44
50
|
}
|
|
45
51
|
export declare function heartbeatCheck(input: HeartbeatCheckInput): Promise<HeartbeatSurfaceResult>;
|
|
@@ -73,6 +73,7 @@ export async function heartbeatCheck(input) {
|
|
|
73
73
|
runtimeRecorder: input.runtimeRecorder,
|
|
74
74
|
state: input.state,
|
|
75
75
|
workspaceRoot: input.workspaceRoot ?? process.cwd(),
|
|
76
|
+
connectorExecutor: input.connectorExecutor,
|
|
76
77
|
});
|
|
77
78
|
const cycle = await run(signal);
|
|
78
79
|
return mapCycleToSurface(cycle, "workspace_full_runtime");
|
|
@@ -5,6 +5,8 @@ import { type HeartbeatCheckInput, type HeartbeatSurfaceResult } from "./heartbe
|
|
|
5
5
|
import type { CliReadModels } from "../read-models/index.js";
|
|
6
6
|
import type { RuntimeDecisionRecorder } from "../../observability/services/runtime-decision-recorder.js";
|
|
7
7
|
import type { StateDatabase } from "../../storage/db/index.js";
|
|
8
|
+
import type { ObservabilityDatabase } from "../../observability/db/index.js";
|
|
9
|
+
import type { ConnectorExecutor } from "../../core/second-nature/orchestrator/effect-dispatcher.js";
|
|
8
10
|
export interface OpsRouterDeps {
|
|
9
11
|
/** When true, packaged runtime artifacts resolved and full graph is loadable */
|
|
10
12
|
runtimeAvailable: boolean;
|
|
@@ -18,6 +20,16 @@ export interface OpsRouterDeps {
|
|
|
18
20
|
*/
|
|
19
21
|
state?: StateDatabase;
|
|
20
22
|
workspaceRoot?: string;
|
|
23
|
+
/**
|
|
24
|
+
* T1.2.8 (SN-CODE-03): observability DB for persisting capability probe reports.
|
|
25
|
+
* When absent, `capability_probe` still runs but skips persistence.
|
|
26
|
+
*/
|
|
27
|
+
observabilityDb?: ObservabilityDatabase;
|
|
28
|
+
/**
|
|
29
|
+
* When present, guard-allowed connector_action intents are dispatched through the
|
|
30
|
+
* connector-system instead of returning connector_dispatch_unwired.
|
|
31
|
+
*/
|
|
32
|
+
connectorExecutor?: ConnectorExecutor;
|
|
21
33
|
}
|
|
22
34
|
export interface OpsRouter {
|
|
23
35
|
heartbeatCheck(input: HeartbeatCheckInput): Promise<HeartbeatSurfaceResult>;
|
|
@@ -3,10 +3,35 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { heartbeatCheck, } from "./heartbeat-surface.js";
|
|
5
5
|
import { showOperatorFallback, OperatorFallbackNotFoundError, } from "./show-operator-fallback.js";
|
|
6
|
+
import { probeHostCapability } from "../host-capability/probe-host-capability.js";
|
|
7
|
+
import { recordHostCapability } from "../host-capability/record-host-capability.js";
|
|
8
|
+
import { runNearRealConnectorSmoke } from "../../connectors/near-real/near-real-connector-smoke.js";
|
|
6
9
|
function coerceProbeOnlyFlag(input) {
|
|
7
10
|
const v = input?.probeOnly;
|
|
8
11
|
return v === true || v === "true" || v === 1 || v === "1";
|
|
9
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* T1.2.8 — static local adapter: all checks return `unknown` when no real host is available.
|
|
15
|
+
* Allows `capability_probe` to be called from CLI / workspace bridge without requiring a live host.
|
|
16
|
+
*/
|
|
17
|
+
function createStaticUnknownAdapter() {
|
|
18
|
+
const now = new Date().toISOString();
|
|
19
|
+
const unknownResult = (name) => ({
|
|
20
|
+
name,
|
|
21
|
+
verdict: "unknown",
|
|
22
|
+
observedAt: now,
|
|
23
|
+
reason: "static_local_probe_no_host_context",
|
|
24
|
+
evidenceRefs: [],
|
|
25
|
+
});
|
|
26
|
+
return {
|
|
27
|
+
checkPluginLoad: () => unknownResult("plugin_load"),
|
|
28
|
+
checkHeartbeatBridge: () => unknownResult("heartbeat_bridge"),
|
|
29
|
+
checkHeartbeatToolInvocation: () => unknownResult("heartbeat_tool_invocation"),
|
|
30
|
+
checkDeliveryTarget: () => ({ status: "unknown", evidenceRefs: [] }),
|
|
31
|
+
checkAckDropBehavior: () => unknownResult("ack_drop"),
|
|
32
|
+
checkHookSupport: () => [],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
10
35
|
export function createOpsRouter(deps) {
|
|
11
36
|
return {
|
|
12
37
|
heartbeatCheck: (input) => heartbeatCheck({
|
|
@@ -16,6 +41,7 @@ export function createOpsRouter(deps) {
|
|
|
16
41
|
runtimeRecorder: input.runtimeRecorder ?? deps.runtimeRecorder,
|
|
17
42
|
state: input.state ?? deps.state,
|
|
18
43
|
workspaceRoot: input.workspaceRoot ?? deps.workspaceRoot,
|
|
44
|
+
connectorExecutor: input.connectorExecutor ?? deps.connectorExecutor,
|
|
19
45
|
}),
|
|
20
46
|
dispatch(command, input) {
|
|
21
47
|
if (command === "heartbeat_check") {
|
|
@@ -42,6 +68,8 @@ export function createOpsRouter(deps) {
|
|
|
42
68
|
? input.sessionContext
|
|
43
69
|
: undefined,
|
|
44
70
|
scopeHint: input?.scopeHint,
|
|
71
|
+
connectorExecutor: input
|
|
72
|
+
?.connectorExecutor ?? deps.connectorExecutor,
|
|
45
73
|
});
|
|
46
74
|
}
|
|
47
75
|
if (command === "fallback") {
|
|
@@ -90,6 +118,67 @@ export function createOpsRouter(deps) {
|
|
|
90
118
|
}
|
|
91
119
|
})();
|
|
92
120
|
}
|
|
121
|
+
if (command === "capability_probe") {
|
|
122
|
+
// T1.2.8 (SN-CODE-03): run host capability probe with static unknown adapter (CLI context).
|
|
123
|
+
// Persists report when observabilityDb is available; returns safe JSON subset.
|
|
124
|
+
return (async () => {
|
|
125
|
+
const adapter = createStaticUnknownAdapter();
|
|
126
|
+
const docCheckedAt = new Date().toISOString();
|
|
127
|
+
const report = probeHostCapability({
|
|
128
|
+
adapter,
|
|
129
|
+
docLinks: [],
|
|
130
|
+
docCheckedAt,
|
|
131
|
+
});
|
|
132
|
+
if (deps.observabilityDb) {
|
|
133
|
+
await recordHostCapability(deps.observabilityDb, report);
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
ok: true,
|
|
137
|
+
command: "capability_probe",
|
|
138
|
+
data: {
|
|
139
|
+
reportId: report.reportId,
|
|
140
|
+
generatedAt: report.generatedAt,
|
|
141
|
+
deliveryTarget: report.deliveryTarget,
|
|
142
|
+
pluginLoad: { verdict: report.pluginLoad.verdict },
|
|
143
|
+
heartbeatBridge: { verdict: report.heartbeatBridge.verdict },
|
|
144
|
+
heartbeatToolInvocation: {
|
|
145
|
+
verdict: report.heartbeatToolInvocation.verdict,
|
|
146
|
+
},
|
|
147
|
+
ackDropBehavior: { verdict: report.ackDropBehavior.verdict },
|
|
148
|
+
conflictCount: report.conflictRecords.length,
|
|
149
|
+
recommendedNextStep: report.recommendedNextStep,
|
|
150
|
+
note: "static_local_probe: all verdicts are unknown without live host context",
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
})();
|
|
154
|
+
}
|
|
155
|
+
if (command === "near_real_smoke") {
|
|
156
|
+
// T3.3.2 (SN-CODE-05): wrap runNearRealConnectorSmoke as an ops surface command.
|
|
157
|
+
// Requires state + observabilityDb + workspaceRoot to be wired into OpsRouterDeps.
|
|
158
|
+
if (!deps.state || !deps.observabilityDb || !deps.workspaceRoot) {
|
|
159
|
+
return {
|
|
160
|
+
ok: false,
|
|
161
|
+
command: "near_real_smoke",
|
|
162
|
+
error: {
|
|
163
|
+
code: "NEAR_REAL_SMOKE_DEPS_UNAVAILABLE",
|
|
164
|
+
message: "near_real_smoke requires state, observabilityDb, and workspaceRoot in OpsRouterDeps",
|
|
165
|
+
nextStep: "wire_deps_into_ops_router",
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return (async () => {
|
|
170
|
+
const result = await runNearRealConnectorSmoke({
|
|
171
|
+
state: deps.state,
|
|
172
|
+
observabilityDb: deps.observabilityDb,
|
|
173
|
+
workspaceRoot: deps.workspaceRoot,
|
|
174
|
+
});
|
|
175
|
+
return {
|
|
176
|
+
ok: true,
|
|
177
|
+
command: "near_real_smoke",
|
|
178
|
+
data: result,
|
|
179
|
+
};
|
|
180
|
+
})();
|
|
181
|
+
}
|
|
93
182
|
return {
|
|
94
183
|
ok: false,
|
|
95
184
|
error: {
|
|
@@ -17,6 +17,7 @@ import type { SnapshotInputs } from "../../core/second-nature/heartbeat/snapshot
|
|
|
17
17
|
import type { CliReadModels } from "../read-models/index.js";
|
|
18
18
|
import type { RuntimeDecisionRecorder } from "../../observability/services/runtime-decision-recorder.js";
|
|
19
19
|
import type { StateDatabase } from "../../storage/db/index.js";
|
|
20
|
+
import type { ConnectorExecutor } from "../../core/second-nature/orchestrator/effect-dispatcher.js";
|
|
20
21
|
export interface WorkspaceHeartbeatRunnerOptions {
|
|
21
22
|
/** When supplied, the runner persists the cycle so `loadStatus` can read it (T1.2.3). */
|
|
22
23
|
runtimeRecorder?: RuntimeDecisionRecorder;
|
|
@@ -32,6 +33,11 @@ export interface WorkspaceHeartbeatRunnerOptions {
|
|
|
32
33
|
* Defaults to true when workspaceRoot is provided, since this is the host-safe workspace path.
|
|
33
34
|
*/
|
|
34
35
|
enableQuietWorkflow?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* When present, guard-allowed connector_action intents are dispatched through the
|
|
38
|
+
* connector-system instead of returning connector_dispatch_unwired.
|
|
39
|
+
*/
|
|
40
|
+
connectorExecutor?: ConnectorExecutor;
|
|
35
41
|
}
|
|
36
42
|
export declare function loadSnapshotInputsForWorkspaceHeartbeat(readModels: CliReadModels, options?: {
|
|
37
43
|
state?: StateDatabase;
|
|
@@ -76,6 +76,7 @@ export function createWorkspaceHeartbeatRunner(readModels, options = {}) {
|
|
|
76
76
|
quietWorkflow: quietEnabled
|
|
77
77
|
? { workspaceRoot: options.workspaceRoot }
|
|
78
78
|
: undefined,
|
|
79
|
+
connectorExecutor: options.connectorExecutor,
|
|
79
80
|
},
|
|
80
81
|
});
|
|
81
82
|
if (options.runtimeRecorder) {
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { StateDatabase } from "../../storage/db/index.js";
|
|
2
2
|
import type { ObservabilityDatabase } from "../../observability/db/index.js";
|
|
3
3
|
import { AppendOnlyAuditStore } from "../../observability/audit/append-only-audit-store.js";
|
|
4
|
-
import type { StatusReadModel, DailyReportReadModel, QuietReadModel, SessionDetailReadModel, CredentialReadModel, ExplainReadModel, ExplainSubjectKind } from "./types.js";
|
|
4
|
+
import type { StatusReadModel, DailyReportReadModel, QuietReadModel, SessionDetailReadModel, CredentialReadModel, ExplainReadModel, ExplainSubjectKind, AuditSummaryReadModel } from "./types.js";
|
|
5
|
+
export type { AuditSummaryReadModel } from "./types.js";
|
|
5
6
|
export type { ExplainSubjectKind } from "./types.js";
|
|
6
7
|
import type { OperatorFallbackView } from "../../storage/fallback/operator-fallback-view.js";
|
|
8
|
+
import { type RhythmPolicySnapshot } from "../../storage/rhythm/rhythm-policy-snapshot.js";
|
|
9
|
+
export type { RhythmPolicySnapshot };
|
|
7
10
|
export interface CliReadModels {
|
|
8
11
|
loadStatus(scope?: string): Promise<StatusReadModel>;
|
|
9
12
|
loadDailyReport(day: string): Promise<DailyReportReadModel>;
|
|
@@ -13,6 +16,14 @@ export interface CliReadModels {
|
|
|
13
16
|
explain(subject: ExplainSubject): Promise<ExplainReadModel>;
|
|
14
17
|
/** T1.2.2 — persisted operator fallback; view status is always not_sent. */
|
|
15
18
|
loadFallbackView(ref: string): Promise<OperatorFallbackView | null>;
|
|
19
|
+
/** T1.2.6 — rhythm policy snapshot for operator `policy show`. */
|
|
20
|
+
loadPolicy(): Promise<RhythmPolicySnapshot>;
|
|
21
|
+
/**
|
|
22
|
+
* T1.2.7 (SN-CODE-02) — minimal audit read-side view for operator `audit` command.
|
|
23
|
+
* Returns a summary of all in-memory audit events in the default store.
|
|
24
|
+
* Empty store returns `{ totalEvents: 0, events: [] }` (honest empty, not an error).
|
|
25
|
+
*/
|
|
26
|
+
loadAuditSummary(): Promise<AuditSummaryReadModel>;
|
|
16
27
|
}
|
|
17
28
|
/** T1.2.1 / T1.2.2 — operator-facing read surface (subset of full CLI read models). */
|
|
18
29
|
export type OpsReadModelPort = Pick<CliReadModels, "loadStatus" | "loadDailyReport" | "loadQuiet" | "loadSession" | "loadCredential" | "explain" | "loadFallbackView">;
|
|
@@ -10,6 +10,7 @@ import { AppendOnlyAuditStore } from "../../observability/audit/append-only-audi
|
|
|
10
10
|
import { queryExplain, } from "../../observability/query/explain-query.js";
|
|
11
11
|
import { mapOperatorExplainToReadModel } from "./operator-explain-map.js";
|
|
12
12
|
import { loadOperatorFallbackRow, toOperatorFallbackView, } from "../../storage/fallback/load-operator-fallback.js";
|
|
13
|
+
import { loadRhythmPolicySnapshot, } from "../../storage/rhythm/rhythm-policy-snapshot.js";
|
|
13
14
|
const INTERNAL_RUNTIME_PLATFORM_ID = "second-nature-runtime";
|
|
14
15
|
const INTERNAL_RUNTIME_TRACE_PREFIX = "sn-runtime-";
|
|
15
16
|
function toExplainQuery(subject) {
|
|
@@ -92,6 +93,11 @@ function mapRuntimeStatus(attempt) {
|
|
|
92
93
|
if (!attempt) {
|
|
93
94
|
return "unknown";
|
|
94
95
|
}
|
|
96
|
+
// T1.2.9 (SN-CODE-04): control-plane denial (no eligible intent) is NOT a runtime fault.
|
|
97
|
+
// Return `awaiting_sources` so operators do not misread a clean denied cycle as a crash/degraded.
|
|
98
|
+
if (attempt.failureClass === "decision_denied") {
|
|
99
|
+
return "awaiting_sources";
|
|
100
|
+
}
|
|
95
101
|
if (attempt.failureClass || attempt.status === "failed") {
|
|
96
102
|
return "degraded";
|
|
97
103
|
}
|
|
@@ -324,6 +330,26 @@ export function createCliReadModels(deps) {
|
|
|
324
330
|
return null;
|
|
325
331
|
return toOperatorFallbackView(row);
|
|
326
332
|
},
|
|
333
|
+
// T1.2.6 (SN-CODE-01): return the current workspace rhythm policy snapshot so that
|
|
334
|
+
// `policy show` is no longer a notImplemented shell. Returns defaults if no policy row exists.
|
|
335
|
+
async loadPolicy() {
|
|
336
|
+
return loadRhythmPolicySnapshot(deps.stateDb);
|
|
337
|
+
},
|
|
338
|
+
// T1.2.7 (SN-CODE-02): minimal audit read-side for operator `audit` command.
|
|
339
|
+
// Lists all in-memory envelopes with safe redacted fields; empty store returns honest empty.
|
|
340
|
+
async loadAuditSummary() {
|
|
341
|
+
const events = auditStore.list();
|
|
342
|
+
return {
|
|
343
|
+
totalEvents: events.length,
|
|
344
|
+
events: events.map((e) => ({
|
|
345
|
+
eventId: e.eventId,
|
|
346
|
+
family: e.family,
|
|
347
|
+
plane: e.plane,
|
|
348
|
+
createdAt: e.createdAt,
|
|
349
|
+
sensitivity: e.redaction.sensitivity,
|
|
350
|
+
})),
|
|
351
|
+
};
|
|
352
|
+
},
|
|
327
353
|
async explain(subject) {
|
|
328
354
|
const q = toExplainQuery(subject);
|
|
329
355
|
// T1.2.5: auditStore is always non-null (default-injected), so the explain path always
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
export interface RuntimeSummary {
|
|
2
2
|
host: "openclaw-plugin";
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* T1.2.9 (SN-CODE-04): `awaiting_sources` signals that the last runtime cycle was
|
|
5
|
+
* control-plane denied (decision_denied) — no eligible intent found, NOT a delivery
|
|
6
|
+
* or execution fault. Operators must not interpret this as a runtime crash.
|
|
7
|
+
*/
|
|
8
|
+
serviceStatus: "idle" | "running" | "degraded" | "awaiting_sources" | "unknown";
|
|
4
9
|
updatedAt: string;
|
|
5
10
|
}
|
|
6
11
|
export interface RhythmSummary {
|
|
@@ -110,3 +115,15 @@ export interface ExplainReadModel {
|
|
|
110
115
|
warnings?: string[];
|
|
111
116
|
relatedAuditEventIds?: string[];
|
|
112
117
|
}
|
|
118
|
+
/** T1.2.7 (SN-CODE-02) — minimal audit read-side summary for operator `audit` command. */
|
|
119
|
+
export interface AuditEventSummaryEntry {
|
|
120
|
+
eventId: string;
|
|
121
|
+
family: string;
|
|
122
|
+
plane: string;
|
|
123
|
+
createdAt: string;
|
|
124
|
+
sensitivity: string;
|
|
125
|
+
}
|
|
126
|
+
export interface AuditSummaryReadModel {
|
|
127
|
+
totalEvents: number;
|
|
128
|
+
events: AuditEventSummaryEntry[];
|
|
129
|
+
}
|
|
@@ -78,6 +78,16 @@ export interface ExecutionRunner {
|
|
|
78
78
|
export interface ConnectorExecutionPort {
|
|
79
79
|
executeCapability(intent: CapabilityIntent, request: ConnectorRequest): Promise<ConnectorResult<unknown>>;
|
|
80
80
|
}
|
|
81
|
+
export interface ConnectorExecutor {
|
|
82
|
+
executeEffect(input: {
|
|
83
|
+
platformId: string;
|
|
84
|
+
intent: CapabilityIntent;
|
|
85
|
+
payload: Record<string, unknown>;
|
|
86
|
+
decisionId: string;
|
|
87
|
+
intentId: string;
|
|
88
|
+
idempotencyKey: string;
|
|
89
|
+
}): Promise<ConnectorResult<unknown>>;
|
|
90
|
+
}
|
|
81
91
|
export declare function normalizeOutcome(attempt: RawAttempt): ConnectorResult<unknown>;
|
|
82
92
|
export declare function createConnectorContractCore(input: {
|
|
83
93
|
manifestLoader: ConnectorManifestLoader;
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { classifyFailure, ConnectorPolicyError } from "./failure-taxonomy.js";
|
|
3
|
-
export const CHANNEL_TYPES = [
|
|
2
|
+
import { classifyFailure, ConnectorPolicyError, } from "./failure-taxonomy.js";
|
|
3
|
+
export const CHANNEL_TYPES = [
|
|
4
|
+
"api_rest",
|
|
5
|
+
"api_rpc",
|
|
6
|
+
"a2a",
|
|
7
|
+
"mcp",
|
|
8
|
+
"cli",
|
|
9
|
+
"skill",
|
|
10
|
+
"browser",
|
|
11
|
+
];
|
|
4
12
|
export const CAPABILITY_INTENTS = [
|
|
5
13
|
"feed.read",
|
|
6
14
|
"post.publish",
|
|
@@ -40,11 +40,15 @@ export class ConnectorPolicyError extends Error {
|
|
|
40
40
|
}
|
|
41
41
|
function readRetryAfterMs(input) {
|
|
42
42
|
const retryAfterMs = input.retryAfterMs;
|
|
43
|
-
if (typeof retryAfterMs === "number" &&
|
|
43
|
+
if (typeof retryAfterMs === "number" &&
|
|
44
|
+
Number.isFinite(retryAfterMs) &&
|
|
45
|
+
retryAfterMs > 0) {
|
|
44
46
|
return retryAfterMs;
|
|
45
47
|
}
|
|
46
48
|
const retryAfterSeconds = input.retryAfterSeconds;
|
|
47
|
-
if (typeof retryAfterSeconds === "number" &&
|
|
49
|
+
if (typeof retryAfterSeconds === "number" &&
|
|
50
|
+
Number.isFinite(retryAfterSeconds) &&
|
|
51
|
+
retryAfterSeconds > 0) {
|
|
48
52
|
return retryAfterSeconds * 1000;
|
|
49
53
|
}
|
|
50
54
|
return undefined;
|
|
@@ -64,24 +68,56 @@ export function classifyFailure(error) {
|
|
|
64
68
|
const record = error;
|
|
65
69
|
const code = record.code;
|
|
66
70
|
if (typeof code === "string") {
|
|
71
|
+
if (code === "auth_failure")
|
|
72
|
+
return {
|
|
73
|
+
class: "auth_failure",
|
|
74
|
+
retryable: RETRYABLE_BY_CLASS.auth_failure,
|
|
75
|
+
};
|
|
67
76
|
if (code === "verification_required")
|
|
68
|
-
return {
|
|
77
|
+
return {
|
|
78
|
+
class: "verification_required",
|
|
79
|
+
retryable: RETRYABLE_BY_CLASS.verification_required,
|
|
80
|
+
};
|
|
69
81
|
if (code === "credential_expired")
|
|
70
|
-
return {
|
|
82
|
+
return {
|
|
83
|
+
class: "credential_expired",
|
|
84
|
+
retryable: RETRYABLE_BY_CLASS.credential_expired,
|
|
85
|
+
};
|
|
71
86
|
if (code === "cooldown_blocked")
|
|
72
|
-
return {
|
|
87
|
+
return {
|
|
88
|
+
class: "cooldown_blocked",
|
|
89
|
+
retryable: RETRYABLE_BY_CLASS.cooldown_blocked,
|
|
90
|
+
};
|
|
73
91
|
if (code === "idempotency_conflict")
|
|
74
|
-
return {
|
|
92
|
+
return {
|
|
93
|
+
class: "idempotency_conflict",
|
|
94
|
+
retryable: RETRYABLE_BY_CLASS.idempotency_conflict,
|
|
95
|
+
};
|
|
75
96
|
if (code === "concurrency_conflict")
|
|
76
|
-
return {
|
|
97
|
+
return {
|
|
98
|
+
class: "concurrency_conflict",
|
|
99
|
+
retryable: RETRYABLE_BY_CLASS.concurrency_conflict,
|
|
100
|
+
};
|
|
77
101
|
if (code === "protocol_mismatch")
|
|
78
|
-
return {
|
|
102
|
+
return {
|
|
103
|
+
class: "protocol_mismatch",
|
|
104
|
+
retryable: RETRYABLE_BY_CLASS.protocol_mismatch,
|
|
105
|
+
};
|
|
79
106
|
if (code === "semantic_rejection")
|
|
80
|
-
return {
|
|
107
|
+
return {
|
|
108
|
+
class: "semantic_rejection",
|
|
109
|
+
retryable: RETRYABLE_BY_CLASS.semantic_rejection,
|
|
110
|
+
};
|
|
81
111
|
if (code === "transport_failure")
|
|
82
|
-
return {
|
|
112
|
+
return {
|
|
113
|
+
class: "transport_failure",
|
|
114
|
+
retryable: RETRYABLE_BY_CLASS.transport_failure,
|
|
115
|
+
};
|
|
83
116
|
if (code === "permanent_input_error")
|
|
84
|
-
return {
|
|
117
|
+
return {
|
|
118
|
+
class: "permanent_input_error",
|
|
119
|
+
retryable: RETRYABLE_BY_CLASS.permanent_input_error,
|
|
120
|
+
};
|
|
85
121
|
}
|
|
86
122
|
const status = record.status;
|
|
87
123
|
if (status === 429) {
|
|
@@ -92,14 +128,26 @@ export function classifyFailure(error) {
|
|
|
92
128
|
};
|
|
93
129
|
}
|
|
94
130
|
if (status === 401 || status === 403) {
|
|
95
|
-
return {
|
|
131
|
+
return {
|
|
132
|
+
class: "auth_failure",
|
|
133
|
+
retryable: RETRYABLE_BY_CLASS.auth_failure,
|
|
134
|
+
};
|
|
96
135
|
}
|
|
97
136
|
if (status === 400 || status === 404 || status === 422) {
|
|
98
|
-
return {
|
|
137
|
+
return {
|
|
138
|
+
class: "permanent_input_error",
|
|
139
|
+
retryable: RETRYABLE_BY_CLASS.permanent_input_error,
|
|
140
|
+
};
|
|
99
141
|
}
|
|
100
142
|
if (status === 500 || status === 502 || status === 503 || status === 504) {
|
|
101
|
-
return {
|
|
143
|
+
return {
|
|
144
|
+
class: "transport_failure",
|
|
145
|
+
retryable: RETRYABLE_BY_CLASS.transport_failure,
|
|
146
|
+
};
|
|
102
147
|
}
|
|
103
148
|
}
|
|
104
|
-
return {
|
|
149
|
+
return {
|
|
150
|
+
class: "unknown_platform_change",
|
|
151
|
+
retryable: RETRYABLE_BY_CLASS.unknown_platform_change,
|
|
152
|
+
};
|
|
105
153
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter: assemble connector-system execution infrastructure into the
|
|
3
|
+
* ConnectorExecutor interface consumed by EffectDispatcher.
|
|
4
|
+
*
|
|
5
|
+
* When credentials / base URLs are missing, returns an honest
|
|
6
|
+
* terminal_failure instead of throwing so the heartbeat loop stays stable.
|
|
7
|
+
*/
|
|
8
|
+
import type { ConnectorExecutor } from "../base/contract.js";
|
|
9
|
+
export type { ConnectorExecutor } from "../base/contract.js";
|
|
10
|
+
import type { ObservabilityDatabase } from "../../observability/db/index.js";
|
|
11
|
+
import type { StateDatabase } from "../../storage/db/index.js";
|
|
12
|
+
export interface ConnectorExecutorAdapterOptions {
|
|
13
|
+
stateDb: StateDatabase;
|
|
14
|
+
observabilityDb: ObservabilityDatabase;
|
|
15
|
+
}
|
|
16
|
+
export declare function createConnectorExecutorAdapter(options: ConnectorExecutorAdapterOptions): ConnectorExecutor;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { CapabilityContractRegistry } from "../base/manifest.js";
|
|
2
|
+
import { ConnectorRoutePlanner } from "../base/route-planner.js";
|
|
3
|
+
import { ChannelHealthStore } from "../base/channel-health.js";
|
|
4
|
+
import { createConnectorPolicyLayer } from "../base/policy-layer.js";
|
|
5
|
+
import { InMemoryEffectCommitLedger } from "../base/execution-policy.js";
|
|
6
|
+
import { moltbookManifest } from "../social-community/moltbook/manifest.js";
|
|
7
|
+
import { evomapManifest } from "../agent-network/evomap/manifest.js";
|
|
8
|
+
import { createMoltbookApiClient } from "../social-community/moltbook/api-client.js";
|
|
9
|
+
import { createMoltbookRunner } from "../social-community/moltbook/adapter.js";
|
|
10
|
+
import { ExecutionTelemetry } from "../../observability/services/execution-telemetry.js";
|
|
11
|
+
import { createCredentialVault } from "../../storage/services/credential-vault.js";
|
|
12
|
+
import { createCredentialRouteContextPort } from "./credential-route-context.js";
|
|
13
|
+
function createAdaptiveExecutionRunner(vault) {
|
|
14
|
+
return {
|
|
15
|
+
async run(_plan, request) {
|
|
16
|
+
const platformId = request.platformId;
|
|
17
|
+
const started = Date.now();
|
|
18
|
+
const credential = await vault.loadCredentialContext(platformId);
|
|
19
|
+
if (!credential ||
|
|
20
|
+
credential.status !== "active" ||
|
|
21
|
+
!credential.encryptedValue) {
|
|
22
|
+
return {
|
|
23
|
+
platformId,
|
|
24
|
+
channel: request.preferredChannel ?? "api_rest",
|
|
25
|
+
latencyMs: Date.now() - started,
|
|
26
|
+
success: false,
|
|
27
|
+
error: {
|
|
28
|
+
code: "auth_failure",
|
|
29
|
+
detail: "credential_unavailable_for_execution",
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (platformId === "moltbook") {
|
|
34
|
+
const baseUrl = process.env.SECOND_NATURE_MOLTBOOK_BASE_URL;
|
|
35
|
+
if (!baseUrl) {
|
|
36
|
+
return {
|
|
37
|
+
platformId,
|
|
38
|
+
channel: request.preferredChannel ?? "api_rest",
|
|
39
|
+
latencyMs: Date.now() - started,
|
|
40
|
+
success: false,
|
|
41
|
+
error: {
|
|
42
|
+
code: "configuration_missing",
|
|
43
|
+
detail: "SECOND_NATURE_MOLTBOOK_BASE_URL not set",
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const apiClient = createMoltbookApiClient({
|
|
48
|
+
baseUrl,
|
|
49
|
+
accessToken: credential.encryptedValue,
|
|
50
|
+
timeoutMs: 10000,
|
|
51
|
+
});
|
|
52
|
+
const runner = createMoltbookRunner({
|
|
53
|
+
apiClient,
|
|
54
|
+
skillRunner: {
|
|
55
|
+
run: async () => {
|
|
56
|
+
throw {
|
|
57
|
+
code: "protocol_mismatch",
|
|
58
|
+
detail: "moltbook_skill_runner_not_configured",
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
return runner.run(_plan, request);
|
|
64
|
+
}
|
|
65
|
+
if (platformId === "evomap") {
|
|
66
|
+
return {
|
|
67
|
+
platformId,
|
|
68
|
+
channel: request.preferredChannel ?? "api_rest",
|
|
69
|
+
latencyMs: Date.now() - started,
|
|
70
|
+
success: false,
|
|
71
|
+
error: {
|
|
72
|
+
code: "not_implemented",
|
|
73
|
+
detail: "evomap_execution_runner_not_yet_implemented",
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
platformId,
|
|
79
|
+
channel: request.preferredChannel ?? "api_rest",
|
|
80
|
+
latencyMs: Date.now() - started,
|
|
81
|
+
success: false,
|
|
82
|
+
error: {
|
|
83
|
+
code: "unknown_platform",
|
|
84
|
+
detail: `no execution runner for ${platformId}`,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function createConnectorExecutorAdapter(options) {
|
|
91
|
+
const vault = createCredentialVault(options.stateDb.db);
|
|
92
|
+
const registry = new CapabilityContractRegistry();
|
|
93
|
+
registry.register({ ...moltbookManifest });
|
|
94
|
+
registry.register({ ...evomapManifest });
|
|
95
|
+
const routeContextPort = createCredentialRouteContextPort(vault);
|
|
96
|
+
const routePlanner = new ConnectorRoutePlanner(registry, routeContextPort, new ChannelHealthStore());
|
|
97
|
+
const telemetry = new ExecutionTelemetry(options.observabilityDb);
|
|
98
|
+
const executionRunner = createAdaptiveExecutionRunner(vault);
|
|
99
|
+
const policy = createConnectorPolicyLayer({
|
|
100
|
+
routePlanner,
|
|
101
|
+
executionRunner,
|
|
102
|
+
telemetry,
|
|
103
|
+
effectCommitLedger: new InMemoryEffectCommitLedger(),
|
|
104
|
+
retryPolicy: { maxRetries: 2, jitter: true },
|
|
105
|
+
});
|
|
106
|
+
return {
|
|
107
|
+
async executeEffect(input) {
|
|
108
|
+
return policy.executeWithPolicy(input.intent, {
|
|
109
|
+
platformId: input.platformId,
|
|
110
|
+
intent: input.intent,
|
|
111
|
+
payload: input.payload,
|
|
112
|
+
decisionId: input.decisionId,
|
|
113
|
+
intentId: input.intentId,
|
|
114
|
+
idempotencyKey: input.idempotencyKey,
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge CredentialVault → RouteContextPort for connector route planning.
|
|
3
|
+
*
|
|
4
|
+
* Loads decrypted credentials from state DB and maps them to the
|
|
5
|
+
* CredentialContext shape expected by ConnectorRoutePlanner.
|
|
6
|
+
* Cooldown is stubbed (always unblocked) until a cooldown ledger is modeled.
|
|
7
|
+
*/
|
|
8
|
+
import type { RouteContextPort } from "../base/contract.js";
|
|
9
|
+
import type { CredentialVault } from "../../storage/services/credential-vault.js";
|
|
10
|
+
export declare function createCredentialRouteContextPort(vault: CredentialVault): RouteContextPort;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function createCredentialRouteContextPort(vault) {
|
|
2
|
+
return {
|
|
3
|
+
async loadCredentialState(platformId) {
|
|
4
|
+
const ctx = await vault.loadCredentialContext(platformId);
|
|
5
|
+
// Defensive: some ORM findFirst variants return {} instead of null/undefined.
|
|
6
|
+
if (!ctx || !ctx.platformId || !ctx.status) {
|
|
7
|
+
return {
|
|
8
|
+
platformId,
|
|
9
|
+
status: "missing",
|
|
10
|
+
credentialType: "api_key",
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
return ctx;
|
|
14
|
+
},
|
|
15
|
+
async loadCooldownState() {
|
|
16
|
+
return { blocked: false };
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -19,6 +19,7 @@ import { type HeartbeatRuntimeSnapshot } from "./runtime-snapshot.js";
|
|
|
19
19
|
import type { GuidanceDraftPort } from "../../../guidance/outreach-draft-schema.js";
|
|
20
20
|
import type { StateDatabase } from "../../../storage/db/index.js";
|
|
21
21
|
import { type OpenClawDeliveryPort } from "../outreach/dispatch-user-outreach.js";
|
|
22
|
+
import type { ConnectorExecutor } from "../../../connectors/base/contract.js";
|
|
22
23
|
export interface HeartbeatDecisionTracePayload {
|
|
23
24
|
scope: RuntimeScope;
|
|
24
25
|
status: HeartbeatCycleStatus;
|
|
@@ -44,7 +45,7 @@ export interface HeartbeatQuietWorkflowDeps {
|
|
|
44
45
|
* Resolves the heartbeat outcome for a guard-allowed intent (outreach dispatch, quiet orchestration, or default).
|
|
45
46
|
* Exported for unit tests (CR-M1 wiring).
|
|
46
47
|
*/
|
|
47
|
-
export declare function resolveAllowedIntentResult(intent: CandidateIntent, runtime: HeartbeatRuntimeSnapshot, inputs: SnapshotInputs, signal: HeartbeatSignal, deps: Pick<HeartbeatDeps, "outreachDispatch" | "quietWorkflow">): Promise<HeartbeatCycleResult>;
|
|
48
|
+
export declare function resolveAllowedIntentResult(intent: CandidateIntent, runtime: HeartbeatRuntimeSnapshot, inputs: SnapshotInputs, signal: HeartbeatSignal, deps: Pick<HeartbeatDeps, "outreachDispatch" | "quietWorkflow" | "connectorExecutor">): Promise<HeartbeatCycleResult>;
|
|
48
49
|
export interface HeartbeatDeps {
|
|
49
50
|
/** Load snapshot inputs from state-system */
|
|
50
51
|
loadSnapshotInputs: () => Promise<SnapshotInputs>;
|
|
@@ -52,6 +53,11 @@ export interface HeartbeatDeps {
|
|
|
52
53
|
recordDecisionTrace?: (payload: HeartbeatDecisionTracePayload) => Promise<void>;
|
|
53
54
|
outreachDispatch?: HeartbeatOutreachDispatchDeps;
|
|
54
55
|
quietWorkflow?: HeartbeatQuietWorkflowDeps;
|
|
56
|
+
/**
|
|
57
|
+
* When present, guard-allowed connector_action intents are dispatched
|
|
58
|
+
* through the connector-system instead of returning connector_dispatch_unwired.
|
|
59
|
+
*/
|
|
60
|
+
connectorExecutor?: ConnectorExecutor;
|
|
55
61
|
}
|
|
56
62
|
/**
|
|
57
63
|
* Ingest a heartbeat rhythm signal and drive one full decision round.
|
|
@@ -5,6 +5,7 @@ import { evaluateHardGuards } from "../orchestrator/guard-layer.js";
|
|
|
5
5
|
import { dispatchUserOutreachIntent, } from "../outreach/dispatch-user-outreach.js";
|
|
6
6
|
import { buildJudgeOutreachInputFromSnapshot } from "../outreach/judge-input-from-snapshot.js";
|
|
7
7
|
import { runSourceBackedQuiet } from "../quiet/run-source-backed-quiet.js";
|
|
8
|
+
import { toCapabilityIntent } from "../orchestrator/effect-dispatcher.js";
|
|
8
9
|
/**
|
|
9
10
|
* Resolves the heartbeat outcome for a guard-allowed intent (outreach dispatch, quiet orchestration, or default).
|
|
10
11
|
* Exported for unit tests (CR-M1 wiring).
|
|
@@ -48,6 +49,28 @@ export async function resolveAllowedIntentResult(intent, runtime, inputs, signal
|
|
|
48
49
|
intent.effectClass === "no_effect" ||
|
|
49
50
|
intent.kind === "maintenance";
|
|
50
51
|
const connectorUnwired = intent.effectClass === "connector_action";
|
|
52
|
+
if (connectorUnwired && deps.connectorExecutor) {
|
|
53
|
+
const result = await deps.connectorExecutor.executeEffect({
|
|
54
|
+
platformId: intent.platformId ?? "unknown",
|
|
55
|
+
intent: toCapabilityIntent(intent),
|
|
56
|
+
payload: {},
|
|
57
|
+
decisionId: `decision:${intent.id}:${Date.now()}`,
|
|
58
|
+
intentId: intent.id,
|
|
59
|
+
idempotencyKey: `idem:${intent.id}:${Date.now()}`,
|
|
60
|
+
});
|
|
61
|
+
const base = {
|
|
62
|
+
scope: "rhythm",
|
|
63
|
+
status: "intent_selected",
|
|
64
|
+
selectedIntentId: intent.id,
|
|
65
|
+
decisionId: `decision:${intent.id}:${Date.now()}`,
|
|
66
|
+
reasons: result.status === "success"
|
|
67
|
+
? ["connector_effect_executed"]
|
|
68
|
+
: result.status === "retryable_failure"
|
|
69
|
+
? ["connector_retryable_failure", result.failureClass ?? "unknown"]
|
|
70
|
+
: ["connector_terminal_failure", result.failureClass ?? "unknown"],
|
|
71
|
+
};
|
|
72
|
+
return base;
|
|
73
|
+
}
|
|
51
74
|
const reasons = noExternalEffect
|
|
52
75
|
? ["internal_tick"]
|
|
53
76
|
: connectorUnwired
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { ConnectorResult, CapabilityIntent } from "../../../connectors/base/contract.js";
|
|
1
|
+
import type { ConnectorResult, CapabilityIntent, ConnectorExecutor } from "../../../connectors/base/contract.js";
|
|
2
|
+
export type { ConnectorExecutor } from "../../../connectors/base/contract.js";
|
|
2
3
|
import { LeaseManager, type EffectClass } from "./lease-manager.js";
|
|
3
4
|
export interface AllowedIntent {
|
|
4
5
|
id: string;
|
|
@@ -31,16 +32,6 @@ export interface IntentCommitPort {
|
|
|
31
32
|
}): Promise<void>;
|
|
32
33
|
abortIntentCommit(id: string, reason: string): Promise<void>;
|
|
33
34
|
}
|
|
34
|
-
export interface ConnectorExecutor {
|
|
35
|
-
executeEffect(input: {
|
|
36
|
-
platformId: string;
|
|
37
|
-
intent: CapabilityIntent;
|
|
38
|
-
payload: Record<string, unknown>;
|
|
39
|
-
decisionId: string;
|
|
40
|
-
intentId: string;
|
|
41
|
-
idempotencyKey: string;
|
|
42
|
-
}): Promise<ConnectorResult<unknown>>;
|
|
43
|
-
}
|
|
44
35
|
export interface CheckpointPort {
|
|
45
36
|
saveCheckpoint(input: {
|
|
46
37
|
id: string;
|
|
@@ -83,6 +74,7 @@ export type DispatchResult = {
|
|
|
83
74
|
status: "maintenance_done";
|
|
84
75
|
commitId: string;
|
|
85
76
|
};
|
|
77
|
+
export declare function toCapabilityIntent(intent: Pick<AllowedIntent, "kind">): CapabilityIntent;
|
|
86
78
|
export declare class EffectDispatcher {
|
|
87
79
|
private readonly leaseManager;
|
|
88
80
|
private readonly commitPort;
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import * as crypto from "crypto";
|
|
2
2
|
function needsLease(effectClass) {
|
|
3
|
-
return effectClass === "external_platform_action" ||
|
|
3
|
+
return (effectClass === "external_platform_action" ||
|
|
4
|
+
effectClass === "connector_action" ||
|
|
5
|
+
effectClass === "user_outreach");
|
|
4
6
|
}
|
|
5
7
|
function needsCheckpoint(effectClass) {
|
|
6
8
|
return effectClass !== "maintenance" && effectClass !== "no_effect";
|
|
7
9
|
}
|
|
8
10
|
function isConnectorEffect(effectClass) {
|
|
9
|
-
return effectClass === "external_platform_action" ||
|
|
11
|
+
return (effectClass === "external_platform_action" ||
|
|
12
|
+
effectClass === "connector_action");
|
|
10
13
|
}
|
|
11
|
-
function toCapabilityIntent(intent) {
|
|
14
|
+
export function toCapabilityIntent(intent) {
|
|
12
15
|
if (intent.kind === "work")
|
|
13
16
|
return "work.discover";
|
|
14
17
|
if (intent.kind === "exploration")
|
|
@@ -48,7 +51,9 @@ export class EffectDispatcher {
|
|
|
48
51
|
id: decision.checkpointId,
|
|
49
52
|
tickId: decision.tickId,
|
|
50
53
|
intentId: decision.intentId,
|
|
51
|
-
phase: isConnectorEffect(intent.effectClass)
|
|
54
|
+
phase: isConnectorEffect(intent.effectClass)
|
|
55
|
+
? "before_effect"
|
|
56
|
+
: "before_quiet_write",
|
|
52
57
|
snapshotRef: decision.traceId,
|
|
53
58
|
});
|
|
54
59
|
}
|
|
@@ -27,7 +27,7 @@ export function startRuntimeService(ctx) {
|
|
|
27
27
|
// - control-plane-system (heartbeat bridge preparation)
|
|
28
28
|
const workspaceRoot = ctx?.workspaceRoot ?? process.cwd();
|
|
29
29
|
/** Keep in sync with `plugin/package.json` when cutting releases. */
|
|
30
|
-
const version = "0.1.
|
|
30
|
+
const version = "0.1.20";
|
|
31
31
|
activeHandle = {
|
|
32
32
|
ready: true,
|
|
33
33
|
version,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ObservabilityDatabase } from "../db/index.js";
|
|
2
|
+
export interface RetentionCleanupInput {
|
|
3
|
+
/** Delete rows with createdAt < this ISO string. */
|
|
4
|
+
beforeDate: string;
|
|
5
|
+
}
|
|
6
|
+
export interface RetentionCleanupResult {
|
|
7
|
+
decisionLedgerDeleted: number;
|
|
8
|
+
executionAttemptsDeleted: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function pruneObservabilityTables(db: ObservabilityDatabase, input: RetentionCleanupInput): Promise<RetentionCleanupResult>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observability retention cleanup (P2-06).
|
|
3
|
+
*
|
|
4
|
+
* Core logic: delete rows older than a retention threshold from
|
|
5
|
+
* decision_ledger and execution_attempts. Host capability reports and
|
|
6
|
+
* governance audit are intentionally kept longer (they are rare and
|
|
7
|
+
* operator-relevant).
|
|
8
|
+
*
|
|
9
|
+
* Boundaries:
|
|
10
|
+
* - Does NOT vacuum the SQLite file (callers may do so separately).
|
|
11
|
+
* - Returns honest counts so operators can verify.
|
|
12
|
+
* - Safe to run while the system is active (SQLite DELETE is row-level).
|
|
13
|
+
*/
|
|
14
|
+
import { lt, sql } from "drizzle-orm";
|
|
15
|
+
import { decisionLedger, executionAttempts } from "../db/schema/index.js";
|
|
16
|
+
export async function pruneObservabilityTables(db, input) {
|
|
17
|
+
// Count before delete so we can return honest deletion numbers
|
|
18
|
+
// (SQLite DELETE result does not expose changes in Drizzle's type).
|
|
19
|
+
const dlBefore = await db.db
|
|
20
|
+
.select({ count: sql `count(*)` })
|
|
21
|
+
.from(decisionLedger)
|
|
22
|
+
.where(lt(decisionLedger.createdAt, input.beforeDate));
|
|
23
|
+
const eaBefore = await db.db
|
|
24
|
+
.select({ count: sql `count(*)` })
|
|
25
|
+
.from(executionAttempts)
|
|
26
|
+
.where(lt(executionAttempts.startedAt, input.beforeDate));
|
|
27
|
+
await db.db
|
|
28
|
+
.delete(decisionLedger)
|
|
29
|
+
.where(lt(decisionLedger.createdAt, input.beforeDate));
|
|
30
|
+
await db.db
|
|
31
|
+
.delete(executionAttempts)
|
|
32
|
+
.where(lt(executionAttempts.startedAt, input.beforeDate));
|
|
33
|
+
return {
|
|
34
|
+
decisionLedgerDeleted: dlBefore[0]?.count ?? 0,
|
|
35
|
+
executionAttemptsDeleted: eaBefore[0]?.count ?? 0,
|
|
36
|
+
};
|
|
37
|
+
}
|
package/workspace-ops-bridge.js
CHANGED
|
@@ -22,7 +22,9 @@ export async function openWorkspaceOpsBridge(workspaceRoot) {
|
|
|
22
22
|
try {
|
|
23
23
|
const pluginPackageRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
24
24
|
// Packaged `plugin/runtime` is emitted JS without sibling `.d.ts` in this repo layout.
|
|
25
|
-
//
|
|
25
|
+
// Dynamic import of artifact bundle — typed via PackagedCliModule interface above.
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
27
|
+
// @ts-ignore TS7016 — intentional: runtime artifact has no adjacent .d.ts in this layout
|
|
26
28
|
const cliIndex = (await import("./runtime/cli/index.js"));
|
|
27
29
|
const commandsMod = (await import("./runtime/cli/commands/index.js"));
|
|
28
30
|
const storageDb = (await import("./runtime/storage/db/index.js"));
|
|
@@ -40,6 +42,10 @@ export async function openWorkspaceOpsBridge(workspaceRoot) {
|
|
|
40
42
|
runtimeAvailable: runtimeResolved.ok,
|
|
41
43
|
readModels: deps.readModels,
|
|
42
44
|
runtimeRecorder: deps.runtimeRecorder,
|
|
45
|
+
// T1.2.8 (SN-CODE-03): pass observabilityDb so capability_probe can persist reports
|
|
46
|
+
observabilityDb,
|
|
47
|
+
state: stateDb,
|
|
48
|
+
workspaceRoot: resolvedRoot,
|
|
43
49
|
});
|
|
44
50
|
const commands = commandsMod.createCliCommands({
|
|
45
51
|
readModels: deps.readModels,
|
|
@@ -51,7 +57,10 @@ export async function openWorkspaceOpsBridge(workspaceRoot) {
|
|
|
51
57
|
if (!def) {
|
|
52
58
|
return {
|
|
53
59
|
ok: false,
|
|
54
|
-
error: {
|
|
60
|
+
error: {
|
|
61
|
+
code: "unknown_command",
|
|
62
|
+
message: `Unknown Second Nature command: ${command}`,
|
|
63
|
+
},
|
|
55
64
|
};
|
|
56
65
|
}
|
|
57
66
|
const prevCwd = process.cwd();
|