@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,236 +1,242 @@
|
|
|
1
|
-
import { runHeartbeatCycle } from "../../core/second-nature/heartbeat/run-heartbeat-cycle.js";
|
|
2
|
-
import { loadLifeEvidenceSnapshot } from "../../storage/snapshots/life-evidence-snapshot.js";
|
|
3
|
-
import { createAgentGoalStore } from "../../storage/goal/agent-goal-store.js";
|
|
4
|
-
import { createNarrativeStateStore } from "../../storage/narrative/narrative-state-store.js";
|
|
5
|
-
import { createRelationshipMemoryStore } from "../../storage/relationship/relationship-memory-store.js";
|
|
6
|
-
import { createIdentityProfileStore } from "../../storage/services/identity-profile-store.js";
|
|
7
|
-
import { generateHeartbeatDigest, } from "../../observability/services/heartbeat-digest-assembler.js";
|
|
8
|
-
import { createHistoryDigestStore } from "../../storage/services/history-digest-store.js";
|
|
9
|
-
export async function loadSnapshotInputsForWorkspaceHeartbeat(readModels, options = {}) {
|
|
10
|
-
const status = await readModels.loadStatus();
|
|
11
|
-
const mode = status.rhythm.mode === "unknown" ? "active" : status.rhythm.mode;
|
|
12
|
-
// CH-15-03: quietEnabledBridge should reflect whether the quiet *execution path* is wired
|
|
13
|
-
// (workspaceRoot available), not whether the last observed rhythm decision was "quiet".
|
|
14
|
-
// status.quiet.mode is typically "unknown" until a Quiet artifact has been persisted, which
|
|
15
|
-
// means binding to it would permanently suppress the quiet window — the opposite of intent.
|
|
16
|
-
// We instead enable the bridge whenever workspaceRoot is provided (same condition as
|
|
17
|
-
// `createWorkspaceHeartbeatRunner` uses for injecting quietWorkflow).
|
|
18
|
-
const quietEnabledBridge = !!options.workspaceRoot;
|
|
19
|
-
// T2.2.2: Load life evidence from state DB when available so SnapshotInputs carries real refs.
|
|
20
|
-
let lifeEvidenceRefs;
|
|
21
|
-
let platformEventCount;
|
|
22
|
-
let workEventCount;
|
|
23
|
-
let lifeEvidenceEmptyReason;
|
|
24
|
-
if (options.state && options.workspaceRoot) {
|
|
25
|
-
try {
|
|
26
|
-
const snapshot = await loadLifeEvidenceSnapshot(options.state, options.workspaceRoot, { limit: 50 },
|
|
27
|
-
// Skip repair gate here — runner is called inside a live cycle; gate ran at startup.
|
|
28
|
-
{ runRepairGate: false });
|
|
29
|
-
lifeEvidenceRefs = snapshot.evidenceRefs.map((ref) => ({
|
|
30
|
-
id: ref.id,
|
|
31
|
-
kind: ref.kind,
|
|
32
|
-
uri: ref.uri,
|
|
33
|
-
}));
|
|
34
|
-
platformEventCount = snapshot.platformEvents.length;
|
|
35
|
-
workEventCount = snapshot.workEvents.length;
|
|
36
|
-
if (snapshot.empty) {
|
|
37
|
-
// L-01: Currently snapshot only exposes `empty` boolean.
|
|
38
|
-
// Future: if snapshot adds `emptyReason` (e.g. "redacted_only"), map it here.
|
|
39
|
-
lifeEvidenceEmptyReason = "no_sources";
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
// If evidence load fails, signal state_unavailable rather than crashing the cycle.
|
|
44
|
-
lifeEvidenceRefs = [];
|
|
45
|
-
platformEventCount = 0;
|
|
46
|
-
workEventCount = 0;
|
|
47
|
-
lifeEvidenceEmptyReason = "state_unavailable";
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
// No state wired — record that life evidence wasn't loaded so guards can reason honestly.
|
|
52
|
-
lifeEvidenceEmptyReason = "state_unavailable";
|
|
53
|
-
}
|
|
54
|
-
// T2.1.4: Load accepted goals from state DB when available.
|
|
55
|
-
// M-03: typed as GoalContext to avoid coupling to the full AgentGoal schema.
|
|
56
|
-
let acceptedGoals;
|
|
57
|
-
let acceptedGoalsLoadError;
|
|
58
|
-
if (options.state) {
|
|
59
|
-
try {
|
|
60
|
-
const goalStore = createAgentGoalStore(options.state);
|
|
61
|
-
acceptedGoals = await goalStore.listAgentGoals({
|
|
62
|
-
statuses: ["accepted"],
|
|
63
|
-
limit: 20,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
catch (err) {
|
|
67
|
-
acceptedGoals = [];
|
|
68
|
-
acceptedGoalsLoadError = err instanceof Error ? err.message : String(err);
|
|
69
|
-
// H-05: Distinguish "load failed" from "no goals" for observability.
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
// CR-02: Load narrative state and relationship memory when state is available.
|
|
73
|
-
let narrativeState;
|
|
74
|
-
let relationshipMemory;
|
|
75
|
-
let identity;
|
|
76
|
-
if (options.state) {
|
|
77
|
-
try {
|
|
78
|
-
const narrativeStore = createNarrativeStateStore(options.state);
|
|
79
|
-
narrativeState = (await narrativeStore.loadNarrativeState()) ?? undefined;
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
// Narrative state is optional; failure should not block the cycle.
|
|
83
|
-
}
|
|
84
|
-
try {
|
|
85
|
-
const relationshipStore = createRelationshipMemoryStore(options.state);
|
|
86
|
-
relationshipMemory = (await relationshipStore.loadRelationshipMemory()) ?? undefined;
|
|
87
|
-
}
|
|
88
|
-
catch {
|
|
89
|
-
// Relationship memory is optional; failure should not block the cycle.
|
|
90
|
-
}
|
|
91
|
-
try {
|
|
92
|
-
const identityStore = createIdentityProfileStore(options.state);
|
|
93
|
-
const identityResult = await identityStore.loadIdentityProfile("default");
|
|
94
|
-
if (identityResult.status === "loaded" && identityResult.profile) {
|
|
95
|
-
identity = identityResult.profile;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
// Identity is optional; failure should not block the cycle.
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return {
|
|
103
|
-
mode,
|
|
104
|
-
currentWindowId: status.rhythm.windowId ?? "workspace-default",
|
|
105
|
-
pendingObligations: [],
|
|
106
|
-
recentOutreachHashes: [],
|
|
107
|
-
deniedIntents: [],
|
|
108
|
-
budgets: { socialUsed: 0, socialLimit: 5 },
|
|
109
|
-
awaitingUserInput: false,
|
|
110
|
-
quietEnabledBridge,
|
|
111
|
-
deliveryCapability: { target: "none" },
|
|
112
|
-
lifeEvidenceRefs,
|
|
113
|
-
platformEventCount,
|
|
114
|
-
workEventCount,
|
|
115
|
-
lifeEvidenceEmptyReason,
|
|
116
|
-
acceptedGoals,
|
|
117
|
-
acceptedGoalsLoadError,
|
|
118
|
-
narrativeState,
|
|
119
|
-
relationshipMemory,
|
|
120
|
-
affordanceMap: options.affordanceMap,
|
|
121
|
-
identity,
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
export function createWorkspaceHeartbeatRunner(readModels, options = {}) {
|
|
125
|
-
// T1.2.4: inject quietWorkflow dep when workspaceRoot is set so quiet/reflection intents
|
|
126
|
-
// can trigger runSourceBackedQuiet and persist artifacts to disk.
|
|
127
|
-
const quietEnabled = options.workspaceRoot && options.enableQuietWorkflow !== false;
|
|
128
|
-
// T2.1.5: when state DB is wired, create a NarrativeStateStore for heartbeat updates.
|
|
129
|
-
const narrativeStateStore = options.state
|
|
130
|
-
? createNarrativeStateStore(options.state)
|
|
131
|
-
: undefined;
|
|
132
|
-
return async (signal) => {
|
|
133
|
-
const cycle = await runHeartbeatCycle({
|
|
134
|
-
signal,
|
|
135
|
-
runtimeAvailable: true,
|
|
136
|
-
deps: {
|
|
137
|
-
loadSnapshotInputs: () => loadSnapshotInputsForWorkspaceHeartbeat(readModels, {
|
|
138
|
-
state: options.state,
|
|
139
|
-
workspaceRoot: options.workspaceRoot,
|
|
140
|
-
affordanceMap: options.affordanceMap,
|
|
141
|
-
}),
|
|
142
|
-
// T1.2.4: pass quietWorkflow dep so runSourceBackedQuiet can persist artifacts.
|
|
143
|
-
quietWorkflow: quietEnabled
|
|
144
|
-
? {
|
|
145
|
-
workspaceRoot: options.workspaceRoot,
|
|
146
|
-
// v7 T-V7C.C.3: pass Dream schedule port so Quiet completion triggers Dream.
|
|
147
|
-
dreamSchedulePort: options.dreamSchedulePort,
|
|
148
|
-
}
|
|
149
|
-
: undefined,
|
|
150
|
-
connectorExecutor: options.connectorExecutor,
|
|
151
|
-
narrativeStateStore,
|
|
152
|
-
// T3.3.1: pass state + workspaceRoot so connector effects can write life evidence.
|
|
153
|
-
state: options.state,
|
|
154
|
-
workspaceRoot: options.workspaceRoot,
|
|
155
|
-
// T2.4.1: pass registry so planner resolves platform-specific intents.
|
|
156
|
-
connectorRegistry: options.connectorRegistry,
|
|
157
|
-
// v7 T-V7C.C.2: pass experience writer for heartbeat connector attempts.
|
|
158
|
-
experienceWriter: options.experienceWriter,
|
|
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
|
-
|
|
1
|
+
import { runHeartbeatCycle } from "../../core/second-nature/heartbeat/run-heartbeat-cycle.js";
|
|
2
|
+
import { loadLifeEvidenceSnapshot } from "../../storage/snapshots/life-evidence-snapshot.js";
|
|
3
|
+
import { createAgentGoalStore } from "../../storage/goal/agent-goal-store.js";
|
|
4
|
+
import { createNarrativeStateStore } from "../../storage/narrative/narrative-state-store.js";
|
|
5
|
+
import { createRelationshipMemoryStore } from "../../storage/relationship/relationship-memory-store.js";
|
|
6
|
+
import { createIdentityProfileStore } from "../../storage/services/identity-profile-store.js";
|
|
7
|
+
import { generateHeartbeatDigest, } from "../../observability/services/heartbeat-digest-assembler.js";
|
|
8
|
+
import { createHistoryDigestStore } from "../../storage/services/history-digest-store.js";
|
|
9
|
+
export async function loadSnapshotInputsForWorkspaceHeartbeat(readModels, options = {}) {
|
|
10
|
+
const status = await readModels.loadStatus();
|
|
11
|
+
const mode = status.rhythm.mode === "unknown" ? "active" : status.rhythm.mode;
|
|
12
|
+
// CH-15-03: quietEnabledBridge should reflect whether the quiet *execution path* is wired
|
|
13
|
+
// (workspaceRoot available), not whether the last observed rhythm decision was "quiet".
|
|
14
|
+
// status.quiet.mode is typically "unknown" until a Quiet artifact has been persisted, which
|
|
15
|
+
// means binding to it would permanently suppress the quiet window — the opposite of intent.
|
|
16
|
+
// We instead enable the bridge whenever workspaceRoot is provided (same condition as
|
|
17
|
+
// `createWorkspaceHeartbeatRunner` uses for injecting quietWorkflow).
|
|
18
|
+
const quietEnabledBridge = !!options.workspaceRoot;
|
|
19
|
+
// T2.2.2: Load life evidence from state DB when available so SnapshotInputs carries real refs.
|
|
20
|
+
let lifeEvidenceRefs;
|
|
21
|
+
let platformEventCount;
|
|
22
|
+
let workEventCount;
|
|
23
|
+
let lifeEvidenceEmptyReason;
|
|
24
|
+
if (options.state && options.workspaceRoot) {
|
|
25
|
+
try {
|
|
26
|
+
const snapshot = await loadLifeEvidenceSnapshot(options.state, options.workspaceRoot, { limit: 50 },
|
|
27
|
+
// Skip repair gate here — runner is called inside a live cycle; gate ran at startup.
|
|
28
|
+
{ runRepairGate: false });
|
|
29
|
+
lifeEvidenceRefs = snapshot.evidenceRefs.map((ref) => ({
|
|
30
|
+
id: ref.id,
|
|
31
|
+
kind: ref.kind,
|
|
32
|
+
uri: ref.uri,
|
|
33
|
+
}));
|
|
34
|
+
platformEventCount = snapshot.platformEvents.length;
|
|
35
|
+
workEventCount = snapshot.workEvents.length;
|
|
36
|
+
if (snapshot.empty) {
|
|
37
|
+
// L-01: Currently snapshot only exposes `empty` boolean.
|
|
38
|
+
// Future: if snapshot adds `emptyReason` (e.g. "redacted_only"), map it here.
|
|
39
|
+
lifeEvidenceEmptyReason = "no_sources";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// If evidence load fails, signal state_unavailable rather than crashing the cycle.
|
|
44
|
+
lifeEvidenceRefs = [];
|
|
45
|
+
platformEventCount = 0;
|
|
46
|
+
workEventCount = 0;
|
|
47
|
+
lifeEvidenceEmptyReason = "state_unavailable";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
// No state wired — record that life evidence wasn't loaded so guards can reason honestly.
|
|
52
|
+
lifeEvidenceEmptyReason = "state_unavailable";
|
|
53
|
+
}
|
|
54
|
+
// T2.1.4: Load accepted goals from state DB when available.
|
|
55
|
+
// M-03: typed as GoalContext to avoid coupling to the full AgentGoal schema.
|
|
56
|
+
let acceptedGoals;
|
|
57
|
+
let acceptedGoalsLoadError;
|
|
58
|
+
if (options.state) {
|
|
59
|
+
try {
|
|
60
|
+
const goalStore = createAgentGoalStore(options.state);
|
|
61
|
+
acceptedGoals = await goalStore.listAgentGoals({
|
|
62
|
+
statuses: ["accepted"],
|
|
63
|
+
limit: 20,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
acceptedGoals = [];
|
|
68
|
+
acceptedGoalsLoadError = err instanceof Error ? err.message : String(err);
|
|
69
|
+
// H-05: Distinguish "load failed" from "no goals" for observability.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// CR-02: Load narrative state and relationship memory when state is available.
|
|
73
|
+
let narrativeState;
|
|
74
|
+
let relationshipMemory;
|
|
75
|
+
let identity;
|
|
76
|
+
if (options.state) {
|
|
77
|
+
try {
|
|
78
|
+
const narrativeStore = createNarrativeStateStore(options.state);
|
|
79
|
+
narrativeState = (await narrativeStore.loadNarrativeState()) ?? undefined;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Narrative state is optional; failure should not block the cycle.
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const relationshipStore = createRelationshipMemoryStore(options.state);
|
|
86
|
+
relationshipMemory = (await relationshipStore.loadRelationshipMemory()) ?? undefined;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Relationship memory is optional; failure should not block the cycle.
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const identityStore = createIdentityProfileStore(options.state);
|
|
93
|
+
const identityResult = await identityStore.loadIdentityProfile("default");
|
|
94
|
+
if (identityResult.status === "loaded" && identityResult.profile) {
|
|
95
|
+
identity = identityResult.profile;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Identity is optional; failure should not block the cycle.
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
mode,
|
|
104
|
+
currentWindowId: status.rhythm.windowId ?? "workspace-default",
|
|
105
|
+
pendingObligations: [],
|
|
106
|
+
recentOutreachHashes: [],
|
|
107
|
+
deniedIntents: [],
|
|
108
|
+
budgets: { socialUsed: 0, socialLimit: 5 },
|
|
109
|
+
awaitingUserInput: false,
|
|
110
|
+
quietEnabledBridge,
|
|
111
|
+
deliveryCapability: { target: "none" },
|
|
112
|
+
lifeEvidenceRefs,
|
|
113
|
+
platformEventCount,
|
|
114
|
+
workEventCount,
|
|
115
|
+
lifeEvidenceEmptyReason,
|
|
116
|
+
acceptedGoals,
|
|
117
|
+
acceptedGoalsLoadError,
|
|
118
|
+
narrativeState,
|
|
119
|
+
relationshipMemory,
|
|
120
|
+
affordanceMap: options.affordanceMap,
|
|
121
|
+
identity,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
export function createWorkspaceHeartbeatRunner(readModels, options = {}) {
|
|
125
|
+
// T1.2.4: inject quietWorkflow dep when workspaceRoot is set so quiet/reflection intents
|
|
126
|
+
// can trigger runSourceBackedQuiet and persist artifacts to disk.
|
|
127
|
+
const quietEnabled = options.workspaceRoot && options.enableQuietWorkflow !== false;
|
|
128
|
+
// T2.1.5: when state DB is wired, create a NarrativeStateStore for heartbeat updates.
|
|
129
|
+
const narrativeStateStore = options.state
|
|
130
|
+
? createNarrativeStateStore(options.state)
|
|
131
|
+
: undefined;
|
|
132
|
+
return async (signal) => {
|
|
133
|
+
const cycle = await runHeartbeatCycle({
|
|
134
|
+
signal,
|
|
135
|
+
runtimeAvailable: true,
|
|
136
|
+
deps: {
|
|
137
|
+
loadSnapshotInputs: () => loadSnapshotInputsForWorkspaceHeartbeat(readModels, {
|
|
138
|
+
state: options.state,
|
|
139
|
+
workspaceRoot: options.workspaceRoot,
|
|
140
|
+
affordanceMap: options.affordanceMap,
|
|
141
|
+
}),
|
|
142
|
+
// T1.2.4: pass quietWorkflow dep so runSourceBackedQuiet can persist artifacts.
|
|
143
|
+
quietWorkflow: quietEnabled
|
|
144
|
+
? {
|
|
145
|
+
workspaceRoot: options.workspaceRoot,
|
|
146
|
+
// v7 T-V7C.C.3: pass Dream schedule port so Quiet completion triggers Dream.
|
|
147
|
+
dreamSchedulePort: options.dreamSchedulePort,
|
|
148
|
+
}
|
|
149
|
+
: undefined,
|
|
150
|
+
connectorExecutor: options.connectorExecutor,
|
|
151
|
+
narrativeStateStore,
|
|
152
|
+
// T3.3.1: pass state + workspaceRoot so connector effects can write life evidence.
|
|
153
|
+
state: options.state,
|
|
154
|
+
workspaceRoot: options.workspaceRoot,
|
|
155
|
+
// T2.4.1: pass registry so planner resolves platform-specific intents.
|
|
156
|
+
connectorRegistry: options.connectorRegistry,
|
|
157
|
+
// v7 T-V7C.C.2: pass experience writer for heartbeat connector attempts.
|
|
158
|
+
experienceWriter: options.experienceWriter,
|
|
159
|
+
// v7 T-CP.C.3: pass goal lifecycle policy for pre-planning goal evaluation.
|
|
160
|
+
goalLifecyclePolicy: options.goalLifecyclePolicy,
|
|
161
|
+
// v7 T-CP.C.3: pass idle curiosity policy for goal-less exploration.
|
|
162
|
+
idleCuriosityPolicy: options.idleCuriosityPolicy,
|
|
163
|
+
// v7 T-BTS.C.5: pass circuit breaker manager for execution health tracking.
|
|
164
|
+
circuitBreakerManager: options.circuitBreakerManager,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
if (options.runtimeRecorder) {
|
|
168
|
+
try {
|
|
169
|
+
await options.runtimeRecorder.recordHeartbeatCycle({ cycle, signal });
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// T1.2.3: recorder must never break the heartbeat surface response.
|
|
173
|
+
// Failure here means status simply remains at its previous aggregate; the
|
|
174
|
+
// cycle outcome itself is still returned to the caller.
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// v7 T-V7C.C.3 / T-V7C.C.6: After each cycle, attempt HeartbeatDigest generation
|
|
178
|
+
// and persist to heartbeat_digest table so the digest index actually grows.
|
|
179
|
+
// Only runs inside the designated UTC digest window hour, or on every cycle when
|
|
180
|
+
// digestWindowHour is unset (test / always-on mode).
|
|
181
|
+
if (options.digestOpts) {
|
|
182
|
+
const { assemblerDeps, digestWindowHour } = options.digestOpts;
|
|
183
|
+
const nowHour = new Date().getUTCHours();
|
|
184
|
+
const inDigestWindow = digestWindowHour === undefined || nowHour === digestWindowHour;
|
|
185
|
+
if (inDigestWindow) {
|
|
186
|
+
try {
|
|
187
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
188
|
+
const assembledDigest = await generateHeartbeatDigest(date, assemblerDeps);
|
|
189
|
+
// v7 T-V7C.C.6: Persist assembled digest to heartbeat_digest table when state DB is wired.
|
|
190
|
+
if (options.state) {
|
|
191
|
+
const digestStore = createHistoryDigestStore(options.state);
|
|
192
|
+
await digestStore.writeHeartbeatDigest(toStoreDigest(assembledDigest));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
// Digest generation / persistence must not break the heartbeat cycle response.
|
|
197
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
198
|
+
console.warn(`[workspace-heartbeat-runner] Digest generation/persistence failed: ${msg}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return cycle;
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Bridge: converts the assembler-facing HeartbeatDigest into the storage-facing
|
|
207
|
+
* HeartbeatDigest (shared/types/v7-entities.ts) so it can be written to heartbeat_digest.
|
|
208
|
+
*
|
|
209
|
+
* The two shapes diverge by design: assembler is an audit-aggregate rich view;
|
|
210
|
+
* store is a flattened day-keyed row. Mapping is lossy but sufficient for growth.
|
|
211
|
+
*/
|
|
212
|
+
function toStoreDigest(d) {
|
|
213
|
+
return {
|
|
214
|
+
digestId: `digest:${d.date}:${Date.now()}`,
|
|
215
|
+
day: d.date,
|
|
216
|
+
connectorSummary: d.connectorSummary.map((c) => ({
|
|
217
|
+
platformId: c.platformId,
|
|
218
|
+
status: c.blockedCount > 0
|
|
219
|
+
? "blocked"
|
|
220
|
+
: c.circuitOpenCount > 0
|
|
221
|
+
? "blocked"
|
|
222
|
+
: c.failureCount > 0
|
|
223
|
+
? "degraded"
|
|
224
|
+
: "ok",
|
|
225
|
+
attemptCount: c.successCount + c.failureCount + c.blockedCount,
|
|
226
|
+
})),
|
|
227
|
+
goalSummary: [
|
|
228
|
+
{ kind: "new", activeCount: d.goalSummary.newGoals },
|
|
229
|
+
{ kind: "completed", activeCount: d.goalSummary.completedGoals },
|
|
230
|
+
{ kind: "expired", activeCount: d.goalSummary.expiredGoals },
|
|
231
|
+
{ kind: "replaced", activeCount: d.goalSummary.replacedGoals },
|
|
232
|
+
{ kind: "active", activeCount: d.goalSummary.activeGoals },
|
|
233
|
+
].filter((g) => g.activeCount > 0),
|
|
234
|
+
quietCount: d.quietDreamSummary.quietRuns,
|
|
235
|
+
dreamCount: d.quietDreamSummary.dreamRuns,
|
|
236
|
+
breakerSummary: d.healthSummary.circuitBreakerChanges > 0
|
|
237
|
+
? [{ connectorId: "aggregate", state: "changed" }]
|
|
238
|
+
: [],
|
|
239
|
+
healthStatus: d.healthSummary.auditChainHealthy ? "ok" : "degraded",
|
|
240
|
+
createdAt: d.generatedAt,
|
|
241
|
+
};
|
|
242
|
+
}
|