@haaaiawd/second-nature 0.1.27 → 0.1.29

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 (157) hide show
  1. package/SKILL.md +35 -33
  2. package/agent-inner-guide.md +144 -124
  3. package/index.js +76 -1
  4. package/openclaw.plugin.json +2 -2
  5. package/package.json +2 -1
  6. package/runtime/cli/commands/connector-behavior.d.ts +20 -0
  7. package/runtime/cli/commands/connector-behavior.js +160 -0
  8. package/runtime/cli/commands/index.js +8 -0
  9. package/runtime/cli/index.js +9 -2
  10. package/runtime/cli/ops/manual-run-dispatcher.d.ts +79 -0
  11. package/runtime/cli/ops/manual-run-dispatcher.js +110 -0
  12. package/runtime/cli/ops/ops-router.d.ts +45 -4
  13. package/runtime/cli/ops/ops-router.js +543 -2
  14. package/runtime/cli/read-models/index.js +35 -18
  15. package/runtime/cli/read-models/types.d.ts +1 -0
  16. package/runtime/connectors/agent-network/agent-world/adapter.d.ts +1 -0
  17. package/runtime/connectors/agent-network/agent-world/adapter.js +2 -2
  18. package/runtime/connectors/base/contract.d.ts +4 -1
  19. package/runtime/connectors/base/contract.js +5 -1
  20. package/runtime/connectors/base/effect-commit-ledger-sqlite.d.ts +31 -0
  21. package/runtime/connectors/base/effect-commit-ledger-sqlite.js +86 -0
  22. package/runtime/connectors/base/failure-taxonomy.js +5 -0
  23. package/runtime/connectors/base/manifest-v7.d.ts +151 -0
  24. package/runtime/connectors/base/manifest-v7.js +170 -0
  25. package/runtime/connectors/base/manifest.d.ts +67 -77
  26. package/runtime/connectors/base/manifest.js +7 -7
  27. package/runtime/connectors/base/route-planner.js +11 -8
  28. package/runtime/connectors/base/structured-unavailable-reason.d.ts +59 -0
  29. package/runtime/connectors/base/structured-unavailable-reason.js +113 -0
  30. package/runtime/connectors/base/wet-probe-runner.d.ts +40 -0
  31. package/runtime/connectors/base/wet-probe-runner.js +132 -0
  32. package/runtime/connectors/manifest/manifest-schema.d.ts +4 -0
  33. package/runtime/connectors/manifest/manifest-schema.js +2 -0
  34. package/runtime/connectors/services/connector-executor-adapter.d.ts +1 -0
  35. package/runtime/connectors/services/connector-executor-adapter.js +132 -26
  36. package/runtime/core/second-nature/body/behavior-promotion/behavior-promotion-loop.d.ts +45 -0
  37. package/runtime/core/second-nature/body/behavior-promotion/behavior-promotion-loop.js +132 -0
  38. package/runtime/core/second-nature/body/circuit-breaker/circuit-breaker-manager.d.ts +60 -0
  39. package/runtime/core/second-nature/body/circuit-breaker/circuit-breaker-manager.js +174 -0
  40. package/runtime/core/second-nature/body/probe-signal-adapter.d.ts +38 -0
  41. package/runtime/core/second-nature/body/probe-signal-adapter.js +60 -0
  42. package/runtime/core/second-nature/body/tool-affordance/affordance-assembler.d.ts +51 -0
  43. package/runtime/core/second-nature/body/tool-affordance/affordance-assembler.js +129 -0
  44. package/runtime/core/second-nature/body/tool-affordance/affordance-context-scope.d.ts +30 -0
  45. package/runtime/core/second-nature/body/tool-affordance/affordance-context-scope.js +92 -0
  46. package/runtime/core/second-nature/body/tool-experience/experience-writer.d.ts +34 -0
  47. package/runtime/core/second-nature/body/tool-experience/experience-writer.js +67 -0
  48. package/runtime/core/second-nature/body/tool-experience/pain-signal-query.d.ts +37 -0
  49. package/runtime/core/second-nature/body/tool-experience/pain-signal-query.js +62 -0
  50. package/runtime/core/second-nature/heartbeat/decision-trace-emitter.d.ts +29 -0
  51. package/runtime/core/second-nature/heartbeat/decision-trace-emitter.js +28 -0
  52. package/runtime/core/second-nature/heartbeat/embodied-context-assembler.d.ts +54 -0
  53. package/runtime/core/second-nature/heartbeat/embodied-context-assembler.js +164 -0
  54. package/runtime/core/second-nature/heartbeat/goal-lifecycle-policy.d.ts +37 -0
  55. package/runtime/core/second-nature/heartbeat/goal-lifecycle-policy.js +61 -0
  56. package/runtime/core/second-nature/heartbeat/idle-curiosity-policy.d.ts +37 -0
  57. package/runtime/core/second-nature/heartbeat/idle-curiosity-policy.js +60 -0
  58. package/runtime/core/second-nature/heartbeat/index.d.ts +4 -0
  59. package/runtime/core/second-nature/heartbeat/index.js +5 -0
  60. package/runtime/core/second-nature/heartbeat/run-heartbeat-cycle-v7.d.ts +63 -0
  61. package/runtime/core/second-nature/heartbeat/run-heartbeat-cycle-v7.js +118 -0
  62. package/runtime/core/second-nature/orchestrator/downstream-intent-orchestrator.d.ts +41 -0
  63. package/runtime/core/second-nature/orchestrator/downstream-intent-orchestrator.js +43 -0
  64. package/runtime/core/second-nature/orchestrator/effect-dispatcher.d.ts +2 -1
  65. package/runtime/core/second-nature/orchestrator/effect-dispatcher.js +2 -0
  66. package/runtime/core/second-nature/orchestrator/hard-guard-evaluator.d.ts +31 -0
  67. package/runtime/core/second-nature/orchestrator/hard-guard-evaluator.js +102 -0
  68. package/runtime/core/second-nature/orchestrator/index.d.ts +5 -0
  69. package/runtime/core/second-nature/orchestrator/index.js +7 -0
  70. package/runtime/core/second-nature/quiet/claim-synthesizer.d.ts +53 -0
  71. package/runtime/core/second-nature/quiet/claim-synthesizer.js +153 -0
  72. package/runtime/core/second-nature/quiet/daily-diary-writer.d.ts +29 -0
  73. package/runtime/core/second-nature/quiet/daily-diary-writer.js +92 -0
  74. package/runtime/core/second-nature/quiet/index.d.ts +5 -0
  75. package/runtime/core/second-nature/quiet/index.js +5 -0
  76. package/runtime/core/second-nature/quiet/run-source-backed-quiet.js +19 -12
  77. package/runtime/core/second-nature/types.d.ts +2 -0
  78. package/runtime/guidance/channel-feedback-ingestion-service.d.ts +88 -0
  79. package/runtime/guidance/channel-feedback-ingestion-service.js +231 -0
  80. package/runtime/guidance/guidance-draft-service.d.ts +60 -0
  81. package/runtime/guidance/guidance-draft-service.js +80 -0
  82. package/runtime/guidance/index.d.ts +3 -0
  83. package/runtime/guidance/index.js +3 -0
  84. package/runtime/guidance/outreach-draft-schema.d.ts +8 -8
  85. package/runtime/guidance/outreach-strategy-selector.d.ts +77 -0
  86. package/runtime/guidance/outreach-strategy-selector.js +211 -0
  87. package/runtime/observability/audit/append-only-audit-store.d.ts +20 -2
  88. package/runtime/observability/audit/append-only-audit-store.js +32 -6
  89. package/runtime/observability/audit/audit-envelope.d.ts +2 -1
  90. package/runtime/observability/audit/audit-envelope.js +8 -7
  91. package/runtime/observability/audit/audit-family-registry.json +66 -0
  92. package/runtime/observability/audit/family-registry.d.ts +43 -0
  93. package/runtime/observability/audit/family-registry.js +70 -0
  94. package/runtime/observability/index.d.ts +6 -1
  95. package/runtime/observability/index.js +6 -1
  96. package/runtime/observability/redaction/policy.d.ts +24 -3
  97. package/runtime/observability/redaction/policy.js +74 -0
  98. package/runtime/observability/services/heartbeat-digest-assembler.d.ts +152 -0
  99. package/runtime/observability/services/heartbeat-digest-assembler.js +248 -0
  100. package/runtime/observability/services/lived-experience-audit.js +6 -6
  101. package/runtime/observability/services/narrative-timeline-query-service.d.ts +136 -0
  102. package/runtime/observability/services/narrative-timeline-query-service.js +169 -0
  103. package/runtime/observability/services/restore-audit-service.d.ts +74 -0
  104. package/runtime/observability/services/restore-audit-service.js +79 -0
  105. package/runtime/observability/services/runtime-secret-anchor-view.d.ts +77 -0
  106. package/runtime/observability/services/runtime-secret-anchor-view.js +168 -0
  107. package/runtime/observability/services/self-health-snapshot.d.ts +92 -0
  108. package/runtime/observability/services/self-health-snapshot.js +251 -0
  109. package/runtime/shared/types/goal.d.ts +62 -0
  110. package/runtime/shared/types/goal.js +20 -0
  111. package/runtime/shared/types/index.d.ts +3 -0
  112. package/runtime/shared/types/index.js +3 -0
  113. package/runtime/shared/types/source-ref.d.ts +14 -0
  114. package/runtime/shared/types/source-ref.js +1 -0
  115. package/runtime/shared/types/v7-entities.d.ts +206 -0
  116. package/runtime/shared/types/v7-entities.js +27 -0
  117. package/runtime/storage/db/index.js +3 -0
  118. package/runtime/storage/db/migration-runner.d.ts +30 -0
  119. package/runtime/storage/db/migration-runner.js +93 -0
  120. package/runtime/storage/db/migrations/index.d.ts +5 -0
  121. package/runtime/storage/db/migrations/index.js +13 -0
  122. package/runtime/storage/db/migrations/v7-001-foundation.d.ts +13 -0
  123. package/runtime/storage/db/migrations/v7-001-foundation.js +144 -0
  124. package/runtime/storage/db/migrations/v7-002-effect-commit-ledger.d.ts +8 -0
  125. package/runtime/storage/db/migrations/v7-002-effect-commit-ledger.js +27 -0
  126. package/runtime/storage/db/migrations/v7-003-circuit-breaker.d.ts +7 -0
  127. package/runtime/storage/db/migrations/v7-003-circuit-breaker.js +26 -0
  128. package/runtime/storage/db/migrations/v7-004-behavior-promotion.d.ts +7 -0
  129. package/runtime/storage/db/migrations/v7-004-behavior-promotion.js +26 -0
  130. package/runtime/storage/db/schema/agent-goal.d.ts +38 -0
  131. package/runtime/storage/db/schema/agent-goal.js +2 -0
  132. package/runtime/storage/db/transaction-utils.d.ts +14 -0
  133. package/runtime/storage/db/transaction-utils.js +29 -0
  134. package/runtime/storage/db/write-queue.d.ts +38 -0
  135. package/runtime/storage/db/write-queue.js +97 -0
  136. package/runtime/storage/quiet/persist-quiet-artifact.js +2 -1
  137. package/runtime/storage/services/diary-dream-store.d.ts +35 -0
  138. package/runtime/storage/services/diary-dream-store.js +165 -0
  139. package/runtime/storage/services/embodied-context-state-port.d.ts +77 -0
  140. package/runtime/storage/services/embodied-context-state-port.js +115 -0
  141. package/runtime/storage/services/goal-lifecycle-store.d.ts +42 -0
  142. package/runtime/storage/services/goal-lifecycle-store.js +181 -0
  143. package/runtime/storage/services/history-digest-store.d.ts +33 -0
  144. package/runtime/storage/services/history-digest-store.js +140 -0
  145. package/runtime/storage/services/identity-profile-store.d.ts +25 -0
  146. package/runtime/storage/services/identity-profile-store.js +81 -0
  147. package/runtime/storage/services/interaction-snapshot-projector.d.ts +15 -0
  148. package/runtime/storage/services/interaction-snapshot-projector.js +35 -0
  149. package/runtime/storage/services/restore-snapshot-store.d.ts +52 -0
  150. package/runtime/storage/services/restore-snapshot-store.js +193 -0
  151. package/runtime/storage/services/runtime-secret-anchor-store.d.ts +26 -0
  152. package/runtime/storage/services/runtime-secret-anchor-store.js +82 -0
  153. package/runtime/storage/services/tool-experience-store.d.ts +25 -0
  154. package/runtime/storage/services/tool-experience-store.js +116 -0
  155. package/runtime/storage/services/write-validation-gate.d.ts +46 -0
  156. package/runtime/storage/services/write-validation-gate.js +200 -0
  157. package/workspace-ops-bridge.js +16 -1
@@ -12,11 +12,114 @@ import { createAgentWorldRunner } from "../agent-network/agent-world/adapter.js"
12
12
  import { ExecutionTelemetry } from "../../observability/services/execution-telemetry.js";
13
13
  import { createCredentialVault } from "../../storage/services/credential-vault.js";
14
14
  import { createCredentialRouteContextPort } from "./credential-route-context.js";
15
+ import { scanConnectorManifests } from "../registry/manifest-scanner.js";
16
+ import { parseConnectorManifestV6 } from "../manifest/manifest-parser.js";
17
+ const DEFAULT_AGENT_WORLD_USERNAME = "nyx_ha";
18
+ const DEFAULT_AGENT_WORLD_PROFILE_PATH_TEMPLATE = "/api/agents/profile/{username}";
19
+ function readString(value) {
20
+ return typeof value === "string" && value.trim().length > 0
21
+ ? value.trim()
22
+ : undefined;
23
+ }
24
+ function channelPriorityForRunner(manifest) {
25
+ const declared = manifest.capabilities
26
+ .map((capability) => capability.channel)
27
+ .filter((channel) => channel === "api_rest" ||
28
+ channel === "api_rpc" ||
29
+ channel === "a2a" ||
30
+ channel === "mcp" ||
31
+ channel === "cli" ||
32
+ channel === "skill" ||
33
+ channel === "browser");
34
+ if (declared.length > 0)
35
+ return [...new Set(declared)];
36
+ if (manifest.runner.kind === "declarative_a2a")
37
+ return ["a2a"];
38
+ if (manifest.runner.kind === "declarative_mcp")
39
+ return ["mcp"];
40
+ if (manifest.runner.kind === "cli_descriptor")
41
+ return ["cli"];
42
+ if (manifest.runner.kind === "skill")
43
+ return ["skill"];
44
+ if (manifest.runner.kind === "browser")
45
+ return ["browser"];
46
+ return ["api_rest"];
47
+ }
48
+ function registerWorkspaceManifests(registry, workspaceRoot) {
49
+ if (!workspaceRoot)
50
+ return;
51
+ for (const file of scanConnectorManifests(workspaceRoot)) {
52
+ const parsed = parseConnectorManifestV6(file.content, file.path);
53
+ if (!parsed.ok)
54
+ continue;
55
+ const manifest = parsed.manifest;
56
+ try {
57
+ registry.register({
58
+ platformId: manifest.platformId,
59
+ supportedCapabilities: manifest.capabilities.map((capability) => capability.id),
60
+ channelPriority: channelPriorityForRunner(manifest),
61
+ credentialTypes: manifest.credentials.map((credential) => credential.type),
62
+ sourceRefPolicy: manifest.sourceRefPolicy,
63
+ });
64
+ }
65
+ catch {
66
+ // Invalid workspace manifests remain visible through connector_status validation.
67
+ // Execution side keeps fail-closed behavior by not registering them here.
68
+ }
69
+ }
70
+ }
71
+ function resolveAgentWorldUsername(payload, purpose) {
72
+ const payloadUsername = (purpose === "discover" ? readString(payload.targetUsername) : undefined) ??
73
+ readString(payload.username) ??
74
+ readString(payload.agentUsername);
75
+ return (payloadUsername ??
76
+ readString(process.env.SECOND_NATURE_AGENT_WORLD_USERNAME) ??
77
+ DEFAULT_AGENT_WORLD_USERNAME);
78
+ }
79
+ function resolveAgentWorldProfilePath(payload, username) {
80
+ const template = readString(payload.profilePathTemplate) ??
81
+ readString(process.env.SECOND_NATURE_AGENT_WORLD_PROFILE_PATH_TEMPLATE) ??
82
+ DEFAULT_AGENT_WORLD_PROFILE_PATH_TEMPLATE;
83
+ return template.replaceAll("{username}", encodeURIComponent(username));
84
+ }
85
+ function joinAgentWorldUrl(baseUrl, path) {
86
+ if (/^https?:\/\//i.test(path))
87
+ return path;
88
+ return `${baseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
89
+ }
90
+ async function fetchAgentWorldJson(input) {
91
+ const resp = await fetch(joinAgentWorldUrl(input.baseUrl, input.path), {
92
+ method: input.method ?? "GET",
93
+ headers: {
94
+ "Authorization": `Bearer ${input.apiKey}`,
95
+ "Content-Type": "application/json",
96
+ },
97
+ body: input.body === undefined ? undefined : JSON.stringify(input.body),
98
+ });
99
+ if (!resp.ok) {
100
+ throw { code: "api_error", detail: `agent-world ${input.label}: ${resp.status}` };
101
+ }
102
+ return resp.json();
103
+ }
15
104
  function createAdaptiveExecutionRunner(vault) {
16
105
  return {
17
106
  async run(_plan, request) {
18
107
  const platformId = request.platformId;
19
108
  const started = Date.now();
109
+ if (platformId !== "moltbook" &&
110
+ platformId !== "evomap" &&
111
+ platformId !== "agent-world") {
112
+ return {
113
+ platformId,
114
+ channel: request.preferredChannel ?? "api_rest",
115
+ latencyMs: Date.now() - started,
116
+ success: false,
117
+ error: {
118
+ code: "unknown_platform",
119
+ detail: `no execution runner for ${platformId}`,
120
+ },
121
+ };
122
+ }
20
123
  const credential = await vault.loadCredentialContext(platformId);
21
124
  if (!credential ||
22
125
  credential.status !== "active" ||
@@ -91,47 +194,48 @@ function createAdaptiveExecutionRunner(vault) {
91
194
  };
92
195
  }
93
196
  const runner = createAgentWorldRunner({
197
+ apiKey: credential.encryptedValue,
94
198
  apiClient: {
95
199
  async readFeed(payload, _apiKey) {
96
- const resp = await fetch(`${baseUrl}/api/v1/feed`, {
97
- headers: { "Authorization": `Bearer ${_apiKey}`, "Content-Type": "application/json" },
200
+ const username = resolveAgentWorldUsername(payload, "feed");
201
+ return fetchAgentWorldJson({
202
+ baseUrl,
203
+ path: resolveAgentWorldProfilePath(payload, username),
204
+ apiKey: _apiKey,
205
+ label: "profile feed",
98
206
  });
99
- if (!resp.ok)
100
- throw { code: "api_error", detail: `agent-world feed: ${resp.status}` };
101
- return resp.json();
102
207
  },
103
208
  async discoverWork(payload, _apiKey) {
104
- const resp = await fetch(`${baseUrl}/api/v1/work`, {
105
- headers: { "Authorization": `Bearer ${_apiKey}`, "Content-Type": "application/json" },
209
+ const username = resolveAgentWorldUsername(payload, "discover");
210
+ return fetchAgentWorldJson({
211
+ baseUrl,
212
+ path: resolveAgentWorldProfilePath(payload, username),
213
+ apiKey: _apiKey,
214
+ label: "profile discover",
106
215
  });
107
- if (!resp.ok)
108
- throw { code: "api_error", detail: `agent-world work: ${resp.status}` };
109
- return resp.json();
110
216
  },
111
217
  async claimTask(payload, _apiKey) {
112
- const resp = await fetch(`${baseUrl}/api/v1/tasks/${payload.taskId ?? "unknown"}/claim`, {
218
+ const claimPath = readString(payload.claimEndpointPath);
219
+ if (!claimPath) {
220
+ throw {
221
+ code: "protocol_mismatch",
222
+ detail: "agent_world_task_claim_endpoint_not_configured",
223
+ };
224
+ }
225
+ return fetchAgentWorldJson({
226
+ baseUrl,
227
+ path: claimPath,
228
+ apiKey: _apiKey,
113
229
  method: "POST",
114
- headers: { "Authorization": `Bearer ${_apiKey}`, "Content-Type": "application/json" },
115
- body: JSON.stringify(payload),
230
+ body: payload,
231
+ label: "task claim",
116
232
  });
117
- if (!resp.ok)
118
- throw { code: "api_error", detail: `agent-world claim: ${resp.status}` };
119
- return resp.json();
120
233
  },
121
234
  },
122
235
  });
123
236
  return runner.run(_plan, request);
124
237
  }
125
- return {
126
- platformId,
127
- channel: request.preferredChannel ?? "api_rest",
128
- latencyMs: Date.now() - started,
129
- success: false,
130
- error: {
131
- code: "unknown_platform",
132
- detail: `no execution runner for ${platformId}`,
133
- },
134
- };
238
+ throw new Error(`unreachable_connector_platform:${platformId}`);
135
239
  },
136
240
  };
137
241
  }
@@ -141,6 +245,7 @@ export function createConnectorExecutorAdapter(options) {
141
245
  registry.register({ ...moltbookManifest });
142
246
  registry.register({ ...evomapManifest });
143
247
  registry.register({ ...agentWorldManifest });
248
+ registerWorkspaceManifests(registry, options.workspaceRoot);
144
249
  const routeContextPort = createCredentialRouteContextPort(vault);
145
250
  const routePlanner = new ConnectorRoutePlanner(registry, routeContextPort, new ChannelHealthStore());
146
251
  const telemetry = new ExecutionTelemetry(options.observabilityDb);
@@ -154,6 +259,7 @@ export function createConnectorExecutorAdapter(options) {
154
259
  });
155
260
  return {
156
261
  async executeEffect(input) {
262
+ registerWorkspaceManifests(registry, options.workspaceRoot);
157
263
  return policy.executeWithPolicy(input.intent, {
158
264
  platformId: input.platformId,
159
265
  intent: input.intent,
@@ -0,0 +1,45 @@
1
+ /**
2
+ * BehaviorPromotionLoop — T-BTS.C.3
3
+ *
4
+ * Core logic: Operator-authorized behavior suggestion lifecycle.
5
+ * - candidate → approved (idempotent: repeated approve returns existing)
6
+ * - candidate → rejected (with reason)
7
+ * - candidate → expired (7 days TTL from submission)
8
+ * - rejected/expired are read-only; new submit creates a fresh candidate
9
+ *
10
+ * Dependencies:
11
+ * - `StateDatabase` from `../../../../storage/db/index.js`
12
+ *
13
+ * Boundary:
14
+ * - Does NOT grant execution authorization; approval is a bookkeeping signal.
15
+ * - Only accepts operator-authorized suggestions, not connector auto-probe results.
16
+ *
17
+ * Test coverage: tests/unit/body/behavior-promotion-loop.test.ts
18
+ */
19
+ import type { StateDatabase } from "../../../../storage/db/index.js";
20
+ export type PromotionStatus = "candidate" | "approved" | "rejected" | "expired";
21
+ export interface BehaviorPromotion {
22
+ promotionId: string;
23
+ behaviorKind: string;
24
+ description: string;
25
+ status: PromotionStatus;
26
+ operatorId?: string;
27
+ rejectReason?: string;
28
+ submittedAt: string;
29
+ decidedAt?: string;
30
+ expiresAt: string;
31
+ }
32
+ export interface BehaviorPromotionLoop {
33
+ submitPromotion(input: {
34
+ promotionId: string;
35
+ behaviorKind: string;
36
+ description: string;
37
+ operatorId?: string;
38
+ }): Promise<BehaviorPromotion>;
39
+ approvePromotion(promotionId: string): Promise<BehaviorPromotion>;
40
+ rejectPromotion(promotionId: string, reason: string): Promise<BehaviorPromotion>;
41
+ loadPromotion(promotionId: string): Promise<BehaviorPromotion | undefined>;
42
+ listPromotions(status?: PromotionStatus): Promise<BehaviorPromotion[]>;
43
+ expireStaleCandidates(): Promise<number>;
44
+ }
45
+ export declare function createBehaviorPromotionLoop(database: StateDatabase): BehaviorPromotionLoop;
@@ -0,0 +1,132 @@
1
+ /**
2
+ * BehaviorPromotionLoop — T-BTS.C.3
3
+ *
4
+ * Core logic: Operator-authorized behavior suggestion lifecycle.
5
+ * - candidate → approved (idempotent: repeated approve returns existing)
6
+ * - candidate → rejected (with reason)
7
+ * - candidate → expired (7 days TTL from submission)
8
+ * - rejected/expired are read-only; new submit creates a fresh candidate
9
+ *
10
+ * Dependencies:
11
+ * - `StateDatabase` from `../../../../storage/db/index.js`
12
+ *
13
+ * Boundary:
14
+ * - Does NOT grant execution authorization; approval is a bookkeeping signal.
15
+ * - Only accepts operator-authorized suggestions, not connector auto-probe results.
16
+ *
17
+ * Test coverage: tests/unit/body/behavior-promotion-loop.test.ts
18
+ */
19
+ const DEFAULT_TTL_DAYS = 7;
20
+ function rowToPromotion(row, cols) {
21
+ const get = (name) => row[cols.indexOf(name)];
22
+ return {
23
+ promotionId: get("promotion_id"),
24
+ behaviorKind: get("behavior_kind"),
25
+ description: get("description"),
26
+ status: get("status"),
27
+ operatorId: get("operator_id") ?? undefined,
28
+ rejectReason: get("reject_reason") ?? undefined,
29
+ submittedAt: get("submitted_at"),
30
+ decidedAt: get("decided_at") ?? undefined,
31
+ expiresAt: get("expires_at"),
32
+ };
33
+ }
34
+ export function createBehaviorPromotionLoop(database) {
35
+ const { sqlite } = database;
36
+ function loadRecord(promotionId) {
37
+ const result = sqlite.exec(`SELECT * FROM behavior_promotion WHERE promotion_id = ?`, [promotionId]);
38
+ if (result.length === 0 || result[0].values.length === 0) {
39
+ return undefined;
40
+ }
41
+ return rowToPromotion(result[0].values[0], result[0].columns);
42
+ }
43
+ function saveStatus(promotionId, status, decidedAt, rejectReason) {
44
+ sqlite.run(`UPDATE behavior_promotion
45
+ SET status = ?, decided_at = ?, reject_reason = ?
46
+ WHERE promotion_id = ?`, [status, decidedAt, rejectReason ?? null, promotionId]);
47
+ }
48
+ return {
49
+ async submitPromotion(input) {
50
+ const now = new Date().toISOString();
51
+ const expiresAt = new Date(Date.now() + DEFAULT_TTL_DAYS * 24 * 60 * 60 * 1000).toISOString();
52
+ sqlite.run(`INSERT INTO behavior_promotion
53
+ (promotion_id, behavior_kind, description, status,
54
+ operator_id, submitted_at, expires_at)
55
+ VALUES (?, ?, ?, ?, ?, ?, ?)`, [
56
+ input.promotionId,
57
+ input.behaviorKind,
58
+ input.description,
59
+ "candidate",
60
+ input.operatorId ?? null,
61
+ now,
62
+ expiresAt,
63
+ ]);
64
+ return {
65
+ promotionId: input.promotionId,
66
+ behaviorKind: input.behaviorKind,
67
+ description: input.description,
68
+ status: "candidate",
69
+ operatorId: input.operatorId,
70
+ submittedAt: now,
71
+ expiresAt,
72
+ };
73
+ },
74
+ async approvePromotion(promotionId) {
75
+ const rec = loadRecord(promotionId);
76
+ if (!rec) {
77
+ throw new Error(`promotion_not_found:${promotionId}`);
78
+ }
79
+ if (rec.status === "approved") {
80
+ return rec; // idempotent
81
+ }
82
+ if (rec.status === "rejected" || rec.status === "expired") {
83
+ throw new Error(`promotion_immutable:${promotionId}:${rec.status}`);
84
+ }
85
+ const now = new Date().toISOString();
86
+ saveStatus(promotionId, "approved", now);
87
+ return { ...rec, status: "approved", decidedAt: now };
88
+ },
89
+ async rejectPromotion(promotionId, reason) {
90
+ const rec = loadRecord(promotionId);
91
+ if (!rec) {
92
+ throw new Error(`promotion_not_found:${promotionId}`);
93
+ }
94
+ if (rec.status === "rejected") {
95
+ return rec; // idempotent
96
+ }
97
+ if (rec.status === "approved" || rec.status === "expired") {
98
+ throw new Error(`promotion_immutable:${promotionId}:${rec.status}`);
99
+ }
100
+ const now = new Date().toISOString();
101
+ saveStatus(promotionId, "rejected", now, reason);
102
+ return { ...rec, status: "rejected", decidedAt: now, rejectReason: reason };
103
+ },
104
+ async loadPromotion(promotionId) {
105
+ return loadRecord(promotionId);
106
+ },
107
+ async listPromotions(status) {
108
+ let sql = `SELECT * FROM behavior_promotion`;
109
+ const params = [];
110
+ if (status) {
111
+ sql += ` WHERE status = ?`;
112
+ params.push(status);
113
+ }
114
+ sql += ` ORDER BY submitted_at DESC`;
115
+ const result = sqlite.exec(sql, params);
116
+ if (result.length === 0 || result[0].values.length === 0) {
117
+ return [];
118
+ }
119
+ return result[0].values.map((row) => rowToPromotion(row, result[0].columns));
120
+ },
121
+ async expireStaleCandidates() {
122
+ const now = new Date().toISOString();
123
+ sqlite.run(`UPDATE behavior_promotion
124
+ SET status = 'expired', decided_at = ?
125
+ WHERE status = 'candidate' AND expires_at < ?`, [now, now]);
126
+ // sql.js does not provide changes count easily; approximate via re-query
127
+ const result = sqlite.exec(`SELECT COUNT(*) as cnt FROM behavior_promotion
128
+ WHERE status = 'expired' AND decided_at = ?`, [now]);
129
+ return result[0]?.values[0]?.[0] ?? 0;
130
+ },
131
+ };
132
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * CircuitBreakerManager — T-BTS.C.5
3
+ *
4
+ * Core logic: State machine (Closed → Open → HalfOpen → Closed/Open).
5
+ * - Closed: counts consecutive failures; threshold hit → Open.
6
+ * - Open: rejects execution; cooldown elapsed → HalfOpen.
7
+ * - HalfOpen: initiates runWetProbe via ProbeSignalAdapter.
8
+ * - strict side-effect → probe_policy_denied, stays HalfOpen.
9
+ * - probe success → Closed + invalidate affordance cache (DR-003).
10
+ * - probe failure → Open.
11
+ *
12
+ * Persistence:
13
+ * - State stored in SQLite `circuit_breaker_state` table (v7-003).
14
+ * - Loads previous state on first access.
15
+ *
16
+ * Dependencies:
17
+ * - `StateDatabase` from `../../../../storage/db/index.js`
18
+ * - `WetProbeRunner` from `../../../../connectors/base/wet-probe-runner.js`
19
+ * - `CapabilityContractRegistryV7` from `../../../../connectors/base/manifest-v7.js`
20
+ * - `ProbeSignalAdapter` from `../probe-signal-adapter.js`
21
+ *
22
+ * Boundary:
23
+ * - Manager decides WHEN to probe (DR-002); connector-system executes it.
24
+ * - Does NOT execute HTTP directly — delegates to ProbeSignalAdapter.
25
+ *
26
+ * Test coverage: tests/unit/body/circuit-breaker-manager.test.ts
27
+ */
28
+ import type { StateDatabase } from "../../../../storage/db/index.js";
29
+ import type { CapabilityContractRegistryV7 } from "../../../../connectors/base/manifest-v7.js";
30
+ import type { ProbeSignalAdapter } from "../probe-signal-adapter.js";
31
+ export type BreakerState = "closed" | "open" | "half_open";
32
+ export interface BreakerRecord {
33
+ platformId: string;
34
+ capabilityId: string;
35
+ state: BreakerState;
36
+ failureCount: number;
37
+ consecutiveFailures: number;
38
+ lastFailureAt?: string;
39
+ openedAt?: string;
40
+ lastProbeAt?: string;
41
+ }
42
+ export interface CircuitBreakerManager {
43
+ evaluateFailure(platformId: string, capabilityId: string): Promise<BreakerState>;
44
+ evaluateSuccess(platformId: string, capabilityId: string): Promise<BreakerState>;
45
+ canExecute(platformId: string, capabilityId: string): Promise<boolean>;
46
+ getState(platformId: string, capabilityId: string): Promise<BreakerState>;
47
+ attemptReset(platformId: string, capabilityId: string): Promise<BreakerState>;
48
+ }
49
+ export interface CircuitBreakerManagerOptions {
50
+ database: StateDatabase;
51
+ probeAdapter: ProbeSignalAdapter;
52
+ registry: CapabilityContractRegistryV7;
53
+ /** Consecutive failures before opening. Default 3. */
54
+ failureThreshold?: number;
55
+ /** Cooldown in ms before HalfOpen. Default 30_000. */
56
+ cooldownMs?: number;
57
+ /** Callback when breaker transitions to Closed (for affordance cache invalidation). */
58
+ onClosed?: (platformId: string, capabilityId: string) => void;
59
+ }
60
+ export declare function createCircuitBreakerManager(options: CircuitBreakerManagerOptions): CircuitBreakerManager;
@@ -0,0 +1,174 @@
1
+ /**
2
+ * CircuitBreakerManager — T-BTS.C.5
3
+ *
4
+ * Core logic: State machine (Closed → Open → HalfOpen → Closed/Open).
5
+ * - Closed: counts consecutive failures; threshold hit → Open.
6
+ * - Open: rejects execution; cooldown elapsed → HalfOpen.
7
+ * - HalfOpen: initiates runWetProbe via ProbeSignalAdapter.
8
+ * - strict side-effect → probe_policy_denied, stays HalfOpen.
9
+ * - probe success → Closed + invalidate affordance cache (DR-003).
10
+ * - probe failure → Open.
11
+ *
12
+ * Persistence:
13
+ * - State stored in SQLite `circuit_breaker_state` table (v7-003).
14
+ * - Loads previous state on first access.
15
+ *
16
+ * Dependencies:
17
+ * - `StateDatabase` from `../../../../storage/db/index.js`
18
+ * - `WetProbeRunner` from `../../../../connectors/base/wet-probe-runner.js`
19
+ * - `CapabilityContractRegistryV7` from `../../../../connectors/base/manifest-v7.js`
20
+ * - `ProbeSignalAdapter` from `../probe-signal-adapter.js`
21
+ *
22
+ * Boundary:
23
+ * - Manager decides WHEN to probe (DR-002); connector-system executes it.
24
+ * - Does NOT execute HTTP directly — delegates to ProbeSignalAdapter.
25
+ *
26
+ * Test coverage: tests/unit/body/circuit-breaker-manager.test.ts
27
+ */
28
+ export function createCircuitBreakerManager(options) {
29
+ const { database: { sqlite }, probeAdapter, failureThreshold = 3, cooldownMs = 30_000, onClosed, } = options;
30
+ function loadRecord(platformId, capabilityId) {
31
+ const result = sqlite.exec(`SELECT * FROM circuit_breaker_state
32
+ WHERE platform_id = ? AND capability_id = ?`, [platformId, capabilityId]);
33
+ if (result.length === 0 || result[0].values.length === 0) {
34
+ return {
35
+ platformId,
36
+ capabilityId,
37
+ state: "closed",
38
+ failureCount: 0,
39
+ consecutiveFailures: 0,
40
+ };
41
+ }
42
+ const cols = result[0].columns;
43
+ const get = (name) => result[0].values[0][cols.indexOf(name)];
44
+ return {
45
+ platformId,
46
+ capabilityId,
47
+ state: get("state"),
48
+ failureCount: get("failure_count") ?? 0,
49
+ consecutiveFailures: get("consecutive_failures") ?? 0,
50
+ lastFailureAt: get("last_failure_at") ?? undefined,
51
+ openedAt: get("opened_at") ?? undefined,
52
+ lastProbeAt: get("last_probe_at") ?? undefined,
53
+ };
54
+ }
55
+ function saveRecord(rec) {
56
+ const now = new Date().toISOString();
57
+ sqlite.run(`INSERT INTO circuit_breaker_state
58
+ (platform_id, capability_id, state, failure_count, consecutive_failures,
59
+ last_failure_at, opened_at, last_probe_at, updated_at)
60
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
61
+ ON CONFLICT(platform_id, capability_id) DO UPDATE SET
62
+ state = excluded.state,
63
+ failure_count = excluded.failure_count,
64
+ consecutive_failures = excluded.consecutive_failures,
65
+ last_failure_at = excluded.last_failure_at,
66
+ opened_at = excluded.opened_at,
67
+ last_probe_at = excluded.last_probe_at,
68
+ updated_at = excluded.updated_at`, [
69
+ rec.platformId,
70
+ rec.capabilityId,
71
+ rec.state,
72
+ rec.failureCount,
73
+ rec.consecutiveFailures,
74
+ rec.lastFailureAt ?? null,
75
+ rec.openedAt ?? null,
76
+ rec.lastProbeAt ?? null,
77
+ now,
78
+ ]);
79
+ }
80
+ return {
81
+ async evaluateFailure(platformId, capabilityId) {
82
+ const rec = loadRecord(platformId, capabilityId);
83
+ rec.consecutiveFailures += 1;
84
+ rec.failureCount += 1;
85
+ rec.lastFailureAt = new Date().toISOString();
86
+ if (rec.state === "closed" && rec.consecutiveFailures >= failureThreshold) {
87
+ rec.state = "open";
88
+ rec.openedAt = rec.lastFailureAt;
89
+ }
90
+ else if (rec.state === "half_open") {
91
+ // HalfOpen + failure → back to Open
92
+ rec.state = "open";
93
+ rec.openedAt = rec.lastFailureAt;
94
+ }
95
+ // open + failure stays open
96
+ saveRecord(rec);
97
+ return rec.state;
98
+ },
99
+ async evaluateSuccess(platformId, capabilityId) {
100
+ const rec = loadRecord(platformId, capabilityId);
101
+ if (rec.state === "half_open") {
102
+ rec.state = "closed";
103
+ rec.consecutiveFailures = 0;
104
+ rec.openedAt = undefined;
105
+ if (onClosed) {
106
+ onClosed(platformId, capabilityId);
107
+ }
108
+ }
109
+ else if (rec.state === "closed") {
110
+ rec.consecutiveFailures = 0;
111
+ }
112
+ // open + success stays open (should not happen via normal path)
113
+ saveRecord(rec);
114
+ return rec.state;
115
+ },
116
+ async canExecute(platformId, capabilityId) {
117
+ const rec = loadRecord(platformId, capabilityId);
118
+ if (rec.state === "closed")
119
+ return true;
120
+ if (rec.state === "half_open")
121
+ return true; // allow limited probe traffic
122
+ if (rec.state === "open") {
123
+ if (rec.openedAt) {
124
+ const elapsed = Date.now() - new Date(rec.openedAt).getTime();
125
+ if (elapsed >= cooldownMs) {
126
+ return true; // let caller attempt, will transition to HalfOpen
127
+ }
128
+ }
129
+ return false;
130
+ }
131
+ return true;
132
+ },
133
+ async getState(platformId, capabilityId) {
134
+ return loadRecord(platformId, capabilityId).state;
135
+ },
136
+ async attemptReset(platformId, capabilityId) {
137
+ const rec = loadRecord(platformId, capabilityId);
138
+ if (rec.state !== "half_open" && rec.state !== "open") {
139
+ return rec.state;
140
+ }
141
+ // If cooldown has elapsed from Open, transition to HalfOpen and probe
142
+ if (rec.state === "open" && rec.openedAt) {
143
+ const elapsed = Date.now() - new Date(rec.openedAt).getTime();
144
+ if (elapsed < cooldownMs) {
145
+ return rec.state; // still cooling
146
+ }
147
+ rec.state = "half_open";
148
+ saveRecord(rec);
149
+ }
150
+ // HalfOpen: run wet probe
151
+ const probeResult = await probeAdapter.runAndRecordProbe(platformId, capabilityId, options.registry);
152
+ rec.lastProbeAt = new Date().toISOString();
153
+ if (probeResult.httpStatus === 0 && probeResult.actualStatus === "unavailable") {
154
+ // probe_policy_denied or network failure → stay HalfOpen
155
+ saveRecord(rec);
156
+ return rec.state;
157
+ }
158
+ if (probeResult.actualStatus === "available") {
159
+ rec.state = "closed";
160
+ rec.consecutiveFailures = 0;
161
+ rec.openedAt = undefined;
162
+ if (onClosed) {
163
+ onClosed(platformId, capabilityId);
164
+ }
165
+ }
166
+ else {
167
+ rec.state = "open";
168
+ rec.openedAt = new Date().toISOString();
169
+ }
170
+ saveRecord(rec);
171
+ return rec.state;
172
+ },
173
+ };
174
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * ProbeSignalAdapter — T-BTS.C.4
3
+ *
4
+ * Core logic: Bridge between WetProbeRunner and state-memory.
5
+ * 1. Runs a wet probe for a capability.
6
+ * 2. Persists the CapabilityProbeResult to state-memory.
7
+ * 3. If the probe indicates degradation/unavailability, records a
8
+ * corresponding ToolExperience row with triggerSource="probe".
9
+ *
10
+ * Dependencies:
11
+ * - `WetProbeRunner` from `../../../connectors/base/wet-probe-runner.js`
12
+ * - `CapabilityContractRegistryV7` from `../../../connectors/base/manifest-v7.js`
13
+ * - `CapabilityProbeResultStore` from `../../../storage/services/tool-experience-store.js`
14
+ * - `ToolExperienceStore` from `../../../storage/services/tool-experience-store.js`
15
+ * - `ExperienceWriter` from `./tool-experience/experience-writer.js`
16
+ *
17
+ * Boundary:
18
+ * - Does NOT modify breaker state — caller (CircuitBreakerManager) decides.
19
+ * - probePolicyDenied is treated as a valid result, not an error.
20
+ *
21
+ * Test coverage: tests/unit/body/probe-signal-adapter.test.ts
22
+ */
23
+ import type { WetProbeRunner } from "../../../connectors/base/wet-probe-runner.js";
24
+ import type { CapabilityContractRegistryV7 } from "../../../connectors/base/manifest-v7.js";
25
+ import type { CapabilityProbeResultStore, ToolExperienceStore } from "../../../storage/services/tool-experience-store.js";
26
+ export interface ProbeSignalAdapter {
27
+ runAndRecordProbe(platformId: string, capabilityId: string, registry: CapabilityContractRegistryV7): Promise<{
28
+ actualStatus: string;
29
+ httpStatus: number;
30
+ recorded: boolean;
31
+ experienceRecorded: boolean;
32
+ }>;
33
+ }
34
+ export declare function createProbeSignalAdapter(deps: {
35
+ wetProbeRunner: WetProbeRunner;
36
+ probeResultStore: CapabilityProbeResultStore;
37
+ toolExperienceStore: ToolExperienceStore;
38
+ }): ProbeSignalAdapter;