@haaaiawd/second-nature 0.1.20 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/runtime/cli/index.js +6 -0
- package/runtime/cli/ops/heartbeat-surface.d.ts +6 -0
- package/runtime/cli/ops/heartbeat-surface.js +1 -0
- package/runtime/cli/ops/ops-router.d.ts +6 -0
- package/runtime/cli/ops/ops-router.js +3 -0
- package/runtime/cli/ops/workspace-heartbeat-runner.d.ts +6 -0
- package/runtime/cli/ops/workspace-heartbeat-runner.js +1 -0
- package/runtime/connectors/base/contract.d.ts +10 -0
- package/runtime/connectors/base/contract.js +10 -2
- package/runtime/connectors/base/failure-taxonomy.js +63 -15
- package/runtime/connectors/services/connector-executor-adapter.d.ts +16 -0
- package/runtime/connectors/services/connector-executor-adapter.js +118 -0
- package/runtime/connectors/services/credential-route-context.d.ts +10 -0
- package/runtime/connectors/services/credential-route-context.js +19 -0
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +7 -1
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +23 -0
- package/runtime/core/second-nature/orchestrator/effect-dispatcher.d.ts +3 -11
- package/runtime/core/second-nature/orchestrator/effect-dispatcher.js +9 -4
- package/runtime/core/second-nature/runtime/service-entry.js +1 -1
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "second-nature",
|
|
3
3
|
"name": "Second Nature",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.21",
|
|
5
5
|
"description": "OpenClaw native plugin with synchronous surface registration and bundled runtime spine. Set SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot to the same path as the agent workspace (see README / T1.1.4 ops norm).",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onStartup": true,
|
package/package.json
CHANGED
package/runtime/cli/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { createOpsRouter } from "./ops/ops-router.js";
|
|
|
7
7
|
import { createCliReadModels, } from "./read-models/index.js";
|
|
8
8
|
import { resolvePackagedRuntime } from "./runtime/runtime-artifact-boundary.js";
|
|
9
9
|
import { createRuntimeDecisionRecorder, } from "../observability/services/runtime-decision-recorder.js";
|
|
10
|
+
import { createConnectorExecutorAdapter } from "../connectors/services/connector-executor-adapter.js";
|
|
10
11
|
export function createCliRuntimeDeps(overrides = {}) {
|
|
11
12
|
const stateDb = overrides.stateDb ?? createStateDatabase();
|
|
12
13
|
const observabilityDb = overrides.observabilityDb ?? createObservabilityDatabase();
|
|
@@ -32,6 +33,10 @@ export function createCliRuntimeDeps(overrides = {}) {
|
|
|
32
33
|
export function createCommandRouter(options = {}) {
|
|
33
34
|
const runtime = createCliRuntimeDeps(options.deps);
|
|
34
35
|
const pluginRoot = path.join(process.cwd(), "plugin");
|
|
36
|
+
const connectorExecutor = createConnectorExecutorAdapter({
|
|
37
|
+
stateDb: runtime.stateDb,
|
|
38
|
+
observabilityDb: runtime.observabilityDb,
|
|
39
|
+
});
|
|
35
40
|
const opsRouter = createOpsRouter({
|
|
36
41
|
runtimeAvailable: resolvePackagedRuntime(pluginRoot).ok,
|
|
37
42
|
readModels: runtime.readModels,
|
|
@@ -39,6 +44,7 @@ export function createCommandRouter(options = {}) {
|
|
|
39
44
|
state: runtime.stateDb,
|
|
40
45
|
workspaceRoot: process.cwd(),
|
|
41
46
|
observabilityDb: runtime.observabilityDb,
|
|
47
|
+
connectorExecutor,
|
|
42
48
|
});
|
|
43
49
|
const commands = createCliCommands({
|
|
44
50
|
readModels: runtime.readModels,
|
|
@@ -9,6 +9,7 @@ import type { HeartbeatSignal } from "../../core/second-nature/heartbeat/signal.
|
|
|
9
9
|
import type { CliReadModels } from "../read-models/index.js";
|
|
10
10
|
import type { RuntimeDecisionRecorder } from "../../observability/services/runtime-decision-recorder.js";
|
|
11
11
|
import type { StateDatabase } from "../../storage/db/index.js";
|
|
12
|
+
import type { ConnectorExecutor } from "../../core/second-nature/orchestrator/effect-dispatcher.js";
|
|
12
13
|
export type HeartbeatSurfaceStatus = "heartbeat_ok" | "intent_selected" | "denied" | "deferred" | "runtime_carrier_only" | "delivery_unavailable";
|
|
13
14
|
export interface HeartbeatSurfaceResult {
|
|
14
15
|
ok: boolean;
|
|
@@ -41,5 +42,10 @@ export interface HeartbeatCheckInput {
|
|
|
41
42
|
timestamp?: string;
|
|
42
43
|
sessionContext?: string;
|
|
43
44
|
scopeHint?: HeartbeatSignal["scopeHint"];
|
|
45
|
+
/**
|
|
46
|
+
* When present, guard-allowed connector_action intents are dispatched through the
|
|
47
|
+
* connector-system instead of returning connector_dispatch_unwired.
|
|
48
|
+
*/
|
|
49
|
+
connectorExecutor?: ConnectorExecutor;
|
|
44
50
|
}
|
|
45
51
|
export declare function heartbeatCheck(input: HeartbeatCheckInput): Promise<HeartbeatSurfaceResult>;
|
|
@@ -73,6 +73,7 @@ export async function heartbeatCheck(input) {
|
|
|
73
73
|
runtimeRecorder: input.runtimeRecorder,
|
|
74
74
|
state: input.state,
|
|
75
75
|
workspaceRoot: input.workspaceRoot ?? process.cwd(),
|
|
76
|
+
connectorExecutor: input.connectorExecutor,
|
|
76
77
|
});
|
|
77
78
|
const cycle = await run(signal);
|
|
78
79
|
return mapCycleToSurface(cycle, "workspace_full_runtime");
|
|
@@ -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 = [
|
|
2
|
+
import { classifyFailure, ConnectorPolicyError, } from "./failure-taxonomy.js";
|
|
3
|
+
export const CHANNEL_TYPES = [
|
|
4
|
+
"api_rest",
|
|
5
|
+
"api_rpc",
|
|
6
|
+
"a2a",
|
|
7
|
+
"mcp",
|
|
8
|
+
"cli",
|
|
9
|
+
"skill",
|
|
10
|
+
"browser",
|
|
11
|
+
];
|
|
4
12
|
export const CAPABILITY_INTENTS = [
|
|
5
13
|
"feed.read",
|
|
6
14
|
"post.publish",
|
|
@@ -40,11 +40,15 @@ export class ConnectorPolicyError extends Error {
|
|
|
40
40
|
}
|
|
41
41
|
function readRetryAfterMs(input) {
|
|
42
42
|
const retryAfterMs = input.retryAfterMs;
|
|
43
|
-
if (typeof retryAfterMs === "number" &&
|
|
43
|
+
if (typeof retryAfterMs === "number" &&
|
|
44
|
+
Number.isFinite(retryAfterMs) &&
|
|
45
|
+
retryAfterMs > 0) {
|
|
44
46
|
return retryAfterMs;
|
|
45
47
|
}
|
|
46
48
|
const retryAfterSeconds = input.retryAfterSeconds;
|
|
47
|
-
if (typeof retryAfterSeconds === "number" &&
|
|
49
|
+
if (typeof retryAfterSeconds === "number" &&
|
|
50
|
+
Number.isFinite(retryAfterSeconds) &&
|
|
51
|
+
retryAfterSeconds > 0) {
|
|
48
52
|
return retryAfterSeconds * 1000;
|
|
49
53
|
}
|
|
50
54
|
return undefined;
|
|
@@ -64,24 +68,56 @@ export function classifyFailure(error) {
|
|
|
64
68
|
const record = error;
|
|
65
69
|
const code = record.code;
|
|
66
70
|
if (typeof code === "string") {
|
|
71
|
+
if (code === "auth_failure")
|
|
72
|
+
return {
|
|
73
|
+
class: "auth_failure",
|
|
74
|
+
retryable: RETRYABLE_BY_CLASS.auth_failure,
|
|
75
|
+
};
|
|
67
76
|
if (code === "verification_required")
|
|
68
|
-
return {
|
|
77
|
+
return {
|
|
78
|
+
class: "verification_required",
|
|
79
|
+
retryable: RETRYABLE_BY_CLASS.verification_required,
|
|
80
|
+
};
|
|
69
81
|
if (code === "credential_expired")
|
|
70
|
-
return {
|
|
82
|
+
return {
|
|
83
|
+
class: "credential_expired",
|
|
84
|
+
retryable: RETRYABLE_BY_CLASS.credential_expired,
|
|
85
|
+
};
|
|
71
86
|
if (code === "cooldown_blocked")
|
|
72
|
-
return {
|
|
87
|
+
return {
|
|
88
|
+
class: "cooldown_blocked",
|
|
89
|
+
retryable: RETRYABLE_BY_CLASS.cooldown_blocked,
|
|
90
|
+
};
|
|
73
91
|
if (code === "idempotency_conflict")
|
|
74
|
-
return {
|
|
92
|
+
return {
|
|
93
|
+
class: "idempotency_conflict",
|
|
94
|
+
retryable: RETRYABLE_BY_CLASS.idempotency_conflict,
|
|
95
|
+
};
|
|
75
96
|
if (code === "concurrency_conflict")
|
|
76
|
-
return {
|
|
97
|
+
return {
|
|
98
|
+
class: "concurrency_conflict",
|
|
99
|
+
retryable: RETRYABLE_BY_CLASS.concurrency_conflict,
|
|
100
|
+
};
|
|
77
101
|
if (code === "protocol_mismatch")
|
|
78
|
-
return {
|
|
102
|
+
return {
|
|
103
|
+
class: "protocol_mismatch",
|
|
104
|
+
retryable: RETRYABLE_BY_CLASS.protocol_mismatch,
|
|
105
|
+
};
|
|
79
106
|
if (code === "semantic_rejection")
|
|
80
|
-
return {
|
|
107
|
+
return {
|
|
108
|
+
class: "semantic_rejection",
|
|
109
|
+
retryable: RETRYABLE_BY_CLASS.semantic_rejection,
|
|
110
|
+
};
|
|
81
111
|
if (code === "transport_failure")
|
|
82
|
-
return {
|
|
112
|
+
return {
|
|
113
|
+
class: "transport_failure",
|
|
114
|
+
retryable: RETRYABLE_BY_CLASS.transport_failure,
|
|
115
|
+
};
|
|
83
116
|
if (code === "permanent_input_error")
|
|
84
|
-
return {
|
|
117
|
+
return {
|
|
118
|
+
class: "permanent_input_error",
|
|
119
|
+
retryable: RETRYABLE_BY_CLASS.permanent_input_error,
|
|
120
|
+
};
|
|
85
121
|
}
|
|
86
122
|
const status = record.status;
|
|
87
123
|
if (status === 429) {
|
|
@@ -92,14 +128,26 @@ export function classifyFailure(error) {
|
|
|
92
128
|
};
|
|
93
129
|
}
|
|
94
130
|
if (status === 401 || status === 403) {
|
|
95
|
-
return {
|
|
131
|
+
return {
|
|
132
|
+
class: "auth_failure",
|
|
133
|
+
retryable: RETRYABLE_BY_CLASS.auth_failure,
|
|
134
|
+
};
|
|
96
135
|
}
|
|
97
136
|
if (status === 400 || status === 404 || status === 422) {
|
|
98
|
-
return {
|
|
137
|
+
return {
|
|
138
|
+
class: "permanent_input_error",
|
|
139
|
+
retryable: RETRYABLE_BY_CLASS.permanent_input_error,
|
|
140
|
+
};
|
|
99
141
|
}
|
|
100
142
|
if (status === 500 || status === 502 || status === 503 || status === 504) {
|
|
101
|
-
return {
|
|
143
|
+
return {
|
|
144
|
+
class: "transport_failure",
|
|
145
|
+
retryable: RETRYABLE_BY_CLASS.transport_failure,
|
|
146
|
+
};
|
|
102
147
|
}
|
|
103
148
|
}
|
|
104
|
-
return {
|
|
149
|
+
return {
|
|
150
|
+
class: "unknown_platform_change",
|
|
151
|
+
retryable: RETRYABLE_BY_CLASS.unknown_platform_change,
|
|
152
|
+
};
|
|
105
153
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter: assemble connector-system execution infrastructure into the
|
|
3
|
+
* ConnectorExecutor interface consumed by EffectDispatcher.
|
|
4
|
+
*
|
|
5
|
+
* When credentials / base URLs are missing, returns an honest
|
|
6
|
+
* terminal_failure instead of throwing so the heartbeat loop stays stable.
|
|
7
|
+
*/
|
|
8
|
+
import type { ConnectorExecutor } from "../base/contract.js";
|
|
9
|
+
export type { ConnectorExecutor } from "../base/contract.js";
|
|
10
|
+
import type { ObservabilityDatabase } from "../../observability/db/index.js";
|
|
11
|
+
import type { StateDatabase } from "../../storage/db/index.js";
|
|
12
|
+
export interface ConnectorExecutorAdapterOptions {
|
|
13
|
+
stateDb: StateDatabase;
|
|
14
|
+
observabilityDb: ObservabilityDatabase;
|
|
15
|
+
}
|
|
16
|
+
export declare function createConnectorExecutorAdapter(options: ConnectorExecutorAdapterOptions): ConnectorExecutor;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { CapabilityContractRegistry } from "../base/manifest.js";
|
|
2
|
+
import { ConnectorRoutePlanner } from "../base/route-planner.js";
|
|
3
|
+
import { ChannelHealthStore } from "../base/channel-health.js";
|
|
4
|
+
import { createConnectorPolicyLayer } from "../base/policy-layer.js";
|
|
5
|
+
import { InMemoryEffectCommitLedger } from "../base/execution-policy.js";
|
|
6
|
+
import { moltbookManifest } from "../social-community/moltbook/manifest.js";
|
|
7
|
+
import { evomapManifest } from "../agent-network/evomap/manifest.js";
|
|
8
|
+
import { createMoltbookApiClient } from "../social-community/moltbook/api-client.js";
|
|
9
|
+
import { createMoltbookRunner } from "../social-community/moltbook/adapter.js";
|
|
10
|
+
import { ExecutionTelemetry } from "../../observability/services/execution-telemetry.js";
|
|
11
|
+
import { createCredentialVault } from "../../storage/services/credential-vault.js";
|
|
12
|
+
import { createCredentialRouteContextPort } from "./credential-route-context.js";
|
|
13
|
+
function createAdaptiveExecutionRunner(vault) {
|
|
14
|
+
return {
|
|
15
|
+
async run(_plan, request) {
|
|
16
|
+
const platformId = request.platformId;
|
|
17
|
+
const started = Date.now();
|
|
18
|
+
const credential = await vault.loadCredentialContext(platformId);
|
|
19
|
+
if (!credential ||
|
|
20
|
+
credential.status !== "active" ||
|
|
21
|
+
!credential.encryptedValue) {
|
|
22
|
+
return {
|
|
23
|
+
platformId,
|
|
24
|
+
channel: request.preferredChannel ?? "api_rest",
|
|
25
|
+
latencyMs: Date.now() - started,
|
|
26
|
+
success: false,
|
|
27
|
+
error: {
|
|
28
|
+
code: "auth_failure",
|
|
29
|
+
detail: "credential_unavailable_for_execution",
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (platformId === "moltbook") {
|
|
34
|
+
const baseUrl = process.env.SECOND_NATURE_MOLTBOOK_BASE_URL;
|
|
35
|
+
if (!baseUrl) {
|
|
36
|
+
return {
|
|
37
|
+
platformId,
|
|
38
|
+
channel: request.preferredChannel ?? "api_rest",
|
|
39
|
+
latencyMs: Date.now() - started,
|
|
40
|
+
success: false,
|
|
41
|
+
error: {
|
|
42
|
+
code: "configuration_missing",
|
|
43
|
+
detail: "SECOND_NATURE_MOLTBOOK_BASE_URL not set",
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const apiClient = createMoltbookApiClient({
|
|
48
|
+
baseUrl,
|
|
49
|
+
accessToken: credential.encryptedValue,
|
|
50
|
+
timeoutMs: 10000,
|
|
51
|
+
});
|
|
52
|
+
const runner = createMoltbookRunner({
|
|
53
|
+
apiClient,
|
|
54
|
+
skillRunner: {
|
|
55
|
+
run: async () => {
|
|
56
|
+
throw {
|
|
57
|
+
code: "protocol_mismatch",
|
|
58
|
+
detail: "moltbook_skill_runner_not_configured",
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
return runner.run(_plan, request);
|
|
64
|
+
}
|
|
65
|
+
if (platformId === "evomap") {
|
|
66
|
+
return {
|
|
67
|
+
platformId,
|
|
68
|
+
channel: request.preferredChannel ?? "api_rest",
|
|
69
|
+
latencyMs: Date.now() - started,
|
|
70
|
+
success: false,
|
|
71
|
+
error: {
|
|
72
|
+
code: "not_implemented",
|
|
73
|
+
detail: "evomap_execution_runner_not_yet_implemented",
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
platformId,
|
|
79
|
+
channel: request.preferredChannel ?? "api_rest",
|
|
80
|
+
latencyMs: Date.now() - started,
|
|
81
|
+
success: false,
|
|
82
|
+
error: {
|
|
83
|
+
code: "unknown_platform",
|
|
84
|
+
detail: `no execution runner for ${platformId}`,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function createConnectorExecutorAdapter(options) {
|
|
91
|
+
const vault = createCredentialVault(options.stateDb.db);
|
|
92
|
+
const registry = new CapabilityContractRegistry();
|
|
93
|
+
registry.register({ ...moltbookManifest });
|
|
94
|
+
registry.register({ ...evomapManifest });
|
|
95
|
+
const routeContextPort = createCredentialRouteContextPort(vault);
|
|
96
|
+
const routePlanner = new ConnectorRoutePlanner(registry, routeContextPort, new ChannelHealthStore());
|
|
97
|
+
const telemetry = new ExecutionTelemetry(options.observabilityDb);
|
|
98
|
+
const executionRunner = createAdaptiveExecutionRunner(vault);
|
|
99
|
+
const policy = createConnectorPolicyLayer({
|
|
100
|
+
routePlanner,
|
|
101
|
+
executionRunner,
|
|
102
|
+
telemetry,
|
|
103
|
+
effectCommitLedger: new InMemoryEffectCommitLedger(),
|
|
104
|
+
retryPolicy: { maxRetries: 2, jitter: true },
|
|
105
|
+
});
|
|
106
|
+
return {
|
|
107
|
+
async executeEffect(input) {
|
|
108
|
+
return policy.executeWithPolicy(input.intent, {
|
|
109
|
+
platformId: input.platformId,
|
|
110
|
+
intent: input.intent,
|
|
111
|
+
payload: input.payload,
|
|
112
|
+
decisionId: input.decisionId,
|
|
113
|
+
intentId: input.intentId,
|
|
114
|
+
idempotencyKey: input.idempotencyKey,
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge CredentialVault → RouteContextPort for connector route planning.
|
|
3
|
+
*
|
|
4
|
+
* Loads decrypted credentials from state DB and maps them to the
|
|
5
|
+
* CredentialContext shape expected by ConnectorRoutePlanner.
|
|
6
|
+
* Cooldown is stubbed (always unblocked) until a cooldown ledger is modeled.
|
|
7
|
+
*/
|
|
8
|
+
import type { RouteContextPort } from "../base/contract.js";
|
|
9
|
+
import type { CredentialVault } from "../../storage/services/credential-vault.js";
|
|
10
|
+
export declare function createCredentialRouteContextPort(vault: CredentialVault): RouteContextPort;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function createCredentialRouteContextPort(vault) {
|
|
2
|
+
return {
|
|
3
|
+
async loadCredentialState(platformId) {
|
|
4
|
+
const ctx = await vault.loadCredentialContext(platformId);
|
|
5
|
+
// Defensive: some ORM findFirst variants return {} instead of null/undefined.
|
|
6
|
+
if (!ctx || !ctx.platformId || !ctx.status) {
|
|
7
|
+
return {
|
|
8
|
+
platformId,
|
|
9
|
+
status: "missing",
|
|
10
|
+
credentialType: "api_key",
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
return ctx;
|
|
14
|
+
},
|
|
15
|
+
async loadCooldownState() {
|
|
16
|
+
return { blocked: false };
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -19,6 +19,7 @@ import { type HeartbeatRuntimeSnapshot } from "./runtime-snapshot.js";
|
|
|
19
19
|
import type { GuidanceDraftPort } from "../../../guidance/outreach-draft-schema.js";
|
|
20
20
|
import type { StateDatabase } from "../../../storage/db/index.js";
|
|
21
21
|
import { type OpenClawDeliveryPort } from "../outreach/dispatch-user-outreach.js";
|
|
22
|
+
import type { ConnectorExecutor } from "../../../connectors/base/contract.js";
|
|
22
23
|
export interface HeartbeatDecisionTracePayload {
|
|
23
24
|
scope: RuntimeScope;
|
|
24
25
|
status: HeartbeatCycleStatus;
|
|
@@ -44,7 +45,7 @@ export interface HeartbeatQuietWorkflowDeps {
|
|
|
44
45
|
* Resolves the heartbeat outcome for a guard-allowed intent (outreach dispatch, quiet orchestration, or default).
|
|
45
46
|
* Exported for unit tests (CR-M1 wiring).
|
|
46
47
|
*/
|
|
47
|
-
export declare function resolveAllowedIntentResult(intent: CandidateIntent, runtime: HeartbeatRuntimeSnapshot, inputs: SnapshotInputs, signal: HeartbeatSignal, deps: Pick<HeartbeatDeps, "outreachDispatch" | "quietWorkflow">): Promise<HeartbeatCycleResult>;
|
|
48
|
+
export declare function resolveAllowedIntentResult(intent: CandidateIntent, runtime: HeartbeatRuntimeSnapshot, inputs: SnapshotInputs, signal: HeartbeatSignal, deps: Pick<HeartbeatDeps, "outreachDispatch" | "quietWorkflow" | "connectorExecutor">): Promise<HeartbeatCycleResult>;
|
|
48
49
|
export interface HeartbeatDeps {
|
|
49
50
|
/** Load snapshot inputs from state-system */
|
|
50
51
|
loadSnapshotInputs: () => Promise<SnapshotInputs>;
|
|
@@ -52,6 +53,11 @@ export interface HeartbeatDeps {
|
|
|
52
53
|
recordDecisionTrace?: (payload: HeartbeatDecisionTracePayload) => Promise<void>;
|
|
53
54
|
outreachDispatch?: HeartbeatOutreachDispatchDeps;
|
|
54
55
|
quietWorkflow?: HeartbeatQuietWorkflowDeps;
|
|
56
|
+
/**
|
|
57
|
+
* When present, guard-allowed connector_action intents are dispatched
|
|
58
|
+
* through the connector-system instead of returning connector_dispatch_unwired.
|
|
59
|
+
*/
|
|
60
|
+
connectorExecutor?: ConnectorExecutor;
|
|
55
61
|
}
|
|
56
62
|
/**
|
|
57
63
|
* Ingest a heartbeat rhythm signal and drive one full decision round.
|
|
@@ -5,6 +5,7 @@ import { evaluateHardGuards } from "../orchestrator/guard-layer.js";
|
|
|
5
5
|
import { dispatchUserOutreachIntent, } from "../outreach/dispatch-user-outreach.js";
|
|
6
6
|
import { buildJudgeOutreachInputFromSnapshot } from "../outreach/judge-input-from-snapshot.js";
|
|
7
7
|
import { runSourceBackedQuiet } from "../quiet/run-source-backed-quiet.js";
|
|
8
|
+
import { toCapabilityIntent } from "../orchestrator/effect-dispatcher.js";
|
|
8
9
|
/**
|
|
9
10
|
* Resolves the heartbeat outcome for a guard-allowed intent (outreach dispatch, quiet orchestration, or default).
|
|
10
11
|
* Exported for unit tests (CR-M1 wiring).
|
|
@@ -48,6 +49,28 @@ export async function resolveAllowedIntentResult(intent, runtime, inputs, signal
|
|
|
48
49
|
intent.effectClass === "no_effect" ||
|
|
49
50
|
intent.kind === "maintenance";
|
|
50
51
|
const connectorUnwired = intent.effectClass === "connector_action";
|
|
52
|
+
if (connectorUnwired && deps.connectorExecutor) {
|
|
53
|
+
const result = await deps.connectorExecutor.executeEffect({
|
|
54
|
+
platformId: intent.platformId ?? "unknown",
|
|
55
|
+
intent: toCapabilityIntent(intent),
|
|
56
|
+
payload: {},
|
|
57
|
+
decisionId: `decision:${intent.id}:${Date.now()}`,
|
|
58
|
+
intentId: intent.id,
|
|
59
|
+
idempotencyKey: `idem:${intent.id}:${Date.now()}`,
|
|
60
|
+
});
|
|
61
|
+
const base = {
|
|
62
|
+
scope: "rhythm",
|
|
63
|
+
status: "intent_selected",
|
|
64
|
+
selectedIntentId: intent.id,
|
|
65
|
+
decisionId: `decision:${intent.id}:${Date.now()}`,
|
|
66
|
+
reasons: result.status === "success"
|
|
67
|
+
? ["connector_effect_executed"]
|
|
68
|
+
: result.status === "retryable_failure"
|
|
69
|
+
? ["connector_retryable_failure", result.failureClass ?? "unknown"]
|
|
70
|
+
: ["connector_terminal_failure", result.failureClass ?? "unknown"],
|
|
71
|
+
};
|
|
72
|
+
return base;
|
|
73
|
+
}
|
|
51
74
|
const reasons = noExternalEffect
|
|
52
75
|
? ["internal_tick"]
|
|
53
76
|
: connectorUnwired
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { ConnectorResult, CapabilityIntent } from "../../../connectors/base/contract.js";
|
|
1
|
+
import type { ConnectorResult, CapabilityIntent, ConnectorExecutor } from "../../../connectors/base/contract.js";
|
|
2
|
+
export type { ConnectorExecutor } from "../../../connectors/base/contract.js";
|
|
2
3
|
import { LeaseManager, type EffectClass } from "./lease-manager.js";
|
|
3
4
|
export interface AllowedIntent {
|
|
4
5
|
id: string;
|
|
@@ -31,16 +32,6 @@ export interface IntentCommitPort {
|
|
|
31
32
|
}): Promise<void>;
|
|
32
33
|
abortIntentCommit(id: string, reason: string): Promise<void>;
|
|
33
34
|
}
|
|
34
|
-
export interface ConnectorExecutor {
|
|
35
|
-
executeEffect(input: {
|
|
36
|
-
platformId: string;
|
|
37
|
-
intent: CapabilityIntent;
|
|
38
|
-
payload: Record<string, unknown>;
|
|
39
|
-
decisionId: string;
|
|
40
|
-
intentId: string;
|
|
41
|
-
idempotencyKey: string;
|
|
42
|
-
}): Promise<ConnectorResult<unknown>>;
|
|
43
|
-
}
|
|
44
35
|
export interface CheckpointPort {
|
|
45
36
|
saveCheckpoint(input: {
|
|
46
37
|
id: string;
|
|
@@ -83,6 +74,7 @@ export type DispatchResult = {
|
|
|
83
74
|
status: "maintenance_done";
|
|
84
75
|
commitId: string;
|
|
85
76
|
};
|
|
77
|
+
export declare function toCapabilityIntent(intent: Pick<AllowedIntent, "kind">): CapabilityIntent;
|
|
86
78
|
export declare class EffectDispatcher {
|
|
87
79
|
private readonly leaseManager;
|
|
88
80
|
private readonly commitPort;
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import * as crypto from "crypto";
|
|
2
2
|
function needsLease(effectClass) {
|
|
3
|
-
return effectClass === "external_platform_action" ||
|
|
3
|
+
return (effectClass === "external_platform_action" ||
|
|
4
|
+
effectClass === "connector_action" ||
|
|
5
|
+
effectClass === "user_outreach");
|
|
4
6
|
}
|
|
5
7
|
function needsCheckpoint(effectClass) {
|
|
6
8
|
return effectClass !== "maintenance" && effectClass !== "no_effect";
|
|
7
9
|
}
|
|
8
10
|
function isConnectorEffect(effectClass) {
|
|
9
|
-
return effectClass === "external_platform_action" ||
|
|
11
|
+
return (effectClass === "external_platform_action" ||
|
|
12
|
+
effectClass === "connector_action");
|
|
10
13
|
}
|
|
11
|
-
function toCapabilityIntent(intent) {
|
|
14
|
+
export function toCapabilityIntent(intent) {
|
|
12
15
|
if (intent.kind === "work")
|
|
13
16
|
return "work.discover";
|
|
14
17
|
if (intent.kind === "exploration")
|
|
@@ -48,7 +51,9 @@ export class EffectDispatcher {
|
|
|
48
51
|
id: decision.checkpointId,
|
|
49
52
|
tickId: decision.tickId,
|
|
50
53
|
intentId: decision.intentId,
|
|
51
|
-
phase: isConnectorEffect(intent.effectClass)
|
|
54
|
+
phase: isConnectorEffect(intent.effectClass)
|
|
55
|
+
? "before_effect"
|
|
56
|
+
: "before_quiet_write",
|
|
52
57
|
snapshotRef: decision.traceId,
|
|
53
58
|
});
|
|
54
59
|
}
|
|
@@ -27,7 +27,7 @@ export function startRuntimeService(ctx) {
|
|
|
27
27
|
// - control-plane-system (heartbeat bridge preparation)
|
|
28
28
|
const workspaceRoot = ctx?.workspaceRoot ?? process.cwd();
|
|
29
29
|
/** Keep in sync with `plugin/package.json` when cutting releases. */
|
|
30
|
-
const version = "0.1.
|
|
30
|
+
const version = "0.1.20";
|
|
31
31
|
activeHandle = {
|
|
32
32
|
ready: true,
|
|
33
33
|
version,
|