@haaaiawd/second-nature 0.1.20 → 0.1.22

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "second-nature",
3
3
  "name": "Second Nature",
4
- "version": "0.1.20",
4
+ "version": "0.1.22",
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@haaaiawd/second-nature",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
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,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();
@@ -20,6 +21,11 @@ export function createCliRuntimeDeps(overrides = {}) {
20
21
  });
21
22
  const actionBridge = overrides.actionBridge ?? createActionBridge(stateApi);
22
23
  const runtimeRecorder = overrides.runtimeRecorder ?? createRuntimeDecisionRecorder(observabilityDb);
24
+ const connectorExecutor = overrides.connectorExecutor ??
25
+ createConnectorExecutorAdapter({
26
+ stateDb,
27
+ observabilityDb,
28
+ });
23
29
  return {
24
30
  stateDb,
25
31
  observabilityDb,
@@ -27,6 +33,7 @@ export function createCliRuntimeDeps(overrides = {}) {
27
33
  readModels,
28
34
  actionBridge,
29
35
  runtimeRecorder,
36
+ connectorExecutor,
30
37
  };
31
38
  }
32
39
  export function createCommandRouter(options = {}) {
@@ -39,6 +46,7 @@ export function createCommandRouter(options = {}) {
39
46
  state: runtime.stateDb,
40
47
  workspaceRoot: process.cwd(),
41
48
  observabilityDb: runtime.observabilityDb,
49
+ connectorExecutor: runtime.connectorExecutor,
42
50
  });
43
51
  const commands = createCliCommands({
44
52
  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");
@@ -6,6 +6,7 @@ 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
8
  import type { ObservabilityDatabase } from "../../observability/db/index.js";
9
+ import type { ConnectorExecutor } from "../../core/second-nature/orchestrator/effect-dispatcher.js";
9
10
  export interface OpsRouterDeps {
10
11
  /** When true, packaged runtime artifacts resolved and full graph is loadable */
11
12
  runtimeAvailable: boolean;
@@ -24,6 +25,11 @@ export interface OpsRouterDeps {
24
25
  * When absent, `capability_probe` still runs but skips persistence.
25
26
  */
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;
27
33
  }
28
34
  export interface OpsRouter {
29
35
  heartbeatCheck(input: HeartbeatCheckInput): Promise<HeartbeatSurfaceResult>;
@@ -41,6 +41,7 @@ export function createOpsRouter(deps) {
41
41
  runtimeRecorder: input.runtimeRecorder ?? deps.runtimeRecorder,
42
42
  state: input.state ?? deps.state,
43
43
  workspaceRoot: input.workspaceRoot ?? deps.workspaceRoot,
44
+ connectorExecutor: input.connectorExecutor ?? deps.connectorExecutor,
44
45
  }),
45
46
  dispatch(command, input) {
46
47
  if (command === "heartbeat_check") {
@@ -67,6 +68,8 @@ export function createOpsRouter(deps) {
67
68
  ? input.sessionContext
68
69
  : undefined,
69
70
  scopeHint: input?.scopeHint,
71
+ connectorExecutor: input
72
+ ?.connectorExecutor ?? deps.connectorExecutor,
70
73
  });
71
74
  }
72
75
  if (command === "fallback") {
@@ -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) {
@@ -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 = ["api_rest", "api_rpc", "a2a", "mcp", "cli", "skill", "browser"];
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" && Number.isFinite(retryAfterMs) && retryAfterMs > 0) {
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" && Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0) {
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 { class: "verification_required", retryable: RETRYABLE_BY_CLASS.verification_required };
77
+ return {
78
+ class: "verification_required",
79
+ retryable: RETRYABLE_BY_CLASS.verification_required,
80
+ };
69
81
  if (code === "credential_expired")
70
- return { class: "credential_expired", retryable: RETRYABLE_BY_CLASS.credential_expired };
82
+ return {
83
+ class: "credential_expired",
84
+ retryable: RETRYABLE_BY_CLASS.credential_expired,
85
+ };
71
86
  if (code === "cooldown_blocked")
72
- return { class: "cooldown_blocked", retryable: RETRYABLE_BY_CLASS.cooldown_blocked };
87
+ return {
88
+ class: "cooldown_blocked",
89
+ retryable: RETRYABLE_BY_CLASS.cooldown_blocked,
90
+ };
73
91
  if (code === "idempotency_conflict")
74
- return { class: "idempotency_conflict", retryable: RETRYABLE_BY_CLASS.idempotency_conflict };
92
+ return {
93
+ class: "idempotency_conflict",
94
+ retryable: RETRYABLE_BY_CLASS.idempotency_conflict,
95
+ };
75
96
  if (code === "concurrency_conflict")
76
- return { class: "concurrency_conflict", retryable: RETRYABLE_BY_CLASS.concurrency_conflict };
97
+ return {
98
+ class: "concurrency_conflict",
99
+ retryable: RETRYABLE_BY_CLASS.concurrency_conflict,
100
+ };
77
101
  if (code === "protocol_mismatch")
78
- return { class: "protocol_mismatch", retryable: RETRYABLE_BY_CLASS.protocol_mismatch };
102
+ return {
103
+ class: "protocol_mismatch",
104
+ retryable: RETRYABLE_BY_CLASS.protocol_mismatch,
105
+ };
79
106
  if (code === "semantic_rejection")
80
- return { class: "semantic_rejection", retryable: RETRYABLE_BY_CLASS.semantic_rejection };
107
+ return {
108
+ class: "semantic_rejection",
109
+ retryable: RETRYABLE_BY_CLASS.semantic_rejection,
110
+ };
81
111
  if (code === "transport_failure")
82
- return { class: "transport_failure", retryable: RETRYABLE_BY_CLASS.transport_failure };
112
+ return {
113
+ class: "transport_failure",
114
+ retryable: RETRYABLE_BY_CLASS.transport_failure,
115
+ };
83
116
  if (code === "permanent_input_error")
84
- return { class: "permanent_input_error", retryable: RETRYABLE_BY_CLASS.permanent_input_error };
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 { class: "auth_failure", retryable: RETRYABLE_BY_CLASS.auth_failure };
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 { class: "permanent_input_error", retryable: RETRYABLE_BY_CLASS.permanent_input_error };
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 { class: "transport_failure", retryable: RETRYABLE_BY_CLASS.transport_failure };
143
+ return {
144
+ class: "transport_failure",
145
+ retryable: RETRYABLE_BY_CLASS.transport_failure,
146
+ };
102
147
  }
103
148
  }
104
- return { class: "unknown_platform_change", retryable: RETRYABLE_BY_CLASS.unknown_platform_change };
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" || effectClass === "connector_action" || effectClass === "user_outreach";
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" || effectClass === "connector_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) ? "before_effect" : "before_quiet_write",
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.19";
30
+ const version = "0.1.21";
31
31
  activeHandle = {
32
32
  ready: true,
33
33
  version,
@@ -46,6 +46,7 @@ export async function openWorkspaceOpsBridge(workspaceRoot) {
46
46
  observabilityDb,
47
47
  state: stateDb,
48
48
  workspaceRoot: resolvedRoot,
49
+ connectorExecutor: deps.connectorExecutor,
49
50
  });
50
51
  const commands = commandsMod.createCliCommands({
51
52
  readModels: deps.readModels,