@haaaiawd/second-nature 0.1.43 → 0.1.51
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 +29 -29
- package/package.json +55 -55
- package/runtime/cli/commands/index.js +325 -325
- package/runtime/cli/ops/heartbeat-surface.d.ts +84 -75
- package/runtime/cli/ops/heartbeat-surface.js +100 -97
- package/runtime/cli/ops/ops-router.js +1482 -1454
- package/runtime/cli/ops/workspace-heartbeat-runner.d.ts +85 -76
- package/runtime/cli/ops/workspace-heartbeat-runner.js +242 -236
- package/runtime/connectors/base/contract.d.ts +111 -111
- package/runtime/connectors/base/failure-taxonomy.d.ts +13 -13
- package/runtime/connectors/base/failure-taxonomy.js +186 -186
- package/runtime/connectors/base/map-life-evidence.js +137 -137
- package/runtime/connectors/base/policy-layer.js +202 -202
- package/runtime/connectors/manifest/manifest-schema.d.ts +152 -152
- package/runtime/connectors/manifest/manifest-schema.js +54 -54
- package/runtime/connectors/services/connector-executor-adapter.d.ts +20 -20
- package/runtime/connectors/services/connector-executor-adapter.js +645 -645
- package/runtime/core/second-nature/heartbeat/goal-lifecycle-policy.d.ts +24 -37
- package/runtime/core/second-nature/heartbeat/goal-lifecycle-policy.js +61 -61
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +97 -88
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +397 -329
- package/runtime/core/second-nature/orchestrator/platform-capability-router.js +149 -131
|
@@ -1,329 +1,397 @@
|
|
|
1
|
-
import { buildContinuitySnapshot, } from "./snapshot-builder.js";
|
|
2
|
-
import { buildHeartbeatRuntimeSnapshot, } from "./runtime-snapshot.js";
|
|
3
|
-
import { planCandidateIntents } from "../orchestrator/intent-planner.js";
|
|
4
|
-
import { applyGoalPriority } from "../orchestrator/goal-priority.js";
|
|
5
|
-
import { evaluateHardGuards } from "../orchestrator/guard-layer.js";
|
|
6
|
-
import { dispatchUserOutreachIntent, } from "../outreach/dispatch-user-outreach.js";
|
|
7
|
-
import { buildJudgeOutreachInputFromSnapshot } from "../outreach/judge-input-from-snapshot.js";
|
|
8
|
-
import { runSourceBackedQuiet } from "../quiet/run-source-backed-quiet.js";
|
|
9
|
-
import { toCapabilityIntent } from "../orchestrator/effect-dispatcher.js";
|
|
10
|
-
import { updateNarrativeAfterEffect } from "../orchestrator/narrative-update.js";
|
|
11
|
-
import { mapLifeEvidence } from "../../../connectors/base/map-life-evidence.js";
|
|
12
|
-
import { appendLifeEvidence } from "../../../storage/life-evidence/append-life-evidence.js";
|
|
13
|
-
/**
|
|
14
|
-
* Resolves the heartbeat outcome for a guard-allowed intent (outreach dispatch, quiet orchestration, or default).
|
|
15
|
-
* Exported for unit tests (CR-M1 wiring).
|
|
16
|
-
*/
|
|
17
|
-
export async function resolveAllowedIntentResult(intent, runtime, inputs, signal, deps) {
|
|
18
|
-
const day = typeof signal.payload.timestamp === "string"
|
|
19
|
-
? signal.payload.timestamp.slice(0, 10)
|
|
20
|
-
: "1970-01-01";
|
|
21
|
-
if (intent.effectClass === "user_outreach" && deps.outreachDispatch) {
|
|
22
|
-
return dispatchUserOutreachIntent({
|
|
23
|
-
candidate: intent,
|
|
24
|
-
snapshot: runtime,
|
|
25
|
-
judgeInput: buildJudgeOutreachInputFromSnapshot(intent, runtime, inputs),
|
|
26
|
-
guidance: deps.outreachDispatch.guidance,
|
|
27
|
-
delivery: deps.outreachDispatch.delivery,
|
|
28
|
-
state: deps.outreachDispatch.state,
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
if (deps.quietWorkflow &&
|
|
32
|
-
(intent.kind === "quiet" ||
|
|
33
|
-
(intent.kind === "reflection" &&
|
|
34
|
-
intent.effectClass === "narrative_reflection"))) {
|
|
35
|
-
const quietRun = await runSourceBackedQuiet({
|
|
36
|
-
candidate: intent,
|
|
37
|
-
runtime,
|
|
38
|
-
day,
|
|
39
|
-
userInterestSnapshot: inputs.userInterestSnapshot,
|
|
40
|
-
workspaceRoot: deps.quietWorkflow.workspaceRoot,
|
|
41
|
-
// v7 T-V7C.C.3: pass Dream schedule port so Quiet completion triggers Dream.
|
|
42
|
-
dreamSchedulePort: deps.quietWorkflow.dreamSchedulePort,
|
|
43
|
-
});
|
|
44
|
-
return quietRun.result;
|
|
45
|
-
}
|
|
46
|
-
// T2.2.3 (CH-14-02/03 / CH-15-01): all intent_selected results must carry at least one
|
|
47
|
-
// machine-readable reason so operators can distinguish between effect classes:
|
|
48
|
-
// - maintenance / no_effect → "internal_tick" (no external side-effects)
|
|
49
|
-
// - connector_action without dispatch wired → "connector_dispatch_unwired"
|
|
50
|
-
// - external_platform_action / memory_curation → not generated by intent-planner today;
|
|
51
|
-
// if a future path produces them, they will reach the fallback [] branch below and
|
|
52
|
-
// should have dedicated reason codes added (e.g. "external_platform_action_unwired").
|
|
53
|
-
// - other (outreach / quiet) → caught by the early-return branches above
|
|
54
|
-
const noExternalEffect = intent.effectClass === "maintenance" ||
|
|
55
|
-
intent.effectClass === "no_effect" ||
|
|
56
|
-
intent.kind === "maintenance";
|
|
57
|
-
const connectorUnwired = intent.effectClass === "connector_action";
|
|
58
|
-
if (connectorUnwired && deps.connectorExecutor) {
|
|
59
|
-
if (!intent.platformId || intent.platformId === "unknown") {
|
|
60
|
-
return {
|
|
61
|
-
scope: "rhythm",
|
|
62
|
-
status: "intent_selected",
|
|
63
|
-
selectedIntentId: intent.id,
|
|
64
|
-
decisionId: `decision:${intent.id}:${Date.now()}`,
|
|
65
|
-
reasons: ["connector_dispatch_unavailable"],
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
const decisionId = `decision:${intent.id}:${Date.now()}`;
|
|
69
|
-
// T-V7C.C.4: inject identity from EmbodiedContext into connector request (readable, no credential)
|
|
70
|
-
const platformHandle = runtime.identity?.platformHandles.find((h) => h.platformId === intent.platformId)?.handle;
|
|
71
|
-
const result = await deps.connectorExecutor.executeEffect({
|
|
72
|
-
platformId: intent.platformId,
|
|
73
|
-
intent: toCapabilityIntent(intent),
|
|
74
|
-
payload: {},
|
|
75
|
-
decisionId,
|
|
76
|
-
intentId: intent.id,
|
|
77
|
-
idempotencyKey: `idem:${intent.id}:${Date.now()}`,
|
|
78
|
-
identity: platformHandle || runtime.identity?.canonicalName
|
|
79
|
-
? {
|
|
80
|
-
platformHandle,
|
|
81
|
-
canonicalName: runtime.identity?.canonicalName,
|
|
82
|
-
}
|
|
83
|
-
: undefined,
|
|
84
|
-
});
|
|
85
|
-
// T3.3.1: on success, map connector result to life evidence and append.
|
|
86
|
-
// On failure or empty result, no evidence is fabricated — attempt audit
|
|
87
|
-
// is already recorded by the connector policy layer telemetry.
|
|
88
|
-
if (result.status === "success" &&
|
|
89
|
-
deps.state &&
|
|
90
|
-
deps.workspaceRoot) {
|
|
91
|
-
try {
|
|
92
|
-
const candidate = mapLifeEvidence({
|
|
93
|
-
platformId: intent.platformId,
|
|
94
|
-
intent: toCapabilityIntent(intent),
|
|
95
|
-
result,
|
|
96
|
-
observedAt: new Date().toISOString(),
|
|
97
|
-
});
|
|
98
|
-
if (candidate) {
|
|
99
|
-
await appendLifeEvidence(deps.state, deps.workspaceRoot, candidate);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
catch (err) {
|
|
103
|
-
// Evidence append must not break the heartbeat cycle.
|
|
104
|
-
// Missing evidence will be reflected in the next snapshot load.
|
|
105
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
106
|
-
console.warn(`[heartbeat] evidence append failed for ${intent.platformId ?? "unknown"}: ${errorMessage}`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
// v7 T-V7C.C.2: record ToolExperience for all connector attempts in heartbeat.
|
|
110
|
-
if (deps.experienceWriter) {
|
|
111
|
-
try {
|
|
112
|
-
await deps.experienceWriter.recordExperience({
|
|
113
|
-
connectorId: intent.platformId,
|
|
114
|
-
capabilityId: toCapabilityIntent(intent),
|
|
115
|
-
result,
|
|
116
|
-
triggerSource: "heartbeat",
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
catch (err) {
|
|
120
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
121
|
-
console.warn(`[heartbeat] ToolExperience record failed for ${intent.platformId ?? "unknown"}: ${errorMessage}`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
:
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
1
|
+
import { buildContinuitySnapshot, } from "./snapshot-builder.js";
|
|
2
|
+
import { buildHeartbeatRuntimeSnapshot, } from "./runtime-snapshot.js";
|
|
3
|
+
import { planCandidateIntents } from "../orchestrator/intent-planner.js";
|
|
4
|
+
import { applyGoalPriority } from "../orchestrator/goal-priority.js";
|
|
5
|
+
import { evaluateHardGuards } from "../orchestrator/guard-layer.js";
|
|
6
|
+
import { dispatchUserOutreachIntent, } from "../outreach/dispatch-user-outreach.js";
|
|
7
|
+
import { buildJudgeOutreachInputFromSnapshot } from "../outreach/judge-input-from-snapshot.js";
|
|
8
|
+
import { runSourceBackedQuiet } from "../quiet/run-source-backed-quiet.js";
|
|
9
|
+
import { toCapabilityIntent } from "../orchestrator/effect-dispatcher.js";
|
|
10
|
+
import { updateNarrativeAfterEffect } from "../orchestrator/narrative-update.js";
|
|
11
|
+
import { mapLifeEvidence } from "../../../connectors/base/map-life-evidence.js";
|
|
12
|
+
import { appendLifeEvidence } from "../../../storage/life-evidence/append-life-evidence.js";
|
|
13
|
+
/**
|
|
14
|
+
* Resolves the heartbeat outcome for a guard-allowed intent (outreach dispatch, quiet orchestration, or default).
|
|
15
|
+
* Exported for unit tests (CR-M1 wiring).
|
|
16
|
+
*/
|
|
17
|
+
export async function resolveAllowedIntentResult(intent, runtime, inputs, signal, deps) {
|
|
18
|
+
const day = typeof signal.payload.timestamp === "string"
|
|
19
|
+
? signal.payload.timestamp.slice(0, 10)
|
|
20
|
+
: "1970-01-01";
|
|
21
|
+
if (intent.effectClass === "user_outreach" && deps.outreachDispatch) {
|
|
22
|
+
return dispatchUserOutreachIntent({
|
|
23
|
+
candidate: intent,
|
|
24
|
+
snapshot: runtime,
|
|
25
|
+
judgeInput: buildJudgeOutreachInputFromSnapshot(intent, runtime, inputs),
|
|
26
|
+
guidance: deps.outreachDispatch.guidance,
|
|
27
|
+
delivery: deps.outreachDispatch.delivery,
|
|
28
|
+
state: deps.outreachDispatch.state,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (deps.quietWorkflow &&
|
|
32
|
+
(intent.kind === "quiet" ||
|
|
33
|
+
(intent.kind === "reflection" &&
|
|
34
|
+
intent.effectClass === "narrative_reflection"))) {
|
|
35
|
+
const quietRun = await runSourceBackedQuiet({
|
|
36
|
+
candidate: intent,
|
|
37
|
+
runtime,
|
|
38
|
+
day,
|
|
39
|
+
userInterestSnapshot: inputs.userInterestSnapshot,
|
|
40
|
+
workspaceRoot: deps.quietWorkflow.workspaceRoot,
|
|
41
|
+
// v7 T-V7C.C.3: pass Dream schedule port so Quiet completion triggers Dream.
|
|
42
|
+
dreamSchedulePort: deps.quietWorkflow.dreamSchedulePort,
|
|
43
|
+
});
|
|
44
|
+
return quietRun.result;
|
|
45
|
+
}
|
|
46
|
+
// T2.2.3 (CH-14-02/03 / CH-15-01): all intent_selected results must carry at least one
|
|
47
|
+
// machine-readable reason so operators can distinguish between effect classes:
|
|
48
|
+
// - maintenance / no_effect → "internal_tick" (no external side-effects)
|
|
49
|
+
// - connector_action without dispatch wired → "connector_dispatch_unwired"
|
|
50
|
+
// - external_platform_action / memory_curation → not generated by intent-planner today;
|
|
51
|
+
// if a future path produces them, they will reach the fallback [] branch below and
|
|
52
|
+
// should have dedicated reason codes added (e.g. "external_platform_action_unwired").
|
|
53
|
+
// - other (outreach / quiet) → caught by the early-return branches above
|
|
54
|
+
const noExternalEffect = intent.effectClass === "maintenance" ||
|
|
55
|
+
intent.effectClass === "no_effect" ||
|
|
56
|
+
intent.kind === "maintenance";
|
|
57
|
+
const connectorUnwired = intent.effectClass === "connector_action";
|
|
58
|
+
if (connectorUnwired && deps.connectorExecutor) {
|
|
59
|
+
if (!intent.platformId || intent.platformId === "unknown") {
|
|
60
|
+
return {
|
|
61
|
+
scope: "rhythm",
|
|
62
|
+
status: "intent_selected",
|
|
63
|
+
selectedIntentId: intent.id,
|
|
64
|
+
decisionId: `decision:${intent.id}:${Date.now()}`,
|
|
65
|
+
reasons: ["connector_dispatch_unavailable"],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const decisionId = `decision:${intent.id}:${Date.now()}`;
|
|
69
|
+
// T-V7C.C.4: inject identity from EmbodiedContext into connector request (readable, no credential)
|
|
70
|
+
const platformHandle = runtime.identity?.platformHandles.find((h) => h.platformId === intent.platformId)?.handle;
|
|
71
|
+
const result = await deps.connectorExecutor.executeEffect({
|
|
72
|
+
platformId: intent.platformId,
|
|
73
|
+
intent: toCapabilityIntent(intent),
|
|
74
|
+
payload: {},
|
|
75
|
+
decisionId,
|
|
76
|
+
intentId: intent.id,
|
|
77
|
+
idempotencyKey: `idem:${intent.id}:${Date.now()}`,
|
|
78
|
+
identity: platformHandle || runtime.identity?.canonicalName
|
|
79
|
+
? {
|
|
80
|
+
platformHandle,
|
|
81
|
+
canonicalName: runtime.identity?.canonicalName,
|
|
82
|
+
}
|
|
83
|
+
: undefined,
|
|
84
|
+
});
|
|
85
|
+
// T3.3.1: on success, map connector result to life evidence and append.
|
|
86
|
+
// On failure or empty result, no evidence is fabricated — attempt audit
|
|
87
|
+
// is already recorded by the connector policy layer telemetry.
|
|
88
|
+
if (result.status === "success" &&
|
|
89
|
+
deps.state &&
|
|
90
|
+
deps.workspaceRoot) {
|
|
91
|
+
try {
|
|
92
|
+
const candidate = mapLifeEvidence({
|
|
93
|
+
platformId: intent.platformId,
|
|
94
|
+
intent: toCapabilityIntent(intent),
|
|
95
|
+
result,
|
|
96
|
+
observedAt: new Date().toISOString(),
|
|
97
|
+
});
|
|
98
|
+
if (candidate) {
|
|
99
|
+
await appendLifeEvidence(deps.state, deps.workspaceRoot, candidate);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
// Evidence append must not break the heartbeat cycle.
|
|
104
|
+
// Missing evidence will be reflected in the next snapshot load.
|
|
105
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
106
|
+
console.warn(`[heartbeat] evidence append failed for ${intent.platformId ?? "unknown"}: ${errorMessage}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// v7 T-V7C.C.2: record ToolExperience for all connector attempts in heartbeat.
|
|
110
|
+
if (deps.experienceWriter) {
|
|
111
|
+
try {
|
|
112
|
+
await deps.experienceWriter.recordExperience({
|
|
113
|
+
connectorId: intent.platformId,
|
|
114
|
+
capabilityId: toCapabilityIntent(intent),
|
|
115
|
+
result,
|
|
116
|
+
triggerSource: "heartbeat",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
121
|
+
console.warn(`[heartbeat] ToolExperience record failed for ${intent.platformId ?? "unknown"}: ${errorMessage}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// v7 T-BTS.C.5: update circuit breaker state after connector execution.
|
|
125
|
+
if (deps.circuitBreakerManager && intent.platformId && intent.capabilityIntent) {
|
|
126
|
+
try {
|
|
127
|
+
if (result.status === "success") {
|
|
128
|
+
await deps.circuitBreakerManager.evaluateSuccess(intent.platformId, intent.capabilityIntent);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
await deps.circuitBreakerManager.evaluateFailure(intent.platformId, intent.capabilityIntent);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
136
|
+
console.warn(`[heartbeat] CircuitBreaker update failed for ${intent.platformId ?? "unknown"}: ${errorMessage}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const base = {
|
|
140
|
+
scope: "rhythm",
|
|
141
|
+
status: "intent_selected",
|
|
142
|
+
selectedIntentId: intent.id,
|
|
143
|
+
decisionId,
|
|
144
|
+
reasons: result.status === "success"
|
|
145
|
+
? ["connector_effect_executed"]
|
|
146
|
+
: result.status === "retryable_failure"
|
|
147
|
+
? ["connector_retryable_failure", result.failureClass ?? "unknown"]
|
|
148
|
+
: ["connector_terminal_failure", result.failureClass ?? "unknown"],
|
|
149
|
+
};
|
|
150
|
+
return base;
|
|
151
|
+
}
|
|
152
|
+
const reasons = noExternalEffect
|
|
153
|
+
? ["internal_tick"]
|
|
154
|
+
: connectorUnwired
|
|
155
|
+
? ["connector_dispatch_unwired"]
|
|
156
|
+
: [];
|
|
157
|
+
return {
|
|
158
|
+
scope: "rhythm",
|
|
159
|
+
status: "intent_selected",
|
|
160
|
+
selectedIntentId: intent.id,
|
|
161
|
+
reasons,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* T2.1.5: after the cycle result is known, write a narrative revision when
|
|
166
|
+
* a NarrativeStateStore is wired. Errors are swallowed so the cycle result
|
|
167
|
+
* is never blocked by a store failure. Store failures are optionally traced
|
|
168
|
+
* via recordDecisionTrace so operators can monitor store health.
|
|
169
|
+
*/
|
|
170
|
+
async function maybeUpdateNarrativeState(result, selectedIntent, runtime, store, recordTrace, signal, recordNarrativeTrace) {
|
|
171
|
+
if (!store)
|
|
172
|
+
return;
|
|
173
|
+
try {
|
|
174
|
+
const prior = await store.loadNarrativeState();
|
|
175
|
+
const update = updateNarrativeAfterEffect({
|
|
176
|
+
result,
|
|
177
|
+
selectedIntent,
|
|
178
|
+
lifeEvidence: runtime.lifeEvidence,
|
|
179
|
+
priorNarrative: prior,
|
|
180
|
+
});
|
|
181
|
+
await store.updateNarrativeState(update);
|
|
182
|
+
// T5.1.2: record NarrativeTrace on successful state update
|
|
183
|
+
if (recordNarrativeTrace) {
|
|
184
|
+
try {
|
|
185
|
+
await recordNarrativeTrace({
|
|
186
|
+
traceId: `narrative_trace:${crypto.randomUUID()}`,
|
|
187
|
+
narrativeId: update.narrativeId,
|
|
188
|
+
revision: update.revision,
|
|
189
|
+
updateSource: "heartbeat",
|
|
190
|
+
sourceRefs: update.sourceRefs.map((r) => ({
|
|
191
|
+
id: r.sourceId,
|
|
192
|
+
kind: r.kind,
|
|
193
|
+
uri: r.url,
|
|
194
|
+
})),
|
|
195
|
+
unsupportedClaims: update.unsupportedClaims,
|
|
196
|
+
groundingStatus: update.unsupportedClaims.length > 0
|
|
197
|
+
? "degraded"
|
|
198
|
+
: update.status === "insufficient_sources"
|
|
199
|
+
? "blocked"
|
|
200
|
+
: "pass",
|
|
201
|
+
goalInfluenceRefs: selectedIntent?.goalInfluenceRefs ?? [],
|
|
202
|
+
createdAt: update.updatedAt,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// trace emission must not block the cycle
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// degrade silently; narrative update is best-effort
|
|
212
|
+
if (recordTrace && signal) {
|
|
213
|
+
try {
|
|
214
|
+
await recordTrace({
|
|
215
|
+
scope: result.scope,
|
|
216
|
+
status: result.status,
|
|
217
|
+
reasons: ["narrative_update_failed"],
|
|
218
|
+
selectedIntentId: selectedIntent?.id,
|
|
219
|
+
rhythmWindowId: runtime.rhythmWindow.windowId,
|
|
220
|
+
allowedIntentKinds: [...runtime.rhythmWindow.allowedIntentKinds],
|
|
221
|
+
candidateCount: 0,
|
|
222
|
+
lifeEvidenceEmpty: runtime.lifeEvidence.evidenceRefs.length === 0 &&
|
|
223
|
+
runtime.lifeEvidence.platformEventCount === 0 &&
|
|
224
|
+
runtime.lifeEvidence.workEventCount === 0,
|
|
225
|
+
trigger: signal.trigger,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// trace emission must also not block the cycle
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Ingest a heartbeat rhythm signal and drive one full decision round.
|
|
236
|
+
*/
|
|
237
|
+
export async function ingestRhythmSignal(signal, deps) {
|
|
238
|
+
const inputs = await deps.loadSnapshotInputs();
|
|
239
|
+
const snapshot = buildContinuitySnapshot(inputs);
|
|
240
|
+
const timestamp = signal.payload.timestamp;
|
|
241
|
+
const runtime = buildHeartbeatRuntimeSnapshot(timestamp, inputs, snapshot);
|
|
242
|
+
// v7 T-CP.C.3: evaluate goal lifecycle transitions before candidate planning.
|
|
243
|
+
let goalTransitions = [];
|
|
244
|
+
if (deps.goalLifecyclePolicy && inputs.acceptedGoals) {
|
|
245
|
+
try {
|
|
246
|
+
const policyResult = deps.goalLifecyclePolicy.evaluate(inputs.acceptedGoals);
|
|
247
|
+
goalTransitions = policyResult.transitionRequests;
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
251
|
+
console.warn(`[heartbeat] Goal lifecycle evaluation failed: ${msg}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const rawCandidates = planCandidateIntents(runtime, {
|
|
255
|
+
acceptedGoals: inputs.acceptedGoals,
|
|
256
|
+
connectorRegistry: deps.connectorRegistry,
|
|
257
|
+
narrativeState: runtime.narrativeState,
|
|
258
|
+
relationshipMemory: runtime.relationshipMemory,
|
|
259
|
+
});
|
|
260
|
+
// v7 T-CP.C.3: when no active goals and no connector candidates, use idle curiosity.
|
|
261
|
+
let allCandidates = rawCandidates;
|
|
262
|
+
if (deps.idleCuriosityPolicy && runtime.affordanceMap) {
|
|
263
|
+
const hasActiveGoals = (inputs.acceptedGoals ?? []).some((g) => g.status === "accepted");
|
|
264
|
+
const hasConnectorCandidates = rawCandidates.some((c) => c.effectClass === "connector_action" || c.effectClass === "external_platform_action");
|
|
265
|
+
if (!hasActiveGoals && !hasConnectorCandidates) {
|
|
266
|
+
try {
|
|
267
|
+
const idleResult = deps.idleCuriosityPolicy.select(runtime.affordanceMap, []);
|
|
268
|
+
if (idleResult.candidate) {
|
|
269
|
+
const idleIntent = {
|
|
270
|
+
id: `intent-idle-${idleResult.candidate.platformId}-${idleResult.candidate.capabilityId}`,
|
|
271
|
+
kind: "exploration",
|
|
272
|
+
priority: 30,
|
|
273
|
+
source: "tick",
|
|
274
|
+
platformId: idleResult.candidate.platformId,
|
|
275
|
+
summary: `idle curiosity: ${idleResult.candidate.intent}`,
|
|
276
|
+
effectClass: "connector_action",
|
|
277
|
+
capabilityIntent: idleResult.candidate.capabilityId,
|
|
278
|
+
sourceRefs: [
|
|
279
|
+
{
|
|
280
|
+
id: "idle_curiosity",
|
|
281
|
+
kind: "workspace_artifact",
|
|
282
|
+
uri: `idle://${idleResult.candidate.platformId}`,
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
idempotencyKey: `idle:${idleResult.candidate.platformId}:${idleResult.candidate.capabilityId}`,
|
|
286
|
+
goalInfluenceRefs: [],
|
|
287
|
+
};
|
|
288
|
+
allCandidates = [...rawCandidates, idleIntent];
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
293
|
+
console.warn(`[heartbeat] Idle curiosity selection failed: ${msg}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const { candidates } = applyGoalPriority(allCandidates, inputs.acceptedGoals);
|
|
298
|
+
const emitTrace = async (result) => {
|
|
299
|
+
if (!deps.recordDecisionTrace)
|
|
300
|
+
return;
|
|
301
|
+
const traceReasons = [...result.reasons];
|
|
302
|
+
if (goalTransitions.length > 0) {
|
|
303
|
+
traceReasons.push(`goal_transitions:${goalTransitions.length}`);
|
|
304
|
+
}
|
|
305
|
+
await deps.recordDecisionTrace({
|
|
306
|
+
scope: result.scope,
|
|
307
|
+
status: result.status,
|
|
308
|
+
reasons: traceReasons,
|
|
309
|
+
selectedIntentId: result.selectedIntentId,
|
|
310
|
+
rhythmWindowId: runtime.rhythmWindow.windowId,
|
|
311
|
+
allowedIntentKinds: [...runtime.rhythmWindow.allowedIntentKinds],
|
|
312
|
+
candidateCount: candidates.length,
|
|
313
|
+
lifeEvidenceEmpty: runtime.lifeEvidence.evidenceRefs.length === 0 &&
|
|
314
|
+
runtime.lifeEvidence.platformEventCount === 0 &&
|
|
315
|
+
runtime.lifeEvidence.workEventCount === 0,
|
|
316
|
+
trigger: signal.trigger,
|
|
317
|
+
});
|
|
318
|
+
};
|
|
319
|
+
let hasCandidates = false;
|
|
320
|
+
let anyAllow = false;
|
|
321
|
+
let anyDefer = false;
|
|
322
|
+
let anyDeny = false;
|
|
323
|
+
const denyReasons = [];
|
|
324
|
+
for (const intent of candidates) {
|
|
325
|
+
hasCandidates = true;
|
|
326
|
+
const evaluation = evaluateHardGuards(intent, runtime);
|
|
327
|
+
if (evaluation.verdict === "allow") {
|
|
328
|
+
anyAllow = true;
|
|
329
|
+
const base = {
|
|
330
|
+
scope: "rhythm",
|
|
331
|
+
status: "intent_selected",
|
|
332
|
+
selectedIntentId: intent.id,
|
|
333
|
+
reasons: evaluation.reasons,
|
|
334
|
+
};
|
|
335
|
+
const resolved = await resolveAllowedIntentResult(intent, runtime, inputs, signal, deps);
|
|
336
|
+
const result = resolved.status === "intent_selected" &&
|
|
337
|
+
resolved.reasons.length === 0 &&
|
|
338
|
+
evaluation.reasons.length > 0
|
|
339
|
+
? { ...resolved, reasons: evaluation.reasons }
|
|
340
|
+
: resolved;
|
|
341
|
+
await emitTrace(result);
|
|
342
|
+
await maybeUpdateNarrativeState(result, intent, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal, deps.recordNarrativeTrace);
|
|
343
|
+
return result;
|
|
344
|
+
}
|
|
345
|
+
if (evaluation.verdict === "defer") {
|
|
346
|
+
anyDefer = true;
|
|
347
|
+
denyReasons.push(`${intent.id}:${evaluation.verdict}(${evaluation.reasons.join(",")})`);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
anyDeny = true;
|
|
351
|
+
denyReasons.push(`${intent.id}:${evaluation.verdict}(${evaluation.reasons.join(",")})`);
|
|
352
|
+
}
|
|
353
|
+
if (!hasCandidates) {
|
|
354
|
+
const result = {
|
|
355
|
+
scope: "rhythm",
|
|
356
|
+
status: "heartbeat_ok",
|
|
357
|
+
reasons: ["silent_no_candidates"],
|
|
358
|
+
};
|
|
359
|
+
await emitTrace(result);
|
|
360
|
+
await maybeUpdateNarrativeState(result, undefined, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal, deps.recordNarrativeTrace);
|
|
361
|
+
return result;
|
|
362
|
+
}
|
|
363
|
+
if (!anyAllow && anyDefer && !anyDeny) {
|
|
364
|
+
const result = {
|
|
365
|
+
scope: "rhythm",
|
|
366
|
+
status: "deferred",
|
|
367
|
+
reasons: denyReasons.length > 0 ? denyReasons : ["all_candidates_deferred"],
|
|
368
|
+
};
|
|
369
|
+
await emitTrace(result);
|
|
370
|
+
await maybeUpdateNarrativeState(result, undefined, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal, deps.recordNarrativeTrace);
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
if (!anyAllow && denyReasons.length > 0) {
|
|
374
|
+
const result = {
|
|
375
|
+
scope: "rhythm",
|
|
376
|
+
status: "denied",
|
|
377
|
+
reasons: denyReasons,
|
|
378
|
+
};
|
|
379
|
+
await emitTrace(result);
|
|
380
|
+
await maybeUpdateNarrativeState(result, undefined, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal, deps.recordNarrativeTrace);
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
const result = {
|
|
384
|
+
scope: "rhythm",
|
|
385
|
+
status: "heartbeat_ok",
|
|
386
|
+
reasons: ["no_allow_verdict"],
|
|
387
|
+
};
|
|
388
|
+
await emitTrace(result);
|
|
389
|
+
await maybeUpdateNarrativeState(result, undefined, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal, deps.recordNarrativeTrace);
|
|
390
|
+
return result;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Build a snapshot directly from inputs (for testing or when state-system is unavailable).
|
|
394
|
+
*/
|
|
395
|
+
export function buildSnapshotFromInputs(inputs) {
|
|
396
|
+
return buildContinuitySnapshot(inputs);
|
|
397
|
+
}
|