@haaaiawd/second-nature 0.2.9 → 0.2.12

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.
Files changed (84) hide show
  1. package/index.js +6 -0
  2. package/openclaw.plugin.json +2 -5
  3. package/package.json +1 -1
  4. package/runtime/cli/host-capability/probe-host-capability.js +1 -1
  5. package/runtime/cli/host-capability/types.d.ts +2 -7
  6. package/runtime/cli/host-capability/types.js +0 -5
  7. package/runtime/cli/ops/heartbeat-surface.d.ts +2 -0
  8. package/runtime/cli/ops/heartbeat-surface.js +32 -3
  9. package/runtime/cli/ops/ops-router.js +3 -3
  10. package/runtime/cli/ops/workspace-heartbeat-runner.js +2 -5
  11. package/runtime/connectors/manifest/manifest-schema.d.ts +2 -2
  12. package/runtime/connectors/registry/dynamic-connector-registry.js +16 -1
  13. package/runtime/connectors/services/connector-executor-adapter.js +54 -35
  14. package/runtime/core/second-nature/action/action-closure-recorder.d.ts +2 -0
  15. package/runtime/core/second-nature/action/action-closure-recorder.js +2 -4
  16. package/runtime/core/second-nature/action/action-proposal-builder.js +5 -32
  17. package/runtime/core/second-nature/action/policy-bound-dispatch.js +4 -3
  18. package/runtime/core/second-nature/control-plane/accepted-projection-loader.js +1 -11
  19. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.d.ts +1 -0
  20. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.js +10 -6
  21. package/runtime/core/second-nature/control-plane/real-runtime-spine.d.ts +2 -0
  22. package/runtime/core/second-nature/control-plane/real-runtime-spine.js +1 -0
  23. package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +4 -3
  24. package/runtime/core/second-nature/heartbeat/runtime-snapshot.d.ts +3 -2
  25. package/runtime/core/second-nature/heartbeat/snapshot-builder.d.ts +3 -2
  26. package/runtime/core/second-nature/orchestrator/intent-planner.js +4 -2
  27. package/runtime/core/second-nature/orchestrator/narrative-update.js +1 -2
  28. package/runtime/core/second-nature/orchestrator/platform-capability-router.d.ts +2 -2
  29. package/runtime/core/second-nature/orchestrator/platform-capability-router.js +1 -1
  30. package/runtime/core/second-nature/outreach/build-outreach-draft-request.js +2 -3
  31. package/runtime/core/second-nature/outreach/dispatch-user-outreach.d.ts +2 -2
  32. package/runtime/core/second-nature/outreach/dispatch-user-outreach.js +6 -1
  33. package/runtime/core/second-nature/outreach/judge-input-from-snapshot.js +3 -14
  34. package/runtime/core/second-nature/outreach/judge-outreach.d.ts +6 -5
  35. package/runtime/core/second-nature/perception/judgment-engine.js +2 -12
  36. package/runtime/core/second-nature/perception/perception-builder.js +1 -9
  37. package/runtime/core/second-nature/quiet/run-source-backed-quiet.js +13 -15
  38. package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.js +10 -13
  39. package/runtime/core/second-nature/quiet-dream/dream-scheduler.js +0 -2
  40. package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.js +0 -12
  41. package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.js +1 -11
  42. package/runtime/core/second-nature/types.d.ts +2 -9
  43. package/runtime/dream/dream-engine.js +11 -4
  44. package/runtime/guidance/outreach-draft-schema.d.ts +12 -12
  45. package/runtime/guidance/persona-selection.js +5 -0
  46. package/runtime/guidance/template-registry.js +6 -1
  47. package/runtime/guidance/types.d.ts +2 -2
  48. package/runtime/observability/living-loop-health-gate.js +2 -8
  49. package/runtime/observability/loop-stage-event-sink.js +0 -1
  50. package/runtime/observability/services/lived-experience-audit.d.ts +7 -7
  51. package/runtime/shared/serialization.d.ts +17 -0
  52. package/runtime/shared/serialization.js +27 -0
  53. package/runtime/shared/source-ref-compat.d.ts +26 -0
  54. package/runtime/shared/source-ref-compat.js +61 -0
  55. package/runtime/shared/types/goal.d.ts +4 -4
  56. package/runtime/shared/types/goal.js +1 -1
  57. package/runtime/shared/types/index.d.ts +1 -0
  58. package/runtime/shared/types/index.js +1 -3
  59. package/runtime/shared/types/source-ref.d.ts +2 -2
  60. package/runtime/shared/types/v7-entities.d.ts +5 -5
  61. package/runtime/shared/types/v7-entities.js +1 -1
  62. package/runtime/shared/types/v8-contracts.d.ts +1 -1
  63. package/runtime/storage/db/index.js +31 -26
  64. package/runtime/storage/db/migrations/index.js +4 -0
  65. package/runtime/storage/db/migrations/v8-004-schema-closure.d.ts +19 -0
  66. package/runtime/storage/db/migrations/v8-004-schema-closure.js +74 -0
  67. package/runtime/storage/db/migrations/v8-005-single-status-schema.d.ts +11 -0
  68. package/runtime/storage/db/migrations/v8-005-single-status-schema.js +16 -0
  69. package/runtime/storage/db/schema/v8-entities.d.ts +0 -95
  70. package/runtime/storage/db/schema/v8-entities.js +4 -7
  71. package/runtime/storage/delivery/types.d.ts +2 -2
  72. package/runtime/storage/fallback/load-operator-fallback.d.ts +2 -2
  73. package/runtime/storage/fallback/operator-fallback-types.d.ts +2 -2
  74. package/runtime/storage/fallback/operator-fallback-view.d.ts +2 -2
  75. package/runtime/storage/index.d.ts +1 -1
  76. package/runtime/storage/life-evidence/types.d.ts +5 -5
  77. package/runtime/storage/quiet/quiet-artifact-types.d.ts +4 -4
  78. package/runtime/storage/quiet/quiet-artifact-writer.d.ts +2 -2
  79. package/runtime/storage/services/write-validation-gate.d.ts +1 -1
  80. package/runtime/storage/services/write-validation-gate.js +16 -4
  81. package/runtime/storage/snapshots/types.d.ts +8 -8
  82. package/runtime/storage/user-interest/types.d.ts +3 -3
  83. package/runtime/storage/v8-state-stores.d.ts +8 -1
  84. package/runtime/storage/v8-state-stores.js +23 -20
package/index.js CHANGED
@@ -31,6 +31,12 @@
31
31
  * illusion of a working plugin while agent sessions see no tool. We hit
32
32
  * exactly that on 2026-05-06; the fix lives in plugin/openclaw.plugin.json
33
33
  * under the `activation` block.
34
+ * - Cloud Feishu/OpenClaw sessions can report `capabilities=none`. In that
35
+ * mode, binding activation to `onCapabilities:["tool"]` prevents the agent
36
+ * tool from entering the session even though the plugin loaded. Therefore
37
+ * the manifest keeps `activation.onStartup === true` for daemon loading and
38
+ * declares the tool through `contracts.tools`, but does not require a session
39
+ * capability to activate `second_nature_ops`.
34
40
  * - `Shape: non-capability` reported by `openclaw plugins info` is EXPECTED
35
41
  * for this plugin. OpenClaw counts capabilities only across cli-backend /
36
42
  * text-inference / speech / realtime-* / media-understanding /
@@ -1,13 +1,10 @@
1
1
  {
2
2
  "id": "second-nature",
3
3
  "name": "Second Nature",
4
- "version": "0.2.9",
4
+ "version": "0.2.12",
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. 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
- "onStartup": true,
8
- "onCapabilities": [
9
- "tool"
10
- ]
7
+ "onStartup": true
11
8
  },
12
9
  "contracts": {
13
10
  "commands": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haaaiawd/second-nature",
3
- "version": "0.2.9",
3
+ "version": "0.2.12",
4
4
  "description": "OpenClaw native plugin with synchronous registration, a packaged runtime artifact, and operator-facing status/explain flows.",
5
5
  "keywords": [
6
6
  "openclaw",
@@ -7,7 +7,7 @@ function mergeEvidenceRefs(...groups) {
7
7
  const out = [];
8
8
  for (const group of groups) {
9
9
  for (const ref of group) {
10
- const key = `${ref.kind}:${ref.id}`;
10
+ const key = `${ref.family}:${ref.id}`;
11
11
  if (!seen.has(key)) {
12
12
  seen.add(key);
13
13
  out.push(ref);
@@ -3,15 +3,10 @@
3
3
  *
4
4
  * Test coverage: tests/unit/cli/host-capability.test.ts, tests/integration/cli/host-capability-probe.test.ts
5
5
  */
6
+ import type { SourceRef } from "../../shared/types/v8-contracts.js";
7
+ export type { SourceRef } from "../../shared/types/v8-contracts.js";
6
8
  export type DeliveryCapabilityStatus = "target_available" | "target_none" | "channel_missing" | "host_api_unavailable" | "host_unsupported" | "unknown";
7
9
  export type CapabilityVerdict = "pass" | "fail" | "unknown" | "not_applicable";
8
- export interface SourceRef {
9
- id: string;
10
- kind: "platform_item" | "workspace_artifact" | "decision_record" | "user_anchor" | "connector_result" | "host_report" | "fallback_artifact";
11
- uri: string;
12
- excerptHash?: string;
13
- observedAt?: string;
14
- }
15
10
  export interface HostCapabilityDocReference {
16
11
  title: string;
17
12
  url: string;
@@ -1,6 +1 @@
1
- /**
2
- * Host capability probe contracts (cli-system v5 / ADR-007).
3
- *
4
- * Test coverage: tests/unit/cli/host-capability.test.ts, tests/integration/cli/host-capability-probe.test.ts
5
- */
6
1
  export {};
@@ -45,6 +45,8 @@ export interface HeartbeatSurfaceResult {
45
45
  capabilityClass?: string | null;
46
46
  impulseText?: string | null;
47
47
  atmosphereText?: string | null;
48
+ expressionBoundaryConstraints?: string[] | null;
49
+ expressionBoundaryStyle?: string | null;
48
50
  freshnessMs?: number;
49
51
  missingReason?: string;
50
52
  };
@@ -1,6 +1,23 @@
1
1
  import { createWorkspaceHeartbeatRunner } from "./workspace-heartbeat-runner.js";
2
2
  // T-CP.R.2: v8 real runtime spine bridge
3
3
  import { runRealRuntimeHeartbeatCycle, } from "../../core/second-nature/control-plane/real-runtime-spine.js";
4
+ async function refreshHeartbeatImpulseContext(state, now) {
5
+ const { assembleImpulseSync } = await import("../../guidance/impulse-assembler.js");
6
+ const { buildExpressionBoundary } = await import("../../guidance/output-guard.js");
7
+ const { getShortAtmosphereTemplate } = await import("../../guidance/template-registry.js");
8
+ const { writeImpulseContext } = await import("../../core/second-nature/guidance/impulse-context-writer.js");
9
+ const impulseResult = assembleImpulseSync({ sceneType: "heartbeat" });
10
+ const atmosphere = getShortAtmosphereTemplate("active", "low");
11
+ const expressionBoundary = buildExpressionBoundary("heartbeat");
12
+ const result = await writeImpulseContext(state, {
13
+ sceneType: "heartbeat",
14
+ impulseResult,
15
+ atmosphereText: atmosphere.text,
16
+ expressionBoundaryConstraints: expressionBoundary.constraints,
17
+ expressionBoundaryStyle: expressionBoundary.style,
18
+ }, { now });
19
+ return "id" in result ? result.id : undefined;
20
+ }
4
21
  function mapCycleToSurface(cycle, surfaceMode) {
5
22
  const status = cycle.status === "runtime_carrier_only"
6
23
  ? "runtime_carrier_only"
@@ -111,6 +128,11 @@ export async function heartbeatCheck(input) {
111
128
  }
112
129
  else {
113
130
  const spine = v8Result;
131
+ const artifactId = await refreshHeartbeatImpulseContext(input.state, timestamp);
132
+ if (artifactId) {
133
+ spine.impulseContextArtifactId = artifactId;
134
+ surfaceResult.reasons.push(`impulse_context_refreshed:${artifactId}`);
135
+ }
114
136
  surfaceResult.v8Spine = spine;
115
137
  surfaceResult.livedExperienceLoopClaimed = Boolean(spine.cycleId && (spine.closureRef || spine.noActionReason));
116
138
  surfaceResult.reasons = [
@@ -134,7 +156,7 @@ export async function heartbeatCheck(input) {
134
156
  if (input.state) {
135
157
  try {
136
158
  const { readImpulseContext } = await import("../../core/second-nature/guidance/impulse-context-reader.js");
137
- const ctx = await readImpulseContext(input.state, "social");
159
+ const ctx = await readImpulseContext(input.state, "heartbeat");
138
160
  if (ctx.available) {
139
161
  surfaceResult.impulseContext = {
140
162
  available: true,
@@ -142,6 +164,8 @@ export async function heartbeatCheck(input) {
142
164
  capabilityClass: ctx.artifact.capabilityClass,
143
165
  impulseText: ctx.artifact.impulseText,
144
166
  atmosphereText: ctx.artifact.atmosphereText,
167
+ expressionBoundaryConstraints: ctx.artifact.expressionBoundaryConstraints,
168
+ expressionBoundaryStyle: ctx.artifact.expressionBoundaryStyle,
145
169
  freshnessMs: ctx.freshnessMs,
146
170
  };
147
171
  surfaceResult.reasons.push(`impulse_context:${ctx.artifact.id}`);
@@ -154,8 +178,13 @@ export async function heartbeatCheck(input) {
154
178
  surfaceResult.reasons.push(`impulse_context_missing:${ctx.reason}`);
155
179
  }
156
180
  }
157
- catch {
158
- // Non-fatal: impulse context is advisory
181
+ catch (readErr) {
182
+ const msg = readErr instanceof Error ? readErr.message : String(readErr);
183
+ surfaceResult.impulseContext = {
184
+ available: false,
185
+ missingReason: `read_exception:${msg.slice(0, 120)}`,
186
+ };
187
+ surfaceResult.reasons.push(`impulse_context_read_failed:${msg.slice(0, 120)}`);
159
188
  }
160
189
  }
161
190
  return surfaceResult;
@@ -325,9 +325,9 @@ function createStaticUnknownAdapter(workspaceRoot) {
325
325
  evidenceRefs: [
326
326
  {
327
327
  id: `delivery:${parsed.manifest.platformId}`,
328
- kind: "workspace_artifact",
328
+ family: "audit",
329
329
  uri: `workspace://connectors/${parsed.manifest.platformId}/manifest.yaml`,
330
- observedAt: now,
330
+ redactionClass: "none",
331
331
  },
332
332
  ],
333
333
  };
@@ -1527,7 +1527,7 @@ export function createOpsRouter(deps) {
1527
1527
  const platformId = typeof input?.platformId === "string"
1528
1528
  ? input.platformId
1529
1529
  : undefined;
1530
- const validSceneTypes = ["social", "reply", "outreach", "quiet", "explain", "user_reply"];
1530
+ const validSceneTypes = ["social", "reply", "outreach", "quiet", "heartbeat", "explain", "user_reply"];
1531
1531
  if (!validSceneTypes.includes(sceneType)) {
1532
1532
  const envelope = {
1533
1533
  ok: false,
@@ -4,6 +4,7 @@ import { createAgentGoalStore } from "../../storage/goal/agent-goal-store.js";
4
4
  import { createNarrativeStateStore } from "../../storage/narrative/narrative-state-store.js";
5
5
  import { createRelationshipMemoryStore } from "../../storage/relationship/relationship-memory-store.js";
6
6
  import { createIdentityProfileStore } from "../../storage/services/identity-profile-store.js";
7
+ import { toCanonicalSourceRef } from "../../shared/source-ref-compat.js";
7
8
  import { generateHeartbeatDigest, } from "../../observability/services/heartbeat-digest-assembler.js";
8
9
  import { createHistoryDigestStore } from "../../storage/services/history-digest-store.js";
9
10
  export async function loadSnapshotInputsForWorkspaceHeartbeat(readModels, options = {}) {
@@ -26,11 +27,7 @@ export async function loadSnapshotInputsForWorkspaceHeartbeat(readModels, option
26
27
  const snapshot = await loadLifeEvidenceSnapshot(options.state, options.workspaceRoot, { limit: 50 },
27
28
  // Skip repair gate here — runner is called inside a live cycle; gate ran at startup.
28
29
  { runRepairGate: false });
29
- lifeEvidenceRefs = snapshot.evidenceRefs.map((ref) => ({
30
- id: ref.id,
31
- kind: ref.kind,
32
- uri: ref.uri,
33
- }));
30
+ lifeEvidenceRefs = snapshot.evidenceRefs.map((ref) => toCanonicalSourceRef(ref));
34
31
  platformEventCount = snapshot.platformEvents.length;
35
32
  workEventCount = snapshot.workEvents.length;
36
33
  if (snapshot.empty) {
@@ -116,7 +116,7 @@ export declare const connectorManifestV6Schema: z.ZodObject<{
116
116
  export type ConnectorManifestV6 = z.infer<typeof connectorManifestV6Schema>;
117
117
  export interface ConnectorConflict {
118
118
  platformId: string;
119
- existingSource: "built_in" | "workspace";
119
+ existingSource: "built_in" | "workspace" | "workspace_shadow";
120
120
  attemptedSource: "built_in" | "workspace";
121
121
  reason: string;
122
122
  }
@@ -127,7 +127,7 @@ export interface ConnectorManifestValidationError {
127
127
  }
128
128
  export interface ConnectorInventoryEntry {
129
129
  platformId: string;
130
- source: "built_in" | "workspace";
130
+ source: "built_in" | "workspace" | "workspace_shadow";
131
131
  manifestPath?: string;
132
132
  trustStatus: ConnectorTrustStatus;
133
133
  executable: boolean;
@@ -41,6 +41,12 @@ function manifestToInventoryEntry(manifest, source, manifestPath) {
41
41
  validationErrors: [],
42
42
  };
43
43
  }
44
+ function isSafeBuiltInShadow(manifest) {
45
+ const reason = manifest.trust?.reason?.trim();
46
+ if (!manifest.trust?.override || !reason)
47
+ return false;
48
+ return manifest.runner.kind === "declarative_http" || manifest.runner.kind === "scriptable_node";
49
+ }
44
50
  /**
45
51
  * DynamicConnectorRegistry scans workspace manifests, validates, classifies trust,
46
52
  * merges built-in entries, applies fail-closed conflict policy, and publishes
@@ -79,11 +85,20 @@ export class DynamicConnectorRegistry {
79
85
  }
80
86
  const manifest = parseResult.manifest;
81
87
  const manifestPath = path.relative(workspaceRoot, file.path);
82
- // Duplicate platformId without override -> fail-closed
88
+ // Duplicate platformId without explicit safe override -> fail-closed.
89
+ // Built-ins may be shadowed only by an auditable workspace manifest with
90
+ // a trusted runner kind, so operators can repair endpoint config without
91
+ // silently replacing native code.
83
92
  if (builtInMap.has(manifest.platformId) || dynamicMap.has(manifest.platformId)) {
84
93
  const existing = builtInMap.get(manifest.platformId) ?? dynamicMap.get(manifest.platformId);
85
94
  const allowOverride = manifest.trust?.override === true;
86
95
  const isTrustedSource = existing.source === "built_in";
96
+ if (isTrustedSource && isSafeBuiltInShadow(manifest)) {
97
+ const entry = manifestToInventoryEntry(manifest, "workspace_shadow", manifestPath);
98
+ dynamicMap.set(manifest.platformId, entry);
99
+ registered++;
100
+ continue;
101
+ }
87
102
  if (!allowOverride || isTrustedSource) {
88
103
  conflicts.push({
89
104
  platformId: manifest.platformId,
@@ -51,7 +51,13 @@ function channelPriorityForRunner(manifest) {
51
51
  return ["browser"];
52
52
  return ["api_rest"];
53
53
  }
54
- function registerWorkspaceManifests(registry, workspaceRoot) {
54
+ function isSafeBuiltInShadow(manifest) {
55
+ const reason = manifest.trust?.reason?.trim();
56
+ if (!manifest.trust?.override || !reason)
57
+ return false;
58
+ return manifest.runner.kind === "declarative_http" || manifest.runner.kind === "scriptable_node";
59
+ }
60
+ function registerWorkspaceManifests(registry, builtInPlatformIds, workspaceRoot) {
55
61
  if (!workspaceRoot)
56
62
  return;
57
63
  for (const file of scanConnectorManifests(workspaceRoot)) {
@@ -59,6 +65,12 @@ function registerWorkspaceManifests(registry, workspaceRoot) {
59
65
  if (!parsed.ok)
60
66
  continue;
61
67
  const manifest = parsed.manifest;
68
+ // Built-in platforms may only be shadowed by auditable, trusted-runner manifests.
69
+ // Unsafe shadows are ignored by execution registry; they remain visible in
70
+ // DynamicConnectorRegistry conflicts for diagnostics.
71
+ if (builtInPlatformIds.has(manifest.platformId) && !isSafeBuiltInShadow(manifest)) {
72
+ continue;
73
+ }
62
74
  try {
63
75
  registry.register({
64
76
  platformId: manifest.platformId,
@@ -201,8 +213,8 @@ function createMoltbookMockRunner(workspaceRoot) {
201
213
  latencyMs: Date.now() - started,
202
214
  success: false,
203
215
  error: {
204
- code: "mock_read_error",
205
- detail: String(err),
216
+ code: "configuration_missing",
217
+ detail: `moltbook mock unreadable: ${err instanceof Error ? err.message : String(err)}`,
206
218
  },
207
219
  };
208
220
  }
@@ -424,9 +436,13 @@ function createAdaptiveExecutionRunner(vault, workspaceRoot) {
424
436
  const workspaceManifest = workspaceManifestResult?.manifest;
425
437
  const isBuiltInPlatform = platformId === "moltbook" ||
426
438
  platformId === "evomap" ||
427
- platformId === "agent-world";
439
+ platformId === "agent-world" ||
440
+ platformId === "instreet";
441
+ const effectiveWorkspaceManifest = workspaceManifest && (!isBuiltInPlatform || isSafeBuiltInShadow(workspaceManifest))
442
+ ? workspaceManifest
443
+ : undefined;
428
444
  const requiresCredential = isBuiltInPlatform ||
429
- Boolean(workspaceManifest?.credentials.some((credential) => credential.required !== false));
445
+ Boolean(effectiveWorkspaceManifest?.credentials.some((credential) => credential.required !== false));
430
446
  const credential = requiresCredential
431
447
  ? await vault.loadCredentialContext(platformId)
432
448
  : undefined;
@@ -448,9 +464,33 @@ function createAdaptiveExecutionRunner(vault, workspaceRoot) {
448
464
  const activeCredential = credential?.status === "active" && credential.encryptedValue
449
465
  ? { encryptedValue: credential.encryptedValue }
450
466
  : undefined;
467
+ // Safe workspace shadows of built-in platforms take precedence over built-in runners.
468
+ if (effectiveWorkspaceManifest && effectiveWorkspaceManifest.runner.kind === "declarative_http") {
469
+ const httpRunner = createDeclarativeHttpRunner(effectiveWorkspaceManifest, activeCredential);
470
+ return httpRunner.run(_plan, request);
471
+ }
472
+ if (effectiveWorkspaceManifest && effectiveWorkspaceManifest.runner.kind === "scriptable_node") {
473
+ if (!workspaceManifestResult) {
474
+ return {
475
+ platformId,
476
+ channel: request.preferredChannel ?? "api_rest",
477
+ latencyMs: Date.now() - started,
478
+ success: false,
479
+ error: {
480
+ code: "configuration_missing",
481
+ detail: "scriptable_node requires workspace manifest with manifestDir",
482
+ },
483
+ };
484
+ }
485
+ const scriptRunner = createScriptableNodeRunner(effectiveWorkspaceManifest, workspaceManifestResult.manifestDir, activeCredential);
486
+ return scriptRunner.run(_plan, request);
487
+ }
451
488
  if (platformId === "moltbook") {
452
489
  const baseUrl = process.env.SECOND_NATURE_MOLTBOOK_BASE_URL;
453
490
  if (baseUrl) {
491
+ const effectivePlan = request.intent === "feed.read" && _plan.channel !== "api_rest"
492
+ ? { ..._plan, channel: "api_rest", endpointMode: "rest_json", degraded: false }
493
+ : _plan;
454
494
  const apiClient = createMoltbookApiClient({
455
495
  baseUrl,
456
496
  accessToken: activeCredential.encryptedValue,
@@ -461,13 +501,13 @@ function createAdaptiveExecutionRunner(vault, workspaceRoot) {
461
501
  skillRunner: {
462
502
  run: async () => {
463
503
  throw {
464
- code: "protocol_mismatch",
504
+ code: "configuration_missing",
465
505
  detail: "moltbook_skill_runner_not_configured",
466
506
  };
467
507
  },
468
508
  },
469
509
  });
470
- return runner.run(_plan, request);
510
+ return runner.run(effectivePlan, request);
471
511
  }
472
512
  // Mock fallback when real API is not configured
473
513
  const mockRunner = createMoltbookMockRunner(workspaceRoot);
@@ -583,28 +623,6 @@ function createAdaptiveExecutionRunner(vault, workspaceRoot) {
583
623
  },
584
624
  };
585
625
  }
586
- // Wave 83: workspace declarative_http connector fallback
587
- if (workspaceManifest && workspaceManifest.runner.kind === "declarative_http") {
588
- const httpRunner = createDeclarativeHttpRunner(workspaceManifest, activeCredential);
589
- return httpRunner.run(_plan, request);
590
- }
591
- // Wave 90: workspace scriptable_node connector
592
- if (workspaceManifest && workspaceManifest.runner.kind === "scriptable_node") {
593
- if (!workspaceManifestResult) {
594
- return {
595
- platformId,
596
- channel: request.preferredChannel ?? "api_rest",
597
- latencyMs: Date.now() - started,
598
- success: false,
599
- error: {
600
- code: "configuration_missing",
601
- detail: "scriptable_node requires workspace manifest with manifestDir",
602
- },
603
- };
604
- }
605
- const scriptRunner = createScriptableNodeRunner(workspaceManifest, workspaceManifestResult.manifestDir, activeCredential);
606
- return scriptRunner.run(_plan, request);
607
- }
608
626
  return {
609
627
  platformId,
610
628
  channel: request.preferredChannel ?? "api_rest",
@@ -621,11 +639,12 @@ function createAdaptiveExecutionRunner(vault, workspaceRoot) {
621
639
  export function createConnectorExecutorAdapter(options) {
622
640
  const vault = createCredentialVault(options.stateDb.db);
623
641
  const registry = new CapabilityContractRegistry();
624
- registry.register({ ...moltbookManifest });
625
- registry.register({ ...evomapManifest });
626
- registry.register({ ...agentWorldManifest });
627
- registry.register({ ...instreetManifest });
628
- registerWorkspaceManifests(registry, options.workspaceRoot);
642
+ const builtInManifests = [moltbookManifest, evomapManifest, agentWorldManifest, instreetManifest];
643
+ const builtInPlatformIds = new Set(builtInManifests.map((m) => m.platformId));
644
+ for (const manifest of builtInManifests) {
645
+ registry.register({ ...manifest });
646
+ }
647
+ registerWorkspaceManifests(registry, builtInPlatformIds, options.workspaceRoot);
629
648
  const cooldownPort = createConnectorCooldownPort(options.stateDb);
630
649
  const routeContextPort = createCredentialRouteContextPort(vault, options.stateDb);
631
650
  const routePlanner = new ConnectorRoutePlanner(registry, routeContextPort, new ChannelHealthStore());
@@ -641,7 +660,7 @@ export function createConnectorExecutorAdapter(options) {
641
660
  });
642
661
  return {
643
662
  async executeEffect(input) {
644
- registerWorkspaceManifests(registry, options.workspaceRoot);
663
+ registerWorkspaceManifests(registry, builtInPlatformIds, options.workspaceRoot);
645
664
  return policy.executeWithPolicy(input.intent, {
646
665
  platformId: input.platformId,
647
666
  intent: input.intent,
@@ -43,6 +43,8 @@ export interface ActionClosureRecord {
43
43
  }
44
44
  export interface RecordClosureOptions {
45
45
  now?: string;
46
+ platformId?: string;
47
+ capabilityId?: string;
46
48
  }
47
49
  export type RecordClosureResult = {
48
50
  status: "recorded";
@@ -60,7 +60,6 @@ export async function recordNoActionClosure(db, cycleId, noActionReason, options
60
60
  },
61
61
  ],
62
62
  redactionClass: "none",
63
- lifecycleStatus: "closed",
64
63
  payloadJson: JSON.stringify({ dispatchAttempt: 0, inputSummary: "no-action" }),
65
64
  });
66
65
  if ("reason" in result) {
@@ -78,12 +77,13 @@ export async function recordRememberClosure(db, cycleId, memoryReviewCandidate,
78
77
  id: closureId,
79
78
  createdAt: now,
80
79
  cycleId,
80
+ platformId: options?.platformId ?? "heartbeat",
81
+ capabilityId: options?.capabilityId,
81
82
  status: "completed",
82
83
  reason: "remember_for_review",
83
84
  nextState: "pending_daily_review",
84
85
  sourceRefs: memoryReviewCandidate.sourceRefs,
85
86
  redactionClass: "none",
86
- lifecycleStatus: "closed",
87
87
  payloadJson: JSON.stringify({
88
88
  memoryReviewCandidate,
89
89
  dispatchAttempt: 1,
@@ -132,7 +132,6 @@ export async function recordPolicyOutcomeClosure(db, cycleId, closureStatus, rea
132
132
  nextState: params.nextState ?? "await_next_cycle",
133
133
  sourceRefs,
134
134
  redactionClass: "none",
135
- lifecycleStatus: "closed",
136
135
  payloadJson: JSON.stringify({
137
136
  dispatchAttempt: 1,
138
137
  inputSummary: buildInputSummary(params.proposalId, params.decisionId),
@@ -173,7 +172,6 @@ export async function recordExecutionClosure(db, cycleId, closureStatus, reason,
173
172
  nextState: params.nextState ?? (closureStatus === "completed" ? "await_next_cycle" : "retryable"),
174
173
  sourceRefs,
175
174
  redactionClass: "none",
176
- lifecycleStatus: "closed",
177
175
  payloadJson: JSON.stringify({
178
176
  dispatchAttempt: 1,
179
177
  executionResultRef: params.executionResultRef,
@@ -22,22 +22,12 @@
22
22
  *
23
23
  * Test coverage: tests/unit/action/action-proposal-builder.test.ts
24
24
  */
25
- import { readJudgmentVerdictById, writeActionClosureRecord, } from "../../../storage/v8-state-stores.js";
25
+ import { readJudgmentVerdictById, } from "../../../storage/v8-state-stores.js";
26
+ import { parseSourceRefs } from "../../../shared/serialization.js";
26
27
  import { ACTION_KIND_REGISTRY } from "../../../shared/types/v8-contracts.js";
27
28
  // ───────────────────────────────────────────────────────────────
28
29
  // Helpers
29
30
  // ───────────────────────────────────────────────────────────────
30
- function parseVerdictSourceRefs(json) {
31
- if (!json)
32
- return [];
33
- try {
34
- const parsed = JSON.parse(json);
35
- return Array.isArray(parsed) ? parsed : [];
36
- }
37
- catch {
38
- return [];
39
- }
40
- }
41
31
  function buildExpectedOutput(actionKind) {
42
32
  switch (actionKind) {
43
33
  case "ignore":
@@ -99,8 +89,8 @@ export async function buildActionProposal(db, judgmentVerdictId, options) {
99
89
  judgmentVerdictId,
100
90
  };
101
91
  }
102
- const sourceRefs = parseVerdictSourceRefs(verdict.sourceRefsJson);
103
- // remember → memory review candidate closure (no direct projection)
92
+ const sourceRefs = parseSourceRefs(verdict.sourceRefsJson);
93
+ // remember → memory review candidate (no direct projection; orchestrator writes closure)
104
94
  if (actionKind === "remember") {
105
95
  const candidate = {
106
96
  closureSubtype: "remember_for_review",
@@ -131,27 +121,10 @@ export async function buildActionProposal(db, judgmentVerdictId, options) {
131
121
  },
132
122
  ]),
133
123
  };
134
- const closureId = `cls_remember_${judgmentVerdictId}_${now.replace(/[:.]/g, "")}`;
135
- const writeResult = await writeActionClosureRecord(db, {
136
- id: closureId,
137
- createdAt: now,
138
- cycleId,
139
- platformId: "heartbeat",
140
- status: "completed",
141
- reason: "remember_for_review",
142
- nextState: "pending_daily_review",
143
- sourceRefs: candidate.sourceRefs,
144
- redactionClass: "none",
145
- lifecycleStatus: "closed",
146
- payloadJson: JSON.stringify({ memoryReviewCandidate: candidate }),
147
- });
148
- if ("reason" in writeResult) {
149
- return writeResult;
150
- }
151
124
  return {
152
125
  status: "remember_for_review",
153
126
  memoryReviewCandidate: candidate,
154
- closureId,
127
+ closureId: `cls_remember_${judgmentVerdictId}_${now.replace(/[:.]/g, "")}`,
155
128
  };
156
129
  }
157
130
  // Actionable verdict → build proposal
@@ -20,6 +20,7 @@
20
20
  *
21
21
  * Test coverage: tests/unit/action/policy-bound-dispatch.test.ts
22
22
  */
23
+ import { serializeSourceRefs } from "../../../shared/serialization.js";
23
24
  // ───────────────────────────────────────────────────────────────
24
25
  // Helpers
25
26
  // ───────────────────────────────────────────────────────────────
@@ -60,7 +61,7 @@ export function dispatchAllowedAction(proposal, decision, options) {
60
61
  actionKind: target,
61
62
  draftType,
62
63
  policyProof: { decisionId: decision.id, decision: decision.decision },
63
- sourceRefs: JSON.stringify(decision.proofRefs),
64
+ sourceRefs: serializeSourceRefs(decision.proofRefs),
64
65
  },
65
66
  };
66
67
  }
@@ -75,7 +76,7 @@ export function dispatchAllowedAction(proposal, decision, options) {
75
76
  capabilityId: proposal.targetCapabilityId ?? "run_connector",
76
77
  idempotencyKey: proposal.idempotencyKey,
77
78
  policyProof: { decisionId: decision.id, decision: decision.decision },
78
- sourceRefs: JSON.stringify(proposal.sourceRefs),
79
+ sourceRefs: serializeSourceRefs(proposal.sourceRefs),
79
80
  },
80
81
  };
81
82
  }
@@ -91,7 +92,7 @@ export function dispatchAllowedAction(proposal, decision, options) {
91
92
  actionKind: proposal.actionKind,
92
93
  draftType,
93
94
  policyProof: { decisionId: decision.id, decision: decision.decision },
94
- sourceRefs: JSON.stringify(proposal.sourceRefs),
95
+ sourceRefs: serializeSourceRefs(proposal.sourceRefs),
95
96
  },
96
97
  };
97
98
  }
@@ -20,6 +20,7 @@
20
20
  * Test coverage: tests/unit/control-plane/accepted-projection-loader.test.ts
21
21
  */
22
22
  import { readMemoryProjectionsByStatus, } from "../../../storage/v8-state-stores.js";
23
+ import { parseSourceRefs } from "../../../shared/serialization.js";
23
24
  // ───────────────────────────────────────────────────────────────
24
25
  // Helpers
25
26
  // ───────────────────────────────────────────────────────────────
@@ -33,17 +34,6 @@ function parsePayloadJson(json) {
33
34
  return {};
34
35
  }
35
36
  }
36
- function parseSourceRefs(json) {
37
- if (!json)
38
- return [];
39
- try {
40
- const parsed = JSON.parse(json);
41
- return Array.isArray(parsed) ? parsed : [];
42
- }
43
- catch {
44
- return [];
45
- }
46
- }
47
37
  // ───────────────────────────────────────────────────────────────
48
38
  // Public API
49
39
  // ───────────────────────────────────────────────────────────────
@@ -35,6 +35,7 @@ export interface HeartbeatOrchestrationResult {
35
35
  closureRef?: SourceRef;
36
36
  noActionReason?: V8ReasonCode;
37
37
  degraded?: DegradedOperationResult;
38
+ rhythmDegraded?: DegradedOperationResult;
38
39
  rhythmState?: DailyRhythmState;
39
40
  }
40
41
  export declare function runHeartbeatCycle(db: StateDatabase, request: HeartbeatOrchestrationRequest): Promise<HeartbeatOrchestrationResult | DegradedOperationResult>;