@haaaiawd/second-nature 0.2.8 → 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 (90) hide show
  1. package/index.js +12 -3
  2. package/openclaw.plugin.json +3 -6
  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/manual-run-dispatcher.d.ts +10 -0
  10. package/runtime/cli/ops/manual-run-dispatcher.js +49 -0
  11. package/runtime/cli/ops/ops-router.js +5 -3
  12. package/runtime/cli/ops/workspace-heartbeat-runner.js +2 -5
  13. package/runtime/connectors/base/normalized-evidence-content.js +15 -2
  14. package/runtime/connectors/manifest/manifest-schema.d.ts +2 -2
  15. package/runtime/connectors/registry/dynamic-connector-registry.js +16 -1
  16. package/runtime/connectors/services/connector-executor-adapter.js +58 -35
  17. package/runtime/core/second-nature/action/action-closure-recorder.d.ts +2 -0
  18. package/runtime/core/second-nature/action/action-closure-recorder.js +2 -4
  19. package/runtime/core/second-nature/action/action-proposal-builder.js +5 -32
  20. package/runtime/core/second-nature/action/policy-bound-dispatch.js +4 -3
  21. package/runtime/core/second-nature/control-plane/accepted-projection-loader.js +1 -11
  22. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.d.ts +1 -0
  23. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.js +10 -6
  24. package/runtime/core/second-nature/control-plane/real-runtime-spine.d.ts +2 -0
  25. package/runtime/core/second-nature/control-plane/real-runtime-spine.js +1 -0
  26. package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +4 -3
  27. package/runtime/core/second-nature/heartbeat/runtime-snapshot.d.ts +3 -2
  28. package/runtime/core/second-nature/heartbeat/snapshot-builder.d.ts +3 -2
  29. package/runtime/core/second-nature/orchestrator/intent-planner.js +4 -2
  30. package/runtime/core/second-nature/orchestrator/narrative-update.js +1 -2
  31. package/runtime/core/second-nature/orchestrator/platform-capability-router.d.ts +2 -2
  32. package/runtime/core/second-nature/orchestrator/platform-capability-router.js +1 -1
  33. package/runtime/core/second-nature/outreach/build-outreach-draft-request.js +2 -3
  34. package/runtime/core/second-nature/outreach/dispatch-user-outreach.d.ts +2 -2
  35. package/runtime/core/second-nature/outreach/dispatch-user-outreach.js +6 -1
  36. package/runtime/core/second-nature/outreach/judge-input-from-snapshot.js +3 -14
  37. package/runtime/core/second-nature/outreach/judge-outreach.d.ts +6 -5
  38. package/runtime/core/second-nature/perception/judgment-engine.js +2 -12
  39. package/runtime/core/second-nature/perception/perception-builder.js +1 -9
  40. package/runtime/core/second-nature/quiet/run-source-backed-quiet.js +13 -15
  41. package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.js +10 -13
  42. package/runtime/core/second-nature/quiet-dream/dream-consolidation-runner.d.ts +1 -0
  43. package/runtime/core/second-nature/quiet-dream/dream-consolidation-runner.js +11 -19
  44. package/runtime/core/second-nature/quiet-dream/dream-scheduler.js +0 -2
  45. package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.js +0 -12
  46. package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.js +1 -11
  47. package/runtime/core/second-nature/types.d.ts +2 -9
  48. package/runtime/dream/dream-engine.js +11 -4
  49. package/runtime/guidance/outreach-draft-schema.d.ts +12 -12
  50. package/runtime/guidance/persona-selection.js +5 -0
  51. package/runtime/guidance/template-registry.js +6 -1
  52. package/runtime/guidance/types.d.ts +2 -2
  53. package/runtime/observability/living-loop-health-gate.js +2 -8
  54. package/runtime/observability/loop-stage-event-sink.js +0 -1
  55. package/runtime/observability/services/lived-experience-audit.d.ts +7 -7
  56. package/runtime/shared/serialization.d.ts +17 -0
  57. package/runtime/shared/serialization.js +27 -0
  58. package/runtime/shared/source-ref-compat.d.ts +26 -0
  59. package/runtime/shared/source-ref-compat.js +61 -0
  60. package/runtime/shared/types/goal.d.ts +4 -4
  61. package/runtime/shared/types/goal.js +1 -1
  62. package/runtime/shared/types/index.d.ts +1 -0
  63. package/runtime/shared/types/index.js +1 -3
  64. package/runtime/shared/types/source-ref.d.ts +2 -2
  65. package/runtime/shared/types/v7-entities.d.ts +5 -5
  66. package/runtime/shared/types/v7-entities.js +1 -1
  67. package/runtime/shared/types/v8-contracts.d.ts +1 -1
  68. package/runtime/storage/db/index.js +31 -26
  69. package/runtime/storage/db/migrations/index.js +4 -0
  70. package/runtime/storage/db/migrations/v8-004-schema-closure.d.ts +19 -0
  71. package/runtime/storage/db/migrations/v8-004-schema-closure.js +74 -0
  72. package/runtime/storage/db/migrations/v8-005-single-status-schema.d.ts +11 -0
  73. package/runtime/storage/db/migrations/v8-005-single-status-schema.js +16 -0
  74. package/runtime/storage/db/schema/v8-entities.d.ts +0 -95
  75. package/runtime/storage/db/schema/v8-entities.js +4 -7
  76. package/runtime/storage/delivery/types.d.ts +2 -2
  77. package/runtime/storage/fallback/load-operator-fallback.d.ts +2 -2
  78. package/runtime/storage/fallback/operator-fallback-types.d.ts +2 -2
  79. package/runtime/storage/fallback/operator-fallback-view.d.ts +2 -2
  80. package/runtime/storage/index.d.ts +1 -1
  81. package/runtime/storage/life-evidence/types.d.ts +5 -5
  82. package/runtime/storage/quiet/quiet-artifact-types.d.ts +4 -4
  83. package/runtime/storage/quiet/quiet-artifact-writer.d.ts +2 -2
  84. package/runtime/storage/services/write-validation-gate.d.ts +1 -1
  85. package/runtime/storage/services/write-validation-gate.js +16 -4
  86. package/runtime/storage/snapshots/types.d.ts +8 -8
  87. package/runtime/storage/user-interest/types.d.ts +3 -3
  88. package/runtime/storage/v8-state-stores.d.ts +8 -1
  89. package/runtime/storage/v8-state-stores.js +23 -20
  90. package/workspace-ops-bridge.js +29 -1
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 /
@@ -91,6 +97,7 @@ const WORKSPACE_BRIDGE_COMMANDS = new Set([
91
97
  "session",
92
98
  "explain",
93
99
  "heartbeat_check",
100
+ "heartbeat_run",
94
101
  "fallback",
95
102
  "storage_smoke",
96
103
  // T1.2.8 (SN-CODE-03): capability probe surface via workspace bridge
@@ -146,8 +153,10 @@ async function ensureWorkspaceOpsBridge(spine) {
146
153
  return { ok: true, dispatch: opened.dispatch };
147
154
  }
148
155
  async function routeSecondNatureCommand(spine, command, input) {
156
+ // T-ROS.C.1-followup: heartbeat_run is an alias for heartbeat_check on the plugin surface.
157
+ const normalizedCommand = command === "heartbeat_run" ? "heartbeat_check" : command;
149
158
  const wr = spine.workspaceRootContext;
150
- const useBridge = wr.resolution !== "unknown" && isWorkspaceBridgeCommand(command, input);
159
+ const useBridge = wr.resolution !== "unknown" && isWorkspaceBridgeCommand(normalizedCommand, input);
151
160
  if (useBridge) {
152
161
  const bridge = await ensureWorkspaceOpsBridge(spine);
153
162
  if (!bridge.ok) {
@@ -164,10 +173,10 @@ async function routeSecondNatureCommand(spine, command, input) {
164
173
  },
165
174
  };
166
175
  }
167
- const payload = (await bridge.dispatch(command, input));
176
+ const payload = (await bridge.dispatch(normalizedCommand, input));
168
177
  return withSetupNudge(spine, command, payload);
169
178
  }
170
- const def = spine.router.resolve(command);
179
+ const def = spine.router.resolve(normalizedCommand);
171
180
  if (!def) {
172
181
  return { ok: false, message: `Unknown Second Nature command: ${command}` };
173
182
  }
@@ -1,13 +1,10 @@
1
1
  {
2
2
  "id": "second-nature",
3
3
  "name": "Second Nature",
4
- "version": "0.2.8",
5
- "description": "OpenClaw native plugin with synchronous surface registration and bundled runtime spine. Set SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot to the same path as the agent workspace. Agent inner guide is packaged as agent-inner-guide.md. Ops surface: loop_status, self_health, tool_affordance, heartbeat_digest, snapshot:capture, narrative:diff, timeline, restore, runtime_secret_bootstrap, connector:run, guidance_payload.",
4
+ "version": "0.2.12",
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.8",
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;
@@ -29,6 +29,7 @@ import type { CapabilityContractRegistryV7 } from "../../connectors/base/manifes
29
29
  import type { AppendOnlyAuditStore } from "../../observability/audit/append-only-audit-store.js";
30
30
  import { type HeartbeatCheckInput, type HeartbeatSurfaceResult } from "./heartbeat-surface.js";
31
31
  import type { RuntimeOpsEnvelope } from "./ops-router.js";
32
+ import type { StateDatabase } from "../../storage/db/index.js";
32
33
  export interface ManualTriggerContext {
33
34
  triggerSource: "manual_run";
34
35
  affectsHeartbeatCadence: false;
@@ -47,6 +48,11 @@ export interface ConnectorRunResult {
47
48
  experienceId: string;
48
49
  triggerSource: "manual_run";
49
50
  affectsHeartbeatCadence: false;
51
+ evidence?: {
52
+ v7EvidenceId?: string;
53
+ v8EvidenceIds: string[];
54
+ emptyReason?: string;
55
+ };
50
56
  }
51
57
  export interface WetProbeRunInput {
52
58
  platformId: string;
@@ -77,5 +83,9 @@ export interface ManualRunDispatcherDeps {
77
83
  wetProbeRunner: WetProbeRunner;
78
84
  registryV7: CapabilityContractRegistryV7;
79
85
  auditStore?: AppendOnlyAuditStore;
86
+ /** Workspace state database for evidence persistence (v7 life_evidence + v8 EvidenceItem). */
87
+ state?: StateDatabase;
88
+ /** Workspace root required for v7 life evidence JSON artifacts. */
89
+ workspaceRoot?: string;
80
90
  }
81
91
  export declare function createManualRunDispatcher(deps: ManualRunDispatcherDeps): ManualRunDispatcher;
@@ -25,6 +25,9 @@
25
25
  import * as crypto from "node:crypto";
26
26
  import { recordConnectorAttemptAudit } from "../../observability/services/audit-closure-recorders.js";
27
27
  import { heartbeatCheck, } from "./heartbeat-surface.js";
28
+ import { appendLifeEvidence } from "../../storage/life-evidence/append-life-evidence.js";
29
+ import { mapLifeEvidence } from "../../connectors/base/map-life-evidence.js";
30
+ import { normalizeConnectorEvidence } from "../../connectors/evidence-normalizer.js";
28
31
  function buildManualContext(input) {
29
32
  return {
30
33
  triggerSource: "manual_run",
@@ -40,6 +43,7 @@ export function createManualRunDispatcher(deps) {
40
43
  const decisionId = `manual:${crypto.randomUUID()}`;
41
44
  const intentId = `manual-run:${input.platformId}:${input.capabilityId}`;
42
45
  const idempotencyKey = `idem:manual:${crypto.randomUUID()}`;
46
+ const now = new Date().toISOString();
43
47
  const connectorResult = await deps.connectorExecutor.executeEffect({
44
48
  platformId: input.platformId,
45
49
  intent: input.capabilityId,
@@ -64,11 +68,56 @@ export function createManualRunDispatcher(deps) {
64
68
  decisionId,
65
69
  intentId,
66
70
  });
71
+ const evidenceSummary = {
72
+ v8EvidenceIds: [],
73
+ };
74
+ if (connectorResult.status === "success" && deps.state) {
75
+ const capabilityIntent = input.capabilityId;
76
+ // v7 life evidence double-write (parity with heartbeat loop)
77
+ try {
78
+ if (deps.workspaceRoot) {
79
+ const lifeCandidate = mapLifeEvidence({
80
+ platformId: input.platformId,
81
+ intent: capabilityIntent,
82
+ result: connectorResult,
83
+ observedAt: now,
84
+ });
85
+ if (lifeCandidate) {
86
+ const lifeAck = await appendLifeEvidence(deps.state, deps.workspaceRoot, lifeCandidate);
87
+ evidenceSummary.v7EvidenceId = lifeAck.evidenceId;
88
+ }
89
+ }
90
+ }
91
+ catch (v7Err) {
92
+ const msg = v7Err instanceof Error ? v7Err.message : String(v7Err);
93
+ console.warn(`[connector:run] v7 life evidence append failed for ${input.platformId}: ${msg}`);
94
+ }
95
+ // v8 EvidenceItem content-bearing write
96
+ try {
97
+ const v8Result = await normalizeConnectorEvidence(deps.state, {
98
+ status: "success",
99
+ platformId: input.platformId,
100
+ capabilityId: input.capabilityId,
101
+ data: connectorResult.data,
102
+ observedAt: now,
103
+ });
104
+ evidenceSummary.v8EvidenceIds = v8Result.evidenceIds;
105
+ evidenceSummary.emptyReason = v8Result.emptyReason;
106
+ if (v8Result.degraded) {
107
+ console.warn(`[connector:run] v8 evidence normalization degraded for ${input.platformId}: ${v8Result.degraded.reason}`);
108
+ }
109
+ }
110
+ catch (v8Err) {
111
+ const msg = v8Err instanceof Error ? v8Err.message : String(v8Err);
112
+ console.warn(`[connector:run] v8 evidence normalization failed for ${input.platformId}: ${msg}`);
113
+ }
114
+ }
67
115
  const runResult = {
68
116
  connectorResult,
69
117
  experienceId,
70
118
  triggerSource: ctx.triggerSource,
71
119
  affectsHeartbeatCadence: ctx.affectsHeartbeatCadence,
120
+ evidence: evidenceSummary,
72
121
  };
73
122
  return {
74
123
  ok: connectorResult.status === "success",
@@ -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
  };
@@ -785,6 +785,8 @@ export function createOpsRouter(deps) {
785
785
  wetProbeRunner,
786
786
  registryV7,
787
787
  auditStore: deps.auditStore,
788
+ state: deps.state,
789
+ workspaceRoot: typeof input?.workspaceRoot === "string" ? input.workspaceRoot : process.cwd(),
788
790
  });
789
791
  return dispatcher.runConnector({
790
792
  platformId,
@@ -1525,7 +1527,7 @@ export function createOpsRouter(deps) {
1525
1527
  const platformId = typeof input?.platformId === "string"
1526
1528
  ? input.platformId
1527
1529
  : undefined;
1528
- const validSceneTypes = ["social", "reply", "outreach", "quiet", "explain", "user_reply"];
1530
+ const validSceneTypes = ["social", "reply", "outreach", "quiet", "heartbeat", "explain", "user_reply"];
1529
1531
  if (!validSceneTypes.includes(sceneType)) {
1530
1532
  const envelope = {
1531
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) {
@@ -185,12 +185,15 @@ function extractMetrics(item) {
185
185
  // ───────────────────────────────────────────────────────────────
186
186
  // Item extraction from connector payload
187
187
  // ───────────────────────────────────────────────────────────────
188
- function extractItems(data) {
188
+ function extractItems(data, depth = 4) {
189
+ if (depth <= 0)
190
+ return [];
189
191
  if (Array.isArray(data))
190
192
  return data;
191
193
  if (!isRecord(data))
192
194
  return [];
193
- for (const key of ["items", "data", "results", "posts", "nodes", "agents", "edges", "entries", "feed"]) {
195
+ const ARRAY_KEYS = ["items", "data", "results", "posts", "nodes", "agents", "edges", "entries", "feed"];
196
+ for (const key of ARRAY_KEYS) {
194
197
  const candidate = data[key];
195
198
  if (Array.isArray(candidate))
196
199
  return candidate;
@@ -198,6 +201,16 @@ function extractItems(data) {
198
201
  // If the payload itself looks like a single item, treat it as one-item array.
199
202
  if ("id" in data || "title" in data || "content" in data)
200
203
  return [data];
204
+ // Recurse into nested record-valued fields (e.g. real connector runners wrap
205
+ // the platform response in { capability, channel, data: apiResponse }).
206
+ for (const key of ARRAY_KEYS) {
207
+ const candidate = data[key];
208
+ if (isRecord(candidate)) {
209
+ const nested = extractItems(candidate, depth - 1);
210
+ if (nested.length > 0)
211
+ return nested;
212
+ }
213
+ }
201
214
  return [];
202
215
  }
203
216
  function normalizeSingleItem(item, options) {
@@ -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,
@@ -187,6 +199,10 @@ function createMoltbookMockRunner(workspaceRoot) {
187
199
  source: "mock",
188
200
  items: Array.isArray(data.items) ? data.items : [],
189
201
  },
202
+ // Duplicate items at payload top-level so v8 evidence normalizer
203
+ // can extract content-bearing evidence without re-implementing
204
+ // the legacy v7 nested shape.
205
+ items: Array.isArray(data.items) ? data.items : [],
190
206
  },
191
207
  };
192
208
  }
@@ -197,8 +213,8 @@ function createMoltbookMockRunner(workspaceRoot) {
197
213
  latencyMs: Date.now() - started,
198
214
  success: false,
199
215
  error: {
200
- code: "mock_read_error",
201
- detail: String(err),
216
+ code: "configuration_missing",
217
+ detail: `moltbook mock unreadable: ${err instanceof Error ? err.message : String(err)}`,
202
218
  },
203
219
  };
204
220
  }
@@ -420,9 +436,13 @@ function createAdaptiveExecutionRunner(vault, workspaceRoot) {
420
436
  const workspaceManifest = workspaceManifestResult?.manifest;
421
437
  const isBuiltInPlatform = platformId === "moltbook" ||
422
438
  platformId === "evomap" ||
423
- platformId === "agent-world";
439
+ platformId === "agent-world" ||
440
+ platformId === "instreet";
441
+ const effectiveWorkspaceManifest = workspaceManifest && (!isBuiltInPlatform || isSafeBuiltInShadow(workspaceManifest))
442
+ ? workspaceManifest
443
+ : undefined;
424
444
  const requiresCredential = isBuiltInPlatform ||
425
- Boolean(workspaceManifest?.credentials.some((credential) => credential.required !== false));
445
+ Boolean(effectiveWorkspaceManifest?.credentials.some((credential) => credential.required !== false));
426
446
  const credential = requiresCredential
427
447
  ? await vault.loadCredentialContext(platformId)
428
448
  : undefined;
@@ -444,9 +464,33 @@ function createAdaptiveExecutionRunner(vault, workspaceRoot) {
444
464
  const activeCredential = credential?.status === "active" && credential.encryptedValue
445
465
  ? { encryptedValue: credential.encryptedValue }
446
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
+ }
447
488
  if (platformId === "moltbook") {
448
489
  const baseUrl = process.env.SECOND_NATURE_MOLTBOOK_BASE_URL;
449
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;
450
494
  const apiClient = createMoltbookApiClient({
451
495
  baseUrl,
452
496
  accessToken: activeCredential.encryptedValue,
@@ -457,13 +501,13 @@ function createAdaptiveExecutionRunner(vault, workspaceRoot) {
457
501
  skillRunner: {
458
502
  run: async () => {
459
503
  throw {
460
- code: "protocol_mismatch",
504
+ code: "configuration_missing",
461
505
  detail: "moltbook_skill_runner_not_configured",
462
506
  };
463
507
  },
464
508
  },
465
509
  });
466
- return runner.run(_plan, request);
510
+ return runner.run(effectivePlan, request);
467
511
  }
468
512
  // Mock fallback when real API is not configured
469
513
  const mockRunner = createMoltbookMockRunner(workspaceRoot);
@@ -579,28 +623,6 @@ function createAdaptiveExecutionRunner(vault, workspaceRoot) {
579
623
  },
580
624
  };
581
625
  }
582
- // Wave 83: workspace declarative_http connector fallback
583
- if (workspaceManifest && workspaceManifest.runner.kind === "declarative_http") {
584
- const httpRunner = createDeclarativeHttpRunner(workspaceManifest, activeCredential);
585
- return httpRunner.run(_plan, request);
586
- }
587
- // Wave 90: workspace scriptable_node connector
588
- if (workspaceManifest && workspaceManifest.runner.kind === "scriptable_node") {
589
- if (!workspaceManifestResult) {
590
- return {
591
- platformId,
592
- channel: request.preferredChannel ?? "api_rest",
593
- latencyMs: Date.now() - started,
594
- success: false,
595
- error: {
596
- code: "configuration_missing",
597
- detail: "scriptable_node requires workspace manifest with manifestDir",
598
- },
599
- };
600
- }
601
- const scriptRunner = createScriptableNodeRunner(workspaceManifest, workspaceManifestResult.manifestDir, activeCredential);
602
- return scriptRunner.run(_plan, request);
603
- }
604
626
  return {
605
627
  platformId,
606
628
  channel: request.preferredChannel ?? "api_rest",
@@ -617,11 +639,12 @@ function createAdaptiveExecutionRunner(vault, workspaceRoot) {
617
639
  export function createConnectorExecutorAdapter(options) {
618
640
  const vault = createCredentialVault(options.stateDb.db);
619
641
  const registry = new CapabilityContractRegistry();
620
- registry.register({ ...moltbookManifest });
621
- registry.register({ ...evomapManifest });
622
- registry.register({ ...agentWorldManifest });
623
- registry.register({ ...instreetManifest });
624
- 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);
625
648
  const cooldownPort = createConnectorCooldownPort(options.stateDb);
626
649
  const routeContextPort = createCredentialRouteContextPort(vault, options.stateDb);
627
650
  const routePlanner = new ConnectorRoutePlanner(registry, routeContextPort, new ChannelHealthStore());
@@ -637,7 +660,7 @@ export function createConnectorExecutorAdapter(options) {
637
660
  });
638
661
  return {
639
662
  async executeEffect(input) {
640
- registerWorkspaceManifests(registry, options.workspaceRoot);
663
+ registerWorkspaceManifests(registry, builtInPlatformIds, options.workspaceRoot);
641
664
  return policy.executeWithPolicy(input.intent, {
642
665
  platformId: input.platformId,
643
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";