@haaaiawd/second-nature 0.1.8 → 0.1.9

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 (183) hide show
  1. package/index.js +73 -1
  2. package/openclaw.plugin.json +1 -1
  3. package/package.json +1 -1
  4. package/runtime/cli/commands/index.d.ts +2 -0
  5. package/runtime/cli/commands/index.js +61 -6
  6. package/runtime/cli/explain/explain-surface-subject.d.ts +8 -0
  7. package/runtime/cli/explain/explain-surface-subject.js +9 -0
  8. package/runtime/cli/explain/format-explanation.d.ts +2 -0
  9. package/runtime/cli/explain/format-explanation.js +2 -0
  10. package/runtime/cli/explain/resolve-subject.js +15 -0
  11. package/runtime/cli/host-capability/classify-delivery.d.ts +14 -0
  12. package/runtime/cli/host-capability/classify-delivery.js +20 -0
  13. package/runtime/cli/host-capability/probe-host-capability.d.ts +2 -0
  14. package/runtime/cli/host-capability/probe-host-capability.js +58 -0
  15. package/runtime/cli/host-capability/record-host-capability.d.ts +6 -0
  16. package/runtime/cli/host-capability/record-host-capability.js +14 -0
  17. package/runtime/cli/host-capability/types.d.ts +71 -0
  18. package/runtime/cli/host-capability/types.js +6 -0
  19. package/runtime/cli/host-smoke/run-host-smoke.d.ts +2 -0
  20. package/runtime/cli/host-smoke/run-host-smoke.js +40 -0
  21. package/runtime/cli/host-smoke/types.d.ts +35 -0
  22. package/runtime/cli/host-smoke/types.js +6 -0
  23. package/runtime/cli/index.js +18 -0
  24. package/runtime/cli/ops/heartbeat-surface.d.ts +35 -0
  25. package/runtime/cli/ops/heartbeat-surface.js +71 -0
  26. package/runtime/cli/ops/ops-router.d.ts +16 -0
  27. package/runtime/cli/ops/ops-router.js +83 -0
  28. package/runtime/cli/ops/show-operator-fallback.d.ts +13 -0
  29. package/runtime/cli/ops/show-operator-fallback.js +22 -0
  30. package/runtime/cli/ops/workspace-heartbeat-runner.d.ts +10 -0
  31. package/runtime/cli/ops/workspace-heartbeat-runner.js +26 -0
  32. package/runtime/cli/read-models/index.d.ts +11 -2
  33. package/runtime/cli/read-models/index.js +50 -0
  34. package/runtime/cli/read-models/operator-explain-map.d.ts +6 -0
  35. package/runtime/cli/read-models/operator-explain-map.js +10 -0
  36. package/runtime/cli/read-models/types.d.ts +5 -1
  37. package/runtime/cli/runtime/runtime-artifact-boundary.d.ts +28 -0
  38. package/runtime/cli/runtime/runtime-artifact-boundary.js +94 -0
  39. package/runtime/connectors/base/contract.d.ts +6 -0
  40. package/runtime/connectors/base/execution-policy.d.ts +47 -0
  41. package/runtime/connectors/base/execution-policy.js +82 -0
  42. package/runtime/connectors/base/index.d.ts +2 -0
  43. package/runtime/connectors/base/index.js +2 -0
  44. package/runtime/connectors/base/manifest.d.ts +55 -2
  45. package/runtime/connectors/base/manifest.js +50 -0
  46. package/runtime/connectors/base/map-life-evidence.d.ts +16 -0
  47. package/runtime/connectors/base/map-life-evidence.js +79 -0
  48. package/runtime/connectors/base/policy-layer.d.ts +2 -0
  49. package/runtime/connectors/base/policy-layer.js +16 -0
  50. package/runtime/connectors/base/route-planner.js +1 -0
  51. package/runtime/connectors/index.d.ts +1 -0
  52. package/runtime/connectors/index.js +1 -0
  53. package/runtime/connectors/near-real/near-real-connector-smoke.d.ts +19 -0
  54. package/runtime/connectors/near-real/near-real-connector-smoke.js +152 -0
  55. package/runtime/core/second-nature/heartbeat/heartbeat-executor.js +2 -0
  56. package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +37 -16
  57. package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +95 -29
  58. package/runtime/core/second-nature/heartbeat/index.d.ts +4 -1
  59. package/runtime/core/second-nature/heartbeat/index.js +4 -1
  60. package/runtime/core/second-nature/heartbeat/run-heartbeat-cycle.d.ts +21 -0
  61. package/runtime/core/second-nature/heartbeat/run-heartbeat-cycle.js +35 -0
  62. package/runtime/core/second-nature/heartbeat/runtime-snapshot.d.ts +28 -0
  63. package/runtime/core/second-nature/heartbeat/runtime-snapshot.js +35 -0
  64. package/runtime/core/second-nature/heartbeat/signal.d.ts +9 -2
  65. package/runtime/core/second-nature/heartbeat/snapshot-builder.d.ts +19 -1
  66. package/runtime/core/second-nature/index.d.ts +8 -0
  67. package/runtime/core/second-nature/index.js +8 -0
  68. package/runtime/core/second-nature/orchestrator/effect-dispatcher.d.ts +1 -1
  69. package/runtime/core/second-nature/orchestrator/effect-dispatcher.js +9 -4
  70. package/runtime/core/second-nature/orchestrator/guard-layer.d.ts +6 -0
  71. package/runtime/core/second-nature/orchestrator/guard-layer.js +76 -20
  72. package/runtime/core/second-nature/orchestrator/intent-planner.d.ts +10 -0
  73. package/runtime/core/second-nature/orchestrator/intent-planner.js +135 -28
  74. package/runtime/core/second-nature/orchestrator/lease-manager.d.ts +1 -1
  75. package/runtime/core/second-nature/orchestrator/lease-manager.js +1 -1
  76. package/runtime/core/second-nature/outreach/build-outreach-draft-request.d.ts +6 -0
  77. package/runtime/core/second-nature/outreach/build-outreach-draft-request.js +63 -0
  78. package/runtime/core/second-nature/outreach/delivery-target.d.ts +26 -0
  79. package/runtime/core/second-nature/outreach/delivery-target.js +70 -0
  80. package/runtime/core/second-nature/outreach/dispatch-user-outreach.d.ts +38 -0
  81. package/runtime/core/second-nature/outreach/dispatch-user-outreach.js +119 -0
  82. package/runtime/core/second-nature/outreach/judge-input-from-snapshot.d.ts +7 -0
  83. package/runtime/core/second-nature/outreach/judge-input-from-snapshot.js +45 -0
  84. package/runtime/core/second-nature/outreach/judge-outreach.d.ts +40 -0
  85. package/runtime/core/second-nature/outreach/judge-outreach.js +121 -0
  86. package/runtime/core/second-nature/quiet/run-source-backed-quiet.d.ts +21 -0
  87. package/runtime/core/second-nature/quiet/run-source-backed-quiet.js +123 -0
  88. package/runtime/core/second-nature/rhythm/planner-rhythm-window.d.ts +15 -0
  89. package/runtime/core/second-nature/rhythm/planner-rhythm-window.js +52 -0
  90. package/runtime/core/second-nature/rhythm/policy-bridge.d.ts +19 -0
  91. package/runtime/core/second-nature/rhythm/policy-bridge.js +34 -0
  92. package/runtime/core/second-nature/types.d.ts +16 -2
  93. package/runtime/guidance/draft-outreach-message.d.ts +7 -0
  94. package/runtime/guidance/draft-outreach-message.js +42 -0
  95. package/runtime/guidance/evidence-guidance.d.ts +40 -0
  96. package/runtime/guidance/evidence-guidance.js +52 -0
  97. package/runtime/guidance/index.d.ts +3 -0
  98. package/runtime/guidance/index.js +3 -0
  99. package/runtime/guidance/outreach-draft-schema.d.ts +228 -0
  100. package/runtime/guidance/outreach-draft-schema.js +80 -0
  101. package/runtime/observability/audit/append-only-audit-store.d.ts +14 -0
  102. package/runtime/observability/audit/append-only-audit-store.js +21 -0
  103. package/runtime/observability/audit/audit-envelope.d.ts +51 -0
  104. package/runtime/observability/audit/audit-envelope.js +130 -0
  105. package/runtime/observability/audit/verify-audit-hash-chain.d.ts +23 -0
  106. package/runtime/observability/audit/verify-audit-hash-chain.js +83 -0
  107. package/runtime/observability/db/index.js +11 -0
  108. package/runtime/observability/db/schema/host-capability-reports.d.ts +180 -0
  109. package/runtime/observability/db/schema/host-capability-reports.js +12 -0
  110. package/runtime/observability/db/schema/index.d.ts +1 -0
  111. package/runtime/observability/db/schema/index.js +1 -0
  112. package/runtime/observability/index.d.ts +7 -0
  113. package/runtime/observability/index.js +7 -0
  114. package/runtime/observability/query/explain-query.d.ts +48 -0
  115. package/runtime/observability/query/explain-query.js +114 -0
  116. package/runtime/observability/query/export-audit-bundle.d.ts +22 -0
  117. package/runtime/observability/query/export-audit-bundle.js +27 -0
  118. package/runtime/observability/services/decision-ledger.d.ts +1 -1
  119. package/runtime/observability/services/decision-ledger.js +4 -0
  120. package/runtime/observability/services/governance-audit.d.ts +14 -0
  121. package/runtime/observability/services/governance-audit.js +25 -1
  122. package/runtime/observability/services/governance-plane-recorder.d.ts +47 -0
  123. package/runtime/observability/services/governance-plane-recorder.js +55 -0
  124. package/runtime/observability/services/lived-experience-audit.d.ts +97 -0
  125. package/runtime/observability/services/lived-experience-audit.js +161 -0
  126. package/runtime/storage/bootstrap/native-sqlite-probe.d.ts +7 -0
  127. package/runtime/storage/bootstrap/native-sqlite-probe.js +28 -0
  128. package/runtime/storage/bootstrap/repair-gate.d.ts +17 -0
  129. package/runtime/storage/bootstrap/repair-gate.js +71 -0
  130. package/runtime/storage/bootstrap/storage-mode-smoke.d.ts +38 -0
  131. package/runtime/storage/bootstrap/storage-mode-smoke.js +85 -0
  132. package/runtime/storage/db/index.js +49 -0
  133. package/runtime/storage/db/schema/delivery-attempts.d.ts +199 -0
  134. package/runtime/storage/db/schema/delivery-attempts.js +13 -0
  135. package/runtime/storage/db/schema/index.d.ts +3 -0
  136. package/runtime/storage/db/schema/index.js +3 -0
  137. package/runtime/storage/db/schema/life-evidence-index.d.ts +161 -0
  138. package/runtime/storage/db/schema/life-evidence-index.js +11 -0
  139. package/runtime/storage/db/schema/operator-fallback-artifacts.d.ts +161 -0
  140. package/runtime/storage/db/schema/operator-fallback-artifacts.js +11 -0
  141. package/runtime/storage/db/schema/policies.d.ts +17 -0
  142. package/runtime/storage/db/schema/policies.js +1 -0
  143. package/runtime/storage/delivery/query-delivery-attempts.d.ts +3 -0
  144. package/runtime/storage/delivery/query-delivery-attempts.js +32 -0
  145. package/runtime/storage/delivery/types.d.ts +27 -0
  146. package/runtime/storage/delivery/types.js +1 -0
  147. package/runtime/storage/delivery/write-delivery-attempt.d.ts +6 -0
  148. package/runtime/storage/delivery/write-delivery-attempt.js +36 -0
  149. package/runtime/storage/fallback/load-operator-fallback.d.ts +14 -0
  150. package/runtime/storage/fallback/load-operator-fallback.js +47 -0
  151. package/runtime/storage/fallback/operator-fallback-types.d.ts +9 -0
  152. package/runtime/storage/fallback/operator-fallback-types.js +1 -0
  153. package/runtime/storage/fallback/operator-fallback-view.d.ts +11 -0
  154. package/runtime/storage/fallback/operator-fallback-view.js +1 -0
  155. package/runtime/storage/fallback/write-operator-fallback.d.ts +6 -0
  156. package/runtime/storage/fallback/write-operator-fallback.js +21 -0
  157. package/runtime/storage/index.d.ts +21 -0
  158. package/runtime/storage/index.js +14 -0
  159. package/runtime/storage/life-evidence/append-life-evidence.d.ts +7 -0
  160. package/runtime/storage/life-evidence/append-life-evidence.js +64 -0
  161. package/runtime/storage/life-evidence/types.d.ts +45 -0
  162. package/runtime/storage/life-evidence/types.js +6 -0
  163. package/runtime/storage/quiet/persist-quiet-artifact.d.ts +7 -0
  164. package/runtime/storage/quiet/persist-quiet-artifact.js +22 -0
  165. package/runtime/storage/quiet/quiet-artifact-types.d.ts +18 -0
  166. package/runtime/storage/quiet/quiet-artifact-types.js +1 -0
  167. package/runtime/storage/quiet/quiet-artifact-writer.d.ts +15 -0
  168. package/runtime/storage/quiet/quiet-artifact-writer.js +56 -0
  169. package/runtime/storage/rhythm/rhythm-policy-snapshot.d.ts +10 -0
  170. package/runtime/storage/rhythm/rhythm-policy-snapshot.js +34 -0
  171. package/runtime/storage/services/credential-vault.d.ts +5 -0
  172. package/runtime/storage/services/credential-vault.js +46 -9
  173. package/runtime/storage/snapshots/continuity-snapshot.d.ts +9 -0
  174. package/runtime/storage/snapshots/continuity-snapshot.js +41 -0
  175. package/runtime/storage/snapshots/life-evidence-snapshot.d.ts +6 -0
  176. package/runtime/storage/snapshots/life-evidence-snapshot.js +114 -0
  177. package/runtime/storage/snapshots/types.d.ts +58 -0
  178. package/runtime/storage/snapshots/types.js +1 -0
  179. package/runtime/storage/state-api.js +11 -4
  180. package/runtime/storage/user-interest/load-user-interest-snapshot.d.ts +2 -0
  181. package/runtime/storage/user-interest/load-user-interest-snapshot.js +150 -0
  182. package/runtime/storage/user-interest/types.d.ts +25 -0
  183. package/runtime/storage/user-interest/types.js +1 -0
@@ -1,16 +1,45 @@
1
- const MAX_CANDIDATES = 6;
2
- function planObligationIntents(snapshot) {
3
- return snapshot.pendingObligations.map((obligation, index) => ({
1
+ import { isLifeEvidenceSliceEmpty } from "../heartbeat/runtime-snapshot.js";
2
+ import { buildHeartbeatRuntimeSnapshot } from "../heartbeat/runtime-snapshot.js";
3
+ const MAX_CANDIDATE_INTENTS = 6;
4
+ const OBLIGATION_SOURCE = [
5
+ { id: "obligation-anchor", kind: "workspace_artifact", uri: "workspace://obligations/pending" },
6
+ ];
7
+ function evidenceRefsForConnector(runtime) {
8
+ if (!isLifeEvidenceSliceEmpty(runtime.lifeEvidence) && runtime.lifeEvidence.evidenceRefs.length > 0) {
9
+ return runtime.lifeEvidence.evidenceRefs.slice(0, 8);
10
+ }
11
+ if (!isLifeEvidenceSliceEmpty(runtime.lifeEvidence)) {
12
+ return [
13
+ {
14
+ id: "life-evidence-summary",
15
+ kind: "connector_result",
16
+ uri: `workspace://life-evidence/counts/${runtime.lifeEvidence.platformEventCount}/${runtime.lifeEvidence.workEventCount}`,
17
+ },
18
+ ];
19
+ }
20
+ return [];
21
+ }
22
+ function isAllowedKind(kind, runtime) {
23
+ return runtime.rhythmWindow.allowedIntentKinds.includes(kind);
24
+ }
25
+ function planWorkIntents(runtime) {
26
+ if (!isAllowedKind("work", runtime))
27
+ return [];
28
+ return runtime.continuity.pendingObligations.map((obligation, index) => ({
4
29
  id: `intent-obligation-${index}`,
5
30
  kind: "work",
6
31
  priority: 100 - index,
7
32
  source: "obligation",
8
33
  summary: `fulfill obligation: ${obligation}`,
9
- effectClass: "external_platform_action",
34
+ effectClass: "connector_action",
35
+ sourceRefs: [...OBLIGATION_SOURCE],
36
+ idempotencyKey: `obligation:${obligation}:${index}`,
10
37
  }));
11
38
  }
12
- function planPlatformIntents(snapshot) {
13
- const socialPriorityBase = snapshot.budgets && snapshot.budgets.socialUsed >= snapshot.budgets.socialLimit ? 10 : 60;
39
+ function planExplorationIntents(runtime) {
40
+ if (!isAllowedKind("exploration", runtime))
41
+ return [];
42
+ const refs = evidenceRefsForConnector(runtime);
14
43
  return [
15
44
  {
16
45
  id: "intent-exploration",
@@ -18,45 +47,80 @@ function planPlatformIntents(snapshot) {
18
47
  priority: 70,
19
48
  source: "tick",
20
49
  summary: "scan platform opportunities",
21
- effectClass: "external_platform_action",
50
+ effectClass: "connector_action",
51
+ sourceRefs: refs,
52
+ idempotencyKey: "exploration:scan platform opportunities",
22
53
  },
54
+ ];
55
+ }
56
+ function planSocialIntents(runtime) {
57
+ if (!isAllowedKind("social", runtime))
58
+ return [];
59
+ const refs = evidenceRefsForConnector(runtime);
60
+ return [
23
61
  {
24
62
  id: "intent-social",
25
63
  kind: "social",
26
- priority: socialPriorityBase,
64
+ priority: runtime.continuity.budgets && runtime.continuity.budgets.socialUsed >= runtime.continuity.budgets.socialLimit ? 10 : 60,
27
65
  source: "tick",
28
66
  summary: "engage social platforms",
29
- effectClass: "external_platform_action",
67
+ effectClass: "connector_action",
68
+ sourceRefs: refs,
69
+ idempotencyKey: "social:engage social platforms",
30
70
  },
31
71
  ];
32
72
  }
33
- function planQuietIntents(snapshot) {
34
- if (snapshot.mode !== "quiet") {
73
+ function planQuietReflectionIntents(runtime) {
74
+ if (!runtime.rhythmWindow.quietBias && runtime.continuity.mode !== "quiet") {
35
75
  return [];
36
76
  }
37
- return [
38
- {
77
+ const out = [];
78
+ if (isAllowedKind("quiet", runtime)) {
79
+ out.push({
80
+ id: "intent-quiet",
81
+ kind: "quiet",
82
+ priority: 55,
83
+ source: "quiet_plan",
84
+ summary: "quiet window bookkeeping",
85
+ effectClass: "no_effect",
86
+ sourceRefs: [],
87
+ idempotencyKey: "quiet:bookkeeping",
88
+ });
89
+ }
90
+ if (isAllowedKind("maintenance", runtime)) {
91
+ out.push({
39
92
  id: "intent-maintenance",
40
93
  kind: "maintenance",
41
- priority: 90,
94
+ priority: 50,
42
95
  source: "quiet_plan",
43
96
  summary: "run maintenance checks",
44
97
  effectClass: "maintenance",
45
- },
46
- {
98
+ sourceRefs: [],
99
+ idempotencyKey: "maintenance:checks",
100
+ });
101
+ }
102
+ if (isAllowedKind("reflection", runtime)) {
103
+ const refs = evidenceRefsForConnector(runtime);
104
+ out.push({
47
105
  id: "intent-reflection",
48
106
  kind: "reflection",
49
- priority: 80,
107
+ priority: 45,
50
108
  source: "quiet_plan",
51
109
  summary: "run narrative reflection",
52
110
  effectClass: "narrative_reflection",
53
- },
54
- ];
111
+ sourceRefs: refs,
112
+ idempotencyKey: "reflection:narrative",
113
+ });
114
+ }
115
+ return out;
55
116
  }
56
- function planOutreachIntents(snapshot) {
57
- if (snapshot.recentOutreachHashes.length > 3) {
117
+ function planOutreachIntents(runtime) {
118
+ if (!isAllowedKind("outreach", runtime))
119
+ return [];
120
+ if (runtime.continuity.recentOutreachHashes.length > 3) {
58
121
  return [];
59
122
  }
123
+ const refs = evidenceRefsForConnector(runtime);
60
124
  return [
61
125
  {
62
126
  id: "intent-outreach",
@@ -65,24 +129,67 @@ function planOutreachIntents(snapshot) {
65
129
  source: "tick",
66
130
  summary: "consider proactive user outreach",
67
131
  effectClass: "user_outreach",
132
+ sourceRefs: refs,
133
+ idempotencyKey: "outreach:consider proactive user outreach",
68
134
  },
69
135
  ];
70
136
  }
71
- export function planIntent(snapshot) {
137
+ /**
138
+ * Plan ordered candidates for one heartbeat turn using rhythm window + life evidence slice.
139
+ */
140
+ export function planCandidateIntents(runtime) {
141
+ if (runtime.continuity.mode === "paused_for_interrupt") {
142
+ const pausedMaintenance = [
143
+ {
144
+ id: "intent-maintenance",
145
+ kind: "maintenance",
146
+ priority: 40,
147
+ source: "tick",
148
+ summary: "run maintenance checks",
149
+ effectClass: "maintenance",
150
+ sourceRefs: [],
151
+ idempotencyKey: "maintenance:checks",
152
+ },
153
+ ];
154
+ return pausedMaintenance
155
+ .filter((intent) => runtime.rhythmWindow.allowedIntentKinds.includes(intent.kind))
156
+ .slice(0, MAX_CANDIDATE_INTENTS);
157
+ }
158
+ if (runtime.continuity.mode === "maintenance_only") {
159
+ return planWorkIntents(runtime).sort((a, b) => b.priority - a.priority).slice(0, MAX_CANDIDATE_INTENTS);
160
+ }
72
161
  const intents = [
73
- ...planObligationIntents(snapshot),
74
- ...planPlatformIntents(snapshot),
75
- ...planQuietIntents(snapshot),
76
- ...planOutreachIntents(snapshot),
162
+ ...planWorkIntents(runtime),
163
+ ...planExplorationIntents(runtime),
164
+ ...planSocialIntents(runtime),
165
+ ...planQuietReflectionIntents(runtime),
166
+ ...planOutreachIntents(runtime),
77
167
  ];
78
168
  return intents
169
+ .filter((intent) => runtime.rhythmWindow.allowedIntentKinds.includes(intent.kind))
79
170
  .sort((a, b) => b.priority - a.priority)
80
- .slice(0, MAX_CANDIDATES);
171
+ .slice(0, MAX_CANDIDATE_INTENTS);
172
+ }
173
+ /** @deprecated Continuity-only helper for tests; prefer `planCandidateIntents` + `buildHeartbeatRuntimeSnapshot`. */
174
+ export function planIntent(snapshot) {
175
+ const inputs = {
176
+ mode: snapshot.mode,
177
+ currentWindowId: snapshot.currentWindowId,
178
+ pendingObligations: snapshot.pendingObligations,
179
+ recentOutreachHashes: snapshot.recentOutreachHashes,
180
+ deniedIntents: snapshot.deniedIntents,
181
+ budgets: snapshot.budgets,
182
+ awaitingUserInput: snapshot.awaitingUserInput,
183
+ riskSuppressed: snapshot.riskSuppressed,
184
+ quietEnabledBridge: snapshot.mode === "quiet",
185
+ };
186
+ const runtime = buildHeartbeatRuntimeSnapshot("2026-03-25T12:00:00.000Z", inputs, snapshot);
187
+ return planCandidateIntents(runtime);
81
188
  }
82
189
  export function decideDecisionBasis(intent) {
83
190
  if (intent.source === "obligation")
84
191
  return "rule_only";
85
- if (intent.kind === "maintenance")
192
+ if (intent.effectClass === "maintenance" || intent.effectClass === "no_effect")
86
193
  return "rule_only";
87
194
  if (intent.kind === "outreach" || intent.kind === "reflection")
88
195
  return "model_assisted";
@@ -1,4 +1,4 @@
1
- export type EffectClass = "external_platform_action" | "memory_curation" | "narrative_reflection" | "user_outreach" | "maintenance";
1
+ export type EffectClass = "external_platform_action" | "connector_action" | "memory_curation" | "narrative_reflection" | "user_outreach" | "maintenance" | "no_effect";
2
2
  export interface LeaseHandle {
3
3
  id: string;
4
4
  granted: boolean;
@@ -50,7 +50,7 @@ export class LeaseManager {
50
50
  };
51
51
  }
52
52
  resolveScope(effectClass, scopeHint) {
53
- if (effectClass === "external_platform_action" || effectClass === "user_outreach") {
53
+ if (effectClass === "external_platform_action" || effectClass === "connector_action" || effectClass === "user_outreach") {
54
54
  return scopeHint && scopeHint.length > 0 ? `${GLOBAL_SCOPE}:${scopeHint}` : GLOBAL_SCOPE;
55
55
  }
56
56
  return null;
@@ -0,0 +1,6 @@
1
+ import type { OutreachDraftRequest } from "../../../guidance/outreach-draft-schema.js";
2
+ import type { CandidateIntent } from "../types.js";
3
+ import type { HeartbeatRuntimeSnapshot } from "../heartbeat/runtime-snapshot.js";
4
+ import type { OutreachJudgment } from "./judge-outreach.js";
5
+ import type { DeliveryTargetResolution } from "./delivery-target.js";
6
+ export declare function buildOutreachDraftRequest(candidate: CandidateIntent, judgment: OutreachJudgment, snapshot: HeartbeatRuntimeSnapshot, delivery: DeliveryTargetResolution): OutreachDraftRequest;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Maps control-plane judgment + delivery resolution into guidance OutreachDraftRequest (T6.2.1).
3
+ * Aligns with control-plane-system.detail §3.9 buildOutreachDraftRequest.
4
+ */
5
+ import * as crypto from "node:crypto";
6
+ function inferRhythmWindowKind(windowId) {
7
+ const id = windowId.toLowerCase();
8
+ if (id.includes("work"))
9
+ return "work";
10
+ if (id.includes("social"))
11
+ return "social";
12
+ if (id.includes("quiet"))
13
+ return "quiet";
14
+ if (id.includes("reflect"))
15
+ return "reflection";
16
+ if (id.includes("explore"))
17
+ return "exploration";
18
+ return undefined;
19
+ }
20
+ function toGuidanceRefs(refs) {
21
+ return refs.map((r) => ({
22
+ id: r.id,
23
+ kind: r.kind,
24
+ uri: r.uri,
25
+ excerptHash: r.excerptHash,
26
+ observedAt: r.observedAt,
27
+ }));
28
+ }
29
+ function mapDeliveryVerdict(verdict) {
30
+ switch (verdict) {
31
+ case "target_available":
32
+ return "target_available";
33
+ case "target_none":
34
+ return "target_none";
35
+ case "channel_missing":
36
+ return "channel_missing";
37
+ case "host_unsupported":
38
+ return "host_unsupported";
39
+ default:
40
+ return "host_unsupported";
41
+ }
42
+ }
43
+ export function buildOutreachDraftRequest(candidate, judgment, snapshot, delivery) {
44
+ const sceneType = delivery.verdict === "target_available" ? "outreach" : "fallback_candidate";
45
+ const riskLevel = delivery.verdict === "target_available" ? "medium" : "low";
46
+ return {
47
+ requestId: `outreach_draft_request:${crypto.randomUUID()}`,
48
+ sceneType,
49
+ runtimeScope: "rhythm",
50
+ rhythmWindowKind: inferRhythmWindowKind(snapshot.rhythmWindow.windowId),
51
+ riskLevel,
52
+ sourceRefs: toGuidanceRefs(judgment.sourceRefs),
53
+ decisionId: judgment.decisionId,
54
+ candidateId: candidate.id,
55
+ judgmentVerdict: judgment.verdict,
56
+ valueScore: judgment.valueScore,
57
+ interestRefs: toGuidanceRefs(judgment.interestRefs),
58
+ deliveryContext: {
59
+ deliveryVerdict: mapDeliveryVerdict(delivery.verdict),
60
+ wordingMode: delivery.verdict === "target_available" ? "sendable" : "not_sent_fallback_candidate",
61
+ },
62
+ };
63
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Resolves OpenClaw-visible delivery target from host capability snapshot (T2.3.1 / ADR-007).
3
+ *
4
+ * Core logic: explicit/last targets require channel materialization; `none` is a first-class verdict.
5
+ * Test coverage: tests/unit/core/outreach-judgment.test.ts
6
+ */
7
+ export type DeliveryHostTarget = "none" | "last" | "explicit";
8
+ export interface DeliveryCapabilitySnapshot {
9
+ /** Raw host value; may be empty string before normalization. */
10
+ target?: DeliveryHostTarget | string | null;
11
+ channel?: string | null;
12
+ recipient?: string | null;
13
+ lastKnownVisibleChannel?: string | null;
14
+ /** When true, host reports unsupported delivery surface (maps to host_unsupported verdict). */
15
+ hostUnsupported?: boolean;
16
+ }
17
+ export type DeliveryTargetVerdict = "target_none" | "channel_missing" | "target_available" | "host_unsupported";
18
+ export interface DeliveryTargetResolution {
19
+ verdict: DeliveryTargetVerdict;
20
+ target?: DeliveryHostTarget;
21
+ channel?: string;
22
+ recipient?: string;
23
+ reason: string;
24
+ }
25
+ export declare function resolveDeliveryTarget(snapshot: DeliveryCapabilitySnapshot): DeliveryTargetResolution;
26
+ export declare function isDeliveryUnavailableReason(reason: string): boolean;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Resolves OpenClaw-visible delivery target from host capability snapshot (T2.3.1 / ADR-007).
3
+ *
4
+ * Core logic: explicit/last targets require channel materialization; `none` is a first-class verdict.
5
+ * Test coverage: tests/unit/core/outreach-judgment.test.ts
6
+ */
7
+ export function resolveDeliveryTarget(snapshot) {
8
+ if (snapshot.hostUnsupported) {
9
+ return {
10
+ verdict: "host_unsupported",
11
+ target: snapshot.target && String(snapshot.target).trim() !== "" && snapshot.target !== "none"
12
+ ? snapshot.target
13
+ : "none",
14
+ reason: "host_delivery_surface_unsupported",
15
+ };
16
+ }
17
+ const raw = snapshot.target;
18
+ if (raw === undefined || raw === null || String(raw).trim() === "" || raw === "none") {
19
+ return {
20
+ verdict: "target_none",
21
+ target: "none",
22
+ reason: "heartbeat_run_without_user_visible_delivery",
23
+ };
24
+ }
25
+ if (raw === "explicit") {
26
+ const ch = (snapshot.channel ?? "").trim();
27
+ const rec = (snapshot.recipient ?? "").trim();
28
+ if (!ch || !rec) {
29
+ return {
30
+ verdict: "channel_missing",
31
+ target: "explicit",
32
+ reason: "explicit_delivery_requires_channel_and_recipient",
33
+ };
34
+ }
35
+ return {
36
+ verdict: "target_available",
37
+ target: "explicit",
38
+ channel: ch,
39
+ recipient: rec,
40
+ reason: "delivery_target_available",
41
+ };
42
+ }
43
+ if (raw === "last") {
44
+ const lastCh = (snapshot.lastKnownVisibleChannel ?? "").trim();
45
+ if (!lastCh) {
46
+ return {
47
+ verdict: "channel_missing",
48
+ target: "last",
49
+ reason: "last_target_has_no_known_visible_channel",
50
+ };
51
+ }
52
+ return {
53
+ verdict: "target_available",
54
+ target: "last",
55
+ channel: lastCh,
56
+ recipient: snapshot.recipient?.trim() || undefined,
57
+ reason: "delivery_target_available",
58
+ };
59
+ }
60
+ return {
61
+ verdict: "target_available",
62
+ target: raw,
63
+ channel: snapshot.channel?.trim() || snapshot.lastKnownVisibleChannel?.trim() || undefined,
64
+ recipient: snapshot.recipient?.trim() || undefined,
65
+ reason: "delivery_target_available",
66
+ };
67
+ }
68
+ export function isDeliveryUnavailableReason(reason) {
69
+ return reason === "target_none" || reason === "channel_missing" || reason === "host_unsupported";
70
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * User outreach dispatch path: judgment → draft → host delivery → attempt + operator fallback (T2.3.2).
3
+ * Mirrors control-plane-system.detail §3.9 dispatchAllowedIntent user_outreach branch.
4
+ */
5
+ import type { GuidanceDraftPort } from "../../../guidance/outreach-draft-schema.js";
6
+ import type { CandidateIntent } from "../types.js";
7
+ import type { HeartbeatRuntimeSnapshot } from "../heartbeat/runtime-snapshot.js";
8
+ import type { HeartbeatCycleResult } from "../heartbeat/signal.js";
9
+ import type { StateDatabase } from "../../../storage/db/index.js";
10
+ import type { SourceRef } from "../../../storage/life-evidence/types.js";
11
+ import { type JudgeOutreachInput } from "./judge-outreach.js";
12
+ import { type DeliveryTargetResolution } from "./delivery-target.js";
13
+ export interface OpenClawDeliverySendResult {
14
+ id: string;
15
+ status: "sent" | "failed" | "dropped_by_host_policy";
16
+ errorClass?: string;
17
+ messageId?: string;
18
+ /** Host-reported delivery proof when messageId is absent (T4.3.1). */
19
+ hostProofRef?: SourceRef;
20
+ }
21
+ export interface OpenClawDeliveryPort {
22
+ sendDeliveryRequest(input: {
23
+ decisionId: string;
24
+ target: NonNullable<DeliveryTargetResolution["target"]>;
25
+ channel: string;
26
+ recipient?: string;
27
+ message: string;
28
+ sourceRefs: CandidateIntent["sourceRefs"];
29
+ }): Promise<OpenClawDeliverySendResult>;
30
+ }
31
+ export declare function dispatchUserOutreachIntent(input: {
32
+ candidate: CandidateIntent;
33
+ snapshot: HeartbeatRuntimeSnapshot;
34
+ judgeInput: Omit<JudgeOutreachInput, "candidate">;
35
+ guidance: GuidanceDraftPort;
36
+ delivery: OpenClawDeliveryPort;
37
+ state: StateDatabase;
38
+ }): Promise<HeartbeatCycleResult>;
@@ -0,0 +1,119 @@
1
+ import { writeDeliveryAttempt } from "../../../storage/delivery/write-delivery-attempt.js";
2
+ import { writeOperatorFallback } from "../../../storage/fallback/write-operator-fallback.js";
3
+ import { judgeOutreach } from "./judge-outreach.js";
4
+ import { resolveDeliveryTarget } from "./delivery-target.js";
5
+ import { buildOutreachDraftRequest } from "./build-outreach-draft-request.js";
6
+ function toSourceRefs(refs) {
7
+ return refs.map((r) => ({ ...r }));
8
+ }
9
+ function hasDeliveryProof(attempt) {
10
+ return Boolean(attempt.messageId?.trim()) || Boolean(attempt.hostProofRef);
11
+ }
12
+ function operatorReasonForUnavailable(verdict) {
13
+ if (verdict === "target_none")
14
+ return "target_none";
15
+ if (verdict === "channel_missing")
16
+ return "channel_missing";
17
+ return "host_unsupported";
18
+ }
19
+ export async function dispatchUserOutreachIntent(input) {
20
+ const { candidate, snapshot, judgeInput, guidance, delivery, state } = input;
21
+ const judgment = judgeOutreach({ ...judgeInput, candidate });
22
+ if (judgment.verdict !== "allow") {
23
+ return {
24
+ scope: "rhythm",
25
+ status: judgment.verdict === "defer" ? "deferred" : "denied",
26
+ selectedIntentId: candidate.id,
27
+ reasons: judgment.reasons,
28
+ decisionId: judgment.decisionId,
29
+ };
30
+ }
31
+ const deliveryResolution = resolveDeliveryTarget(judgeInput.delivery);
32
+ if (deliveryResolution.verdict !== "target_available") {
33
+ const req = buildOutreachDraftRequest(candidate, judgment, snapshot, deliveryResolution);
34
+ const draft = await guidance.draftOutreachMessage(req);
35
+ const fb = await writeOperatorFallback(state, {
36
+ reason: operatorReasonForUnavailable(deliveryResolution.verdict),
37
+ decisionId: judgment.decisionId,
38
+ sourceRefs: toSourceRefs(judgment.sourceRefs),
39
+ candidateMessage: draft.status === "ready" ? draft.draft.text : undefined,
40
+ nextStep: "resolve_delivery_target_or_retry_after_host_update",
41
+ });
42
+ return {
43
+ scope: "rhythm",
44
+ status: "delivery_unavailable",
45
+ selectedIntentId: candidate.id,
46
+ reasons: [deliveryResolution.reason],
47
+ decisionId: judgment.decisionId,
48
+ fallbackRef: fb.fallbackRef,
49
+ };
50
+ }
51
+ const req = buildOutreachDraftRequest(candidate, judgment, snapshot, deliveryResolution);
52
+ const draft = await guidance.draftOutreachMessage(req);
53
+ if (draft.status !== "ready") {
54
+ return {
55
+ scope: "rhythm",
56
+ status: "denied",
57
+ selectedIntentId: candidate.id,
58
+ reasons: draft.reasons,
59
+ decisionId: judgment.decisionId,
60
+ };
61
+ }
62
+ const attempt = await delivery.sendDeliveryRequest({
63
+ decisionId: judgment.decisionId,
64
+ target: deliveryResolution.target,
65
+ channel: deliveryResolution.channel,
66
+ recipient: deliveryResolution.recipient,
67
+ message: draft.draft.text,
68
+ sourceRefs: judgment.sourceRefs,
69
+ });
70
+ if (attempt.status !== "sent" || !hasDeliveryProof(attempt)) {
71
+ const fb = await writeOperatorFallback(state, {
72
+ reason: "delivery_failed",
73
+ decisionId: judgment.decisionId,
74
+ sourceRefs: toSourceRefs(judgment.sourceRefs),
75
+ candidateMessage: draft.draft.text,
76
+ nextStep: "review_delivery_audit_and_host_capability",
77
+ });
78
+ const hostReportedSentWithoutProof = attempt.status === "sent" && !hasDeliveryProof(attempt);
79
+ await writeDeliveryAttempt(state, {
80
+ attemptId: attempt.id,
81
+ decisionId: judgment.decisionId,
82
+ target: deliveryResolution.target,
83
+ channel: deliveryResolution.channel,
84
+ status: attempt.status === "dropped_by_host_policy" ? "dropped_by_host_policy" : "failed",
85
+ errorClass: hostReportedSentWithoutProof
86
+ ? "delivery_proof_missing"
87
+ : attempt.errorClass ?? attempt.status,
88
+ fallbackRef: fb.fallbackRef,
89
+ });
90
+ return {
91
+ scope: "rhythm",
92
+ status: "delivery_unavailable",
93
+ selectedIntentId: candidate.id,
94
+ reasons: hostReportedSentWithoutProof
95
+ ? ["delivery_failed", "delivery_proof_missing"]
96
+ : ["delivery_failed", attempt.status],
97
+ decisionId: judgment.decisionId,
98
+ deliveryAttemptId: attempt.id,
99
+ fallbackRef: fb.fallbackRef,
100
+ };
101
+ }
102
+ await writeDeliveryAttempt(state, {
103
+ attemptId: attempt.id,
104
+ decisionId: judgment.decisionId,
105
+ target: deliveryResolution.target,
106
+ channel: deliveryResolution.channel,
107
+ status: "sent",
108
+ messageId: attempt.messageId?.trim(),
109
+ hostProofRef: attempt.hostProofRef,
110
+ });
111
+ return {
112
+ scope: "rhythm",
113
+ status: "intent_selected",
114
+ selectedIntentId: candidate.id,
115
+ reasons: ["outreach_sent"],
116
+ decisionId: judgment.decisionId,
117
+ deliveryAttemptId: attempt.id,
118
+ };
119
+ }
@@ -0,0 +1,7 @@
1
+ import type { SnapshotInputs } from "../heartbeat/snapshot-builder.js";
2
+ import type { HeartbeatRuntimeSnapshot } from "../heartbeat/runtime-snapshot.js";
3
+ import type { CandidateIntent } from "../types.js";
4
+ import type { JudgeOutreachInput, JudgeOutreachUserInterest } from "./judge-outreach.js";
5
+ import type { UserInterestSnapshot } from "../../../storage/user-interest/types.js";
6
+ export declare function userInterestSnapshotToJudge(snapshot?: UserInterestSnapshot): JudgeOutreachUserInterest;
7
+ export declare function buildJudgeOutreachInputFromSnapshot(intent: CandidateIntent, runtime: HeartbeatRuntimeSnapshot, inputs: SnapshotInputs): Omit<JudgeOutreachInput, "candidate">;
@@ -0,0 +1,45 @@
1
+ import { isLifeEvidenceSliceEmpty } from "../heartbeat/runtime-snapshot.js";
2
+ function toControlPlaneRefs(refs) {
3
+ return refs.map((r) => ({
4
+ id: r.id,
5
+ kind: r.kind,
6
+ uri: r.uri,
7
+ excerptHash: r.excerptHash,
8
+ observedAt: r.observedAt,
9
+ }));
10
+ }
11
+ export function userInterestSnapshotToJudge(snapshot) {
12
+ if (!snapshot) {
13
+ return { staleness: "insufficient", confidence: 0, signals: [], sourceRefs: [] };
14
+ }
15
+ return {
16
+ staleness: snapshot.staleness,
17
+ confidence: snapshot.confidence,
18
+ signals: snapshot.signals.map((s) => ({
19
+ topic: s.topic,
20
+ confidence: s.confidence,
21
+ sourceRefs: s.sourceRefs.map((r) => ({
22
+ id: r.id,
23
+ kind: r.kind,
24
+ uri: r.uri,
25
+ excerptHash: r.excerptHash,
26
+ observedAt: r.observedAt,
27
+ })),
28
+ })),
29
+ sourceRefs: toControlPlaneRefs(snapshot.sourceRefs),
30
+ };
31
+ }
32
+ export function buildJudgeOutreachInputFromSnapshot(intent, runtime, inputs) {
33
+ const delivery = inputs.deliveryCapability ?? { target: "none" };
34
+ const key = intent.idempotencyKey ?? intent.id;
35
+ return {
36
+ userInterest: userInterestSnapshotToJudge(inputs.userInterestSnapshot),
37
+ lifeEvidence: {
38
+ empty: isLifeEvidenceSliceEmpty(runtime.lifeEvidence),
39
+ evidenceRefCount: runtime.lifeEvidence.evidenceRefs.length,
40
+ },
41
+ delivery,
42
+ duplicateBlocked: runtime.hardGuards.hasDuplicateIntent(key),
43
+ cooldownBlocked: !runtime.hardGuards.isOutreachCooldownClear(key),
44
+ };
45
+ }
@@ -0,0 +1,40 @@
1
+ import type { CandidateIntent, ControlPlaneSourceRef } from "../types.js";
2
+ import { type DeliveryCapabilitySnapshot, type DeliveryTargetResolution } from "./delivery-target.js";
3
+ export type OutreachJudgmentVerdict = "allow" | "deny" | "defer";
4
+ export type CooldownState = "clear" | "cooling_down" | "duplicate";
5
+ export interface JudgeOutreachUserInterest {
6
+ staleness: "fresh" | "stale" | "insufficient";
7
+ confidence: number;
8
+ signals: Array<{
9
+ topic: string;
10
+ confidence: number;
11
+ sourceRefs: ControlPlaneSourceRef[];
12
+ }>;
13
+ sourceRefs: ControlPlaneSourceRef[];
14
+ }
15
+ export interface JudgeOutreachLifeEvidence {
16
+ empty: boolean;
17
+ evidenceRefCount: number;
18
+ }
19
+ export interface JudgeOutreachInput {
20
+ candidate: CandidateIntent;
21
+ userInterest: JudgeOutreachUserInterest;
22
+ lifeEvidence: JudgeOutreachLifeEvidence;
23
+ delivery: DeliveryCapabilitySnapshot;
24
+ duplicateBlocked?: boolean;
25
+ cooldownBlocked?: boolean;
26
+ }
27
+ export interface OutreachJudgment {
28
+ decisionId: string;
29
+ candidateId: string;
30
+ verdict: OutreachJudgmentVerdict;
31
+ valueScore: number;
32
+ userRelevance: number;
33
+ actionability: number;
34
+ interestRefs: ControlPlaneSourceRef[];
35
+ sourceRefs: ControlPlaneSourceRef[];
36
+ cooldownState: CooldownState;
37
+ deliveryVerdict: DeliveryTargetResolution["verdict"];
38
+ reasons: string[];
39
+ }
40
+ export declare function judgeOutreach(input: JudgeOutreachInput): OutreachJudgment;