@haaaiawd/second-nature 0.2.2 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/runtime/cli/ops/heartbeat-surface.d.ts +20 -0
- package/runtime/cli/ops/heartbeat-surface.js +72 -1
- package/runtime/cli/ops/ops-router.js +119 -31
- package/runtime/connectors/base/contract.d.ts +11 -0
- package/runtime/connectors/base/failure-taxonomy.js +45 -26
- package/runtime/connectors/base/policy-bound-write-dispatch.d.ts +29 -0
- package/runtime/connectors/base/policy-bound-write-dispatch.js +127 -0
- package/runtime/connectors/services/connector-cooldown-port.d.ts +22 -0
- package/runtime/connectors/services/connector-cooldown-port.js +123 -0
- package/runtime/connectors/services/connector-executor-adapter.js +10 -4
- package/runtime/connectors/services/credential-route-context.d.ts +3 -2
- package/runtime/connectors/services/credential-route-context.js +19 -3
- package/runtime/core/second-nature/action/action-closure-recorder.d.ts +4 -0
- package/runtime/core/second-nature/action/action-closure-recorder.js +5 -0
- package/runtime/core/second-nature/action/action-proposal-builder.js +1 -0
- package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.d.ts +2 -0
- package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.js +412 -25
- package/runtime/core/second-nature/control-plane/real-runtime-spine.d.ts +35 -0
- package/runtime/core/second-nature/control-plane/real-runtime-spine.js +42 -0
- package/runtime/core/second-nature/guidance/impulse-context-reader.d.ts +44 -0
- package/runtime/core/second-nature/guidance/impulse-context-reader.js +84 -0
- package/runtime/core/second-nature/guidance/impulse-context-writer.d.ts +39 -0
- package/runtime/core/second-nature/guidance/impulse-context-writer.js +70 -0
- package/runtime/core/second-nature/perception/judgment-engine.d.ts +2 -0
- package/runtime/core/second-nature/perception/judgment-engine.js +11 -1
- package/runtime/core/second-nature/perception/perception-builder.d.ts +6 -2
- package/runtime/core/second-nature/perception/perception-builder.js +18 -7
- package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.d.ts +43 -0
- package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.js +162 -0
- package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.d.ts +2 -2
- package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.js +27 -44
- package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.d.ts +3 -0
- package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.js +4 -0
- package/runtime/observability/living-loop-health-gate.d.ts +49 -0
- package/runtime/observability/living-loop-health-gate.js +141 -0
- package/runtime/observability/loop-status.d.ts +30 -0
- package/runtime/observability/loop-status.js +167 -7
- package/runtime/observability/services/heartbeat-digest-assembler.d.ts +21 -0
- package/runtime/observability/services/heartbeat-digest-assembler.js +44 -0
- package/runtime/shared/types/v8-contracts.d.ts +2 -2
- package/runtime/storage/db/index.js +60 -6
- package/runtime/storage/db/migrations/index.js +4 -0
- package/runtime/storage/db/migrations/v8-001-living-perception-loop.js +119 -119
- package/runtime/storage/db/migrations/v8-002-perception-contract-alignment.d.ts +12 -0
- package/runtime/storage/db/migrations/v8-002-perception-contract-alignment.js +14 -0
- package/runtime/storage/db/migrations/v8-003-quiet-closure-refs.d.ts +10 -0
- package/runtime/storage/db/migrations/v8-003-quiet-closure-refs.js +12 -0
- package/runtime/storage/db/schema/v8-entities.d.ts +874 -0
- package/runtime/storage/db/schema/v8-entities.js +62 -1
- package/runtime/storage/v8-state-stores.d.ts +41 -2
- package/runtime/storage/v8-state-stores.js +206 -2
|
@@ -25,6 +25,12 @@ import { writeHeartbeatCycleTrace, readHeartbeatCycleTraces, } from "../../../st
|
|
|
25
25
|
import { recordLoopStageEvent } from "../../../observability/loop-stage-event-sink.js";
|
|
26
26
|
import { buildPerceptionCards } from "../perception/perception-builder.js";
|
|
27
27
|
import { runAgentJudgments } from "../perception/judgment-engine.js";
|
|
28
|
+
import { loadAcceptedProjections } from "./accepted-projection-loader.js";
|
|
29
|
+
import { buildActionProposal, } from "../action/action-proposal-builder.js";
|
|
30
|
+
import { evaluateActionPolicy } from "../action/autonomy-policy-evaluator.js";
|
|
31
|
+
import { dispatchAllowedAction } from "../action/policy-bound-dispatch.js";
|
|
32
|
+
import { recordNoActionClosure, recordRememberClosure, recordPolicyOutcomeClosure, recordExecutionClosure, } from "../action/action-closure-recorder.js";
|
|
33
|
+
import { checkDailyRhythm } from "../quiet-dream/daily-rhythm-scheduler.js";
|
|
28
34
|
// ───────────────────────────────────────────────────────────────
|
|
29
35
|
// Helpers
|
|
30
36
|
// ───────────────────────────────────────────────────────────────
|
|
@@ -38,6 +44,66 @@ async function nextCycleSequence(db) {
|
|
|
38
44
|
function buildCycleId(sequence, now) {
|
|
39
45
|
return `cyc_${now.replace(/[:.]/g, "")}_${sequence}`;
|
|
40
46
|
}
|
|
47
|
+
async function advanceAndRecordDailyRhythm(db, cycleId, cycleSequence, cycleRef, now) {
|
|
48
|
+
try {
|
|
49
|
+
const rhythmResult = await checkDailyRhythm(db, { now });
|
|
50
|
+
if ("status" in rhythmResult && rhythmResult.status === "checked") {
|
|
51
|
+
await recordLoopStageEvent(db, {
|
|
52
|
+
id: `evt_${cycleId}_daily_rhythm`,
|
|
53
|
+
cycleId,
|
|
54
|
+
cycleSequence,
|
|
55
|
+
stage: "quiet",
|
|
56
|
+
status: "completed",
|
|
57
|
+
occurredAt: new Date().toISOString(),
|
|
58
|
+
sourceRefs: [
|
|
59
|
+
cycleRef,
|
|
60
|
+
{
|
|
61
|
+
uri: `sn://rhythm/${rhythmResult.state.day}`,
|
|
62
|
+
family: "dream_run",
|
|
63
|
+
id: `rhythm_${rhythmResult.state.day}`,
|
|
64
|
+
redactionClass: "none",
|
|
65
|
+
resolveStatus: "resolvable",
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
return { rhythmState: rhythmResult.state };
|
|
70
|
+
}
|
|
71
|
+
const degraded = rhythmResult;
|
|
72
|
+
await recordLoopStageEvent(db, {
|
|
73
|
+
id: `evt_${cycleId}_daily_rhythm`,
|
|
74
|
+
cycleId,
|
|
75
|
+
cycleSequence,
|
|
76
|
+
stage: "quiet",
|
|
77
|
+
status: "failed",
|
|
78
|
+
occurredAt: new Date().toISOString(),
|
|
79
|
+
reason: degraded.reason,
|
|
80
|
+
sourceRefs: [cycleRef],
|
|
81
|
+
});
|
|
82
|
+
return { rhythmDegraded: degraded };
|
|
83
|
+
}
|
|
84
|
+
catch (rhythmErr) {
|
|
85
|
+
const errMsg = rhythmErr instanceof Error ? rhythmErr.message : String(rhythmErr);
|
|
86
|
+
const degraded = {
|
|
87
|
+
status: "degraded",
|
|
88
|
+
reason: "state_unreadable",
|
|
89
|
+
ownerStage: "quiet",
|
|
90
|
+
sourceRefs: [cycleRef],
|
|
91
|
+
operatorNextAction: `Daily rhythm check failed: ${errMsg.slice(0, 120)}`,
|
|
92
|
+
retryable: true,
|
|
93
|
+
};
|
|
94
|
+
await recordLoopStageEvent(db, {
|
|
95
|
+
id: `evt_${cycleId}_daily_rhythm`,
|
|
96
|
+
cycleId,
|
|
97
|
+
cycleSequence,
|
|
98
|
+
stage: "quiet",
|
|
99
|
+
status: "failed",
|
|
100
|
+
occurredAt: new Date().toISOString(),
|
|
101
|
+
reason: degraded.reason,
|
|
102
|
+
sourceRefs: [cycleRef],
|
|
103
|
+
});
|
|
104
|
+
return { rhythmDegraded: degraded };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
41
107
|
// ───────────────────────────────────────────────────────────────
|
|
42
108
|
// Public API
|
|
43
109
|
// ───────────────────────────────────────────────────────────────
|
|
@@ -45,6 +111,13 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
45
111
|
const now = request.requestedAt ?? new Date().toISOString();
|
|
46
112
|
const cycleSequence = await nextCycleSequence(db);
|
|
47
113
|
const cycleId = buildCycleId(cycleSequence, now);
|
|
114
|
+
const cycleRef = {
|
|
115
|
+
uri: `sn://heartbeat/${cycleId}`,
|
|
116
|
+
family: "audit",
|
|
117
|
+
id: cycleId,
|
|
118
|
+
redactionClass: "none",
|
|
119
|
+
resolveStatus: "resolvable",
|
|
120
|
+
};
|
|
48
121
|
// Write cycle trace — started
|
|
49
122
|
const traceResult = await writeHeartbeatCycleTrace(db, {
|
|
50
123
|
id: cycleId,
|
|
@@ -53,22 +126,14 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
53
126
|
inputCount: 0,
|
|
54
127
|
outputCount: 0,
|
|
55
128
|
status: "started",
|
|
56
|
-
sourceRefs: [
|
|
57
|
-
{
|
|
58
|
-
uri: `sn://heartbeat/${cycleId}`,
|
|
59
|
-
family: "audit",
|
|
60
|
-
id: cycleId,
|
|
61
|
-
redactionClass: "none",
|
|
62
|
-
resolveStatus: "resolvable",
|
|
63
|
-
},
|
|
64
|
-
],
|
|
129
|
+
sourceRefs: [cycleRef],
|
|
65
130
|
});
|
|
66
131
|
if ("reason" in traceResult) {
|
|
67
132
|
return {
|
|
68
133
|
status: "degraded",
|
|
69
134
|
reason: "state_unreadable",
|
|
70
135
|
ownerStage: "ingestion",
|
|
71
|
-
sourceRefs: [],
|
|
136
|
+
sourceRefs: [cycleRef],
|
|
72
137
|
operatorNextAction: "Retry heartbeat after DB recovery",
|
|
73
138
|
retryable: true,
|
|
74
139
|
};
|
|
@@ -81,7 +146,7 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
81
146
|
stage: "ingestion",
|
|
82
147
|
status: "started",
|
|
83
148
|
occurredAt: now,
|
|
84
|
-
sourceRefs: [],
|
|
149
|
+
sourceRefs: [cycleRef],
|
|
85
150
|
});
|
|
86
151
|
// ── Perception stage ──
|
|
87
152
|
const perceptionResult = await buildPerceptionCards(db, { cycleId, now });
|
|
@@ -98,22 +163,51 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
98
163
|
reason: perceptionDegraded
|
|
99
164
|
? perceptionResult.reason
|
|
100
165
|
: undefined,
|
|
101
|
-
sourceRefs: [],
|
|
166
|
+
sourceRefs: [cycleRef],
|
|
102
167
|
});
|
|
103
168
|
if (perceptionDegraded || !("cards" in perceptionResult)) {
|
|
169
|
+
// Degraded path must still write a closure for observability
|
|
170
|
+
const degradedReason = perceptionDegraded
|
|
171
|
+
? (perceptionResult.reason ?? "state_unreadable")
|
|
172
|
+
: "perception_failed";
|
|
173
|
+
const closureResult = await recordNoActionClosure(db, cycleId, degradedReason, { now });
|
|
174
|
+
let degradedClosureRef;
|
|
175
|
+
if ("closureId" in closureResult) {
|
|
176
|
+
degradedClosureRef = {
|
|
177
|
+
uri: `sn://closure/${closureResult.closureId}`,
|
|
178
|
+
family: "action_closure",
|
|
179
|
+
id: closureResult.closureId,
|
|
180
|
+
redactionClass: "none",
|
|
181
|
+
resolveStatus: "resolvable",
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
await recordLoopStageEvent(db, {
|
|
185
|
+
id: `evt_${cycleId}_closure`,
|
|
186
|
+
cycleId,
|
|
187
|
+
cycleSequence,
|
|
188
|
+
stage: "closure",
|
|
189
|
+
status: "failed",
|
|
190
|
+
occurredAt: new Date().toISOString(),
|
|
191
|
+
reason: degradedReason,
|
|
192
|
+
sourceRefs: degradedClosureRef ? [degradedClosureRef, cycleRef] : [cycleRef],
|
|
193
|
+
});
|
|
194
|
+
const { rhythmState } = await advanceAndRecordDailyRhythm(db, cycleId, cycleSequence, cycleRef, now);
|
|
104
195
|
return {
|
|
105
196
|
cycleId,
|
|
106
197
|
cycleSequence,
|
|
198
|
+
closureRef: degradedClosureRef,
|
|
199
|
+
noActionReason: degradedReason,
|
|
107
200
|
degraded: perceptionDegraded
|
|
108
201
|
? {
|
|
109
202
|
status: "degraded",
|
|
110
203
|
reason: perceptionResult.reason ?? "state_unreadable",
|
|
111
204
|
ownerStage: "perception",
|
|
112
|
-
sourceRefs: [],
|
|
205
|
+
sourceRefs: [cycleRef],
|
|
113
206
|
operatorNextAction: "Retry heartbeat after perception recovery",
|
|
114
207
|
retryable: true,
|
|
115
208
|
}
|
|
116
209
|
: undefined,
|
|
210
|
+
rhythmState,
|
|
117
211
|
};
|
|
118
212
|
}
|
|
119
213
|
const cards = perceptionResult.cards;
|
|
@@ -127,16 +221,44 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
127
221
|
status: "skipped",
|
|
128
222
|
occurredAt: new Date().toISOString(),
|
|
129
223
|
reason: "evidence_batch_empty",
|
|
130
|
-
sourceRefs: [],
|
|
224
|
+
sourceRefs: [cycleRef],
|
|
225
|
+
});
|
|
226
|
+
// Write no-action closure — every cycle must produce exactly one
|
|
227
|
+
const closureResult = await recordNoActionClosure(db, cycleId, "evidence_batch_empty", { now });
|
|
228
|
+
let emptyClosureRef;
|
|
229
|
+
if ("closureId" in closureResult) {
|
|
230
|
+
emptyClosureRef = {
|
|
231
|
+
uri: `sn://closure/${closureResult.closureId}`,
|
|
232
|
+
family: "action_closure",
|
|
233
|
+
id: closureResult.closureId,
|
|
234
|
+
redactionClass: "none",
|
|
235
|
+
resolveStatus: "resolvable",
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
await recordLoopStageEvent(db, {
|
|
239
|
+
id: `evt_${cycleId}_closure`,
|
|
240
|
+
cycleId,
|
|
241
|
+
cycleSequence,
|
|
242
|
+
stage: "closure",
|
|
243
|
+
status: "completed",
|
|
244
|
+
occurredAt: new Date().toISOString(),
|
|
245
|
+
reason: "evidence_batch_empty",
|
|
246
|
+
sourceRefs: emptyClosureRef ? [emptyClosureRef, cycleRef] : [cycleRef],
|
|
131
247
|
});
|
|
248
|
+
const { rhythmState } = await advanceAndRecordDailyRhythm(db, cycleId, cycleSequence, cycleRef, now);
|
|
132
249
|
return {
|
|
133
250
|
cycleId,
|
|
134
251
|
cycleSequence,
|
|
252
|
+
closureRef: emptyClosureRef,
|
|
135
253
|
noActionReason: "evidence_batch_empty",
|
|
254
|
+
rhythmState,
|
|
136
255
|
};
|
|
137
256
|
}
|
|
257
|
+
// ── Context assembly: load accepted projections (T-DQ.R.3) ──
|
|
258
|
+
const projectionResult = await loadAcceptedProjections(db);
|
|
259
|
+
const acceptedProjections = projectionResult.ok ? projectionResult.slice.projections : [];
|
|
138
260
|
// ── Judgment stage ──
|
|
139
|
-
const judgmentResult = await runAgentJudgments(db, cards.map((c) => c.id), { now });
|
|
261
|
+
const judgmentResult = await runAgentJudgments(db, cards.map((c) => c.id), { now, acceptedProjections });
|
|
140
262
|
const judgmentFailed = judgmentResult.failed.length > 0;
|
|
141
263
|
await recordLoopStageEvent(db, {
|
|
142
264
|
id: `evt_${cycleId}_judgment`,
|
|
@@ -145,21 +267,286 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
145
267
|
stage: "judgment",
|
|
146
268
|
status: judgmentFailed ? "failed" : "completed",
|
|
147
269
|
occurredAt: new Date().toISOString(),
|
|
148
|
-
sourceRefs: [],
|
|
270
|
+
sourceRefs: [cycleRef],
|
|
149
271
|
});
|
|
150
|
-
//
|
|
151
|
-
|
|
272
|
+
// ── Action/Closure stage (T-CP.R.2) ──
|
|
273
|
+
// Every cycle must produce exactly one closure or no-action record.
|
|
274
|
+
let closureRef;
|
|
275
|
+
let noActionReason;
|
|
276
|
+
let closureDegraded;
|
|
277
|
+
// Record policy stage started
|
|
278
|
+
await recordLoopStageEvent(db, {
|
|
279
|
+
id: `evt_${cycleId}_policy`,
|
|
152
280
|
cycleId,
|
|
153
281
|
cycleSequence,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
282
|
+
stage: "policy",
|
|
283
|
+
status: "started",
|
|
284
|
+
occurredAt: new Date().toISOString(),
|
|
285
|
+
sourceRefs: [cycleRef],
|
|
286
|
+
});
|
|
287
|
+
if (judgmentResult.succeeded.length === 0) {
|
|
288
|
+
// No actionable verdicts → no-action closure
|
|
289
|
+
const closureResult = await recordNoActionClosure(db, cycleId, "proposal_no_action", { now });
|
|
290
|
+
if ("closureId" in closureResult) {
|
|
291
|
+
closureRef = {
|
|
292
|
+
uri: `sn://closure/${closureResult.closureId}`,
|
|
293
|
+
family: "action_closure",
|
|
294
|
+
id: closureResult.closureId,
|
|
159
295
|
redactionClass: "none",
|
|
160
296
|
resolveStatus: "resolvable",
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
else if ("reason" in closureResult) {
|
|
300
|
+
closureDegraded = closureResult;
|
|
301
|
+
}
|
|
302
|
+
noActionReason = "proposal_no_action";
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
// Find first actionable verdict (non-ignore, non-watch)
|
|
306
|
+
const actionableVerdict = judgmentResult.succeeded.find((v) => v.actionKind !== "ignore" && v.actionKind !== "watch");
|
|
307
|
+
if (!actionableVerdict) {
|
|
308
|
+
// All verdicts are ignore/watch → no-action
|
|
309
|
+
const closureResult = await recordNoActionClosure(db, cycleId, "proposal_no_action", { now });
|
|
310
|
+
if ("closureId" in closureResult) {
|
|
311
|
+
closureRef = {
|
|
312
|
+
uri: `sn://closure/${closureResult.closureId}`,
|
|
313
|
+
family: "action_closure",
|
|
314
|
+
id: closureResult.closureId,
|
|
315
|
+
redactionClass: "none",
|
|
316
|
+
resolveStatus: "resolvable",
|
|
317
|
+
};
|
|
161
318
|
}
|
|
162
|
-
|
|
163
|
-
|
|
319
|
+
else if ("reason" in closureResult) {
|
|
320
|
+
closureDegraded = closureResult;
|
|
321
|
+
}
|
|
322
|
+
noActionReason = "proposal_no_action";
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
// Build proposal for the actionable verdict
|
|
326
|
+
const proposalResult = await buildActionProposal(db, actionableVerdict.id, { now });
|
|
327
|
+
if ("status" in proposalResult && proposalResult.status === "degraded") {
|
|
328
|
+
// Proposal build failed — still need a closure
|
|
329
|
+
closureDegraded = proposalResult;
|
|
330
|
+
const closureResult = await recordNoActionClosure(db, cycleId, closureDegraded.reason, { now });
|
|
331
|
+
if ("closureId" in closureResult) {
|
|
332
|
+
closureRef = {
|
|
333
|
+
uri: `sn://closure/${closureResult.closureId}`,
|
|
334
|
+
family: "action_closure",
|
|
335
|
+
id: closureResult.closureId,
|
|
336
|
+
redactionClass: "none",
|
|
337
|
+
resolveStatus: "resolvable",
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
noActionReason = closureDegraded.reason;
|
|
341
|
+
await recordLoopStageEvent(db, {
|
|
342
|
+
id: `evt_${cycleId}_policy`,
|
|
343
|
+
cycleId,
|
|
344
|
+
cycleSequence,
|
|
345
|
+
stage: "policy",
|
|
346
|
+
status: "failed",
|
|
347
|
+
occurredAt: new Date().toISOString(),
|
|
348
|
+
reason: closureDegraded.reason,
|
|
349
|
+
sourceRefs: closureDegraded.sourceRefs.length > 0 ? closureDegraded.sourceRefs : [cycleRef],
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
else if (proposalResult.status === "no_action") {
|
|
353
|
+
const noAction = proposalResult;
|
|
354
|
+
const closureResult = await recordNoActionClosure(db, cycleId, noAction.reason, { now });
|
|
355
|
+
if ("closureId" in closureResult) {
|
|
356
|
+
closureRef = {
|
|
357
|
+
uri: `sn://closure/${closureResult.closureId}`,
|
|
358
|
+
family: "action_closure",
|
|
359
|
+
id: closureResult.closureId,
|
|
360
|
+
redactionClass: "none",
|
|
361
|
+
resolveStatus: "resolvable",
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
noActionReason = noAction.reason;
|
|
365
|
+
}
|
|
366
|
+
else if (proposalResult.status === "remember_for_review") {
|
|
367
|
+
const remember = proposalResult;
|
|
368
|
+
const closureResult = await recordRememberClosure(db, cycleId, remember.memoryReviewCandidate, { now });
|
|
369
|
+
if ("closureId" in closureResult) {
|
|
370
|
+
closureRef = {
|
|
371
|
+
uri: `sn://closure/${closureResult.closureId}`,
|
|
372
|
+
family: "action_closure",
|
|
373
|
+
id: closureResult.closureId,
|
|
374
|
+
redactionClass: "none",
|
|
375
|
+
resolveStatus: "resolvable",
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
else if ("reason" in closureResult) {
|
|
379
|
+
closureDegraded = closureResult;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
else if (proposalResult.status === "proposal") {
|
|
383
|
+
const { proposal } = proposalResult;
|
|
384
|
+
// Evaluate policy — conservative defaults: no real platform permission, no auto-allow
|
|
385
|
+
const decision = evaluateActionPolicy(proposal, {
|
|
386
|
+
breakerStatus: "closed",
|
|
387
|
+
platformPermissionDeclared: false,
|
|
388
|
+
ownerPreferenceAllowAuto: false,
|
|
389
|
+
}, { now });
|
|
390
|
+
await recordLoopStageEvent(db, {
|
|
391
|
+
id: `evt_${cycleId}_policy`,
|
|
392
|
+
cycleId,
|
|
393
|
+
cycleSequence,
|
|
394
|
+
stage: "policy",
|
|
395
|
+
status: "completed",
|
|
396
|
+
occurredAt: new Date().toISOString(),
|
|
397
|
+
reason: decision.decisionReason,
|
|
398
|
+
sourceRefs: decision.proofRefs,
|
|
399
|
+
});
|
|
400
|
+
// Record execution stage started
|
|
401
|
+
await recordLoopStageEvent(db, {
|
|
402
|
+
id: `evt_${cycleId}_execution`,
|
|
403
|
+
cycleId,
|
|
404
|
+
cycleSequence,
|
|
405
|
+
stage: "execution",
|
|
406
|
+
status: "started",
|
|
407
|
+
occurredAt: new Date().toISOString(),
|
|
408
|
+
sourceRefs: decision.proofRefs,
|
|
409
|
+
});
|
|
410
|
+
// Dispatch — no real external write in T-CP.R.2
|
|
411
|
+
const dispatchResult = dispatchAllowedAction(proposal, decision, { guidanceAvailable: false });
|
|
412
|
+
// Record closure based on dispatch outcome
|
|
413
|
+
if (dispatchResult.type === "none") {
|
|
414
|
+
const closureStatus = decision.decision === "deny" ? "denied" : "deferred";
|
|
415
|
+
const closureResult = await recordPolicyOutcomeClosure(db, cycleId, closureStatus, decision.decisionReason, {
|
|
416
|
+
proposalId: proposal.id,
|
|
417
|
+
decisionId: decision.id,
|
|
418
|
+
platformId: proposal.targetPlatformId,
|
|
419
|
+
capabilityId: proposal.targetCapabilityId,
|
|
420
|
+
nextState: "await_next_cycle",
|
|
421
|
+
}, { now });
|
|
422
|
+
if ("closureId" in closureResult) {
|
|
423
|
+
closureRef = {
|
|
424
|
+
uri: `sn://closure/${closureResult.closureId}`,
|
|
425
|
+
family: "action_closure",
|
|
426
|
+
id: closureResult.closureId,
|
|
427
|
+
redactionClass: "none",
|
|
428
|
+
resolveStatus: "resolvable",
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
else if ("reason" in closureResult) {
|
|
432
|
+
closureDegraded = closureResult;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
else if (dispatchResult.type === "guidance_unavailable") {
|
|
436
|
+
const closureResult = await recordPolicyOutcomeClosure(db, cycleId, "downgraded", "guidance_unavailable", {
|
|
437
|
+
proposalId: proposal.id,
|
|
438
|
+
decisionId: decision.id,
|
|
439
|
+
platformId: proposal.targetPlatformId,
|
|
440
|
+
capabilityId: proposal.targetCapabilityId,
|
|
441
|
+
downgradedActionKind: dispatchResult.downgradedActionKind,
|
|
442
|
+
nextState: "await_guidance_recovery",
|
|
443
|
+
}, { now });
|
|
444
|
+
if ("closureId" in closureResult) {
|
|
445
|
+
closureRef = {
|
|
446
|
+
uri: `sn://closure/${closureResult.closureId}`,
|
|
447
|
+
family: "action_closure",
|
|
448
|
+
id: closureResult.closureId,
|
|
449
|
+
redactionClass: "none",
|
|
450
|
+
resolveStatus: "resolvable",
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
else if ("reason" in closureResult) {
|
|
454
|
+
closureDegraded = closureResult;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
else if (dispatchResult.type === "guidance") {
|
|
458
|
+
// Guidance draft dispatch — no external write
|
|
459
|
+
const closureResult = await recordExecutionClosure(db, cycleId, "completed", "policy_allowed", {
|
|
460
|
+
proposalId: proposal.id,
|
|
461
|
+
decisionId: decision.id,
|
|
462
|
+
platformId: proposal.targetPlatformId,
|
|
463
|
+
capabilityId: proposal.targetCapabilityId,
|
|
464
|
+
outputSummary: "Guidance draft dispatched (simulated)",
|
|
465
|
+
nextState: "await_next_cycle",
|
|
466
|
+
}, { now });
|
|
467
|
+
if ("closureId" in closureResult) {
|
|
468
|
+
closureRef = {
|
|
469
|
+
uri: `sn://closure/${closureResult.closureId}`,
|
|
470
|
+
family: "action_closure",
|
|
471
|
+
id: closureResult.closureId,
|
|
472
|
+
redactionClass: "none",
|
|
473
|
+
resolveStatus: "resolvable",
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
else if ("reason" in closureResult) {
|
|
477
|
+
closureDegraded = closureResult;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
else if (dispatchResult.type === "connector") {
|
|
481
|
+
// Connector dispatch — simulated, no real platform write (T-CP.R.2)
|
|
482
|
+
const closureResult = await recordExecutionClosure(db, cycleId, "completed", "policy_allowed", {
|
|
483
|
+
proposalId: proposal.id,
|
|
484
|
+
decisionId: decision.id,
|
|
485
|
+
platformId: proposal.targetPlatformId,
|
|
486
|
+
capabilityId: proposal.targetCapabilityId,
|
|
487
|
+
outputSummary: "Connector dispatch prepared (simulated — T-CP.R.2)",
|
|
488
|
+
nextState: "await_real_execution",
|
|
489
|
+
}, { now });
|
|
490
|
+
if ("closureId" in closureResult) {
|
|
491
|
+
closureRef = {
|
|
492
|
+
uri: `sn://closure/${closureResult.closureId}`,
|
|
493
|
+
family: "action_closure",
|
|
494
|
+
id: closureResult.closureId,
|
|
495
|
+
redactionClass: "none",
|
|
496
|
+
resolveStatus: "resolvable",
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
else if ("reason" in closureResult) {
|
|
500
|
+
closureDegraded = closureResult;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
// Record execution stage completed
|
|
504
|
+
await recordLoopStageEvent(db, {
|
|
505
|
+
id: `evt_${cycleId}_execution`,
|
|
506
|
+
cycleId,
|
|
507
|
+
cycleSequence,
|
|
508
|
+
stage: "execution",
|
|
509
|
+
status: closureDegraded ? "failed" : "completed",
|
|
510
|
+
occurredAt: new Date().toISOString(),
|
|
511
|
+
reason: closureDegraded?.reason,
|
|
512
|
+
sourceRefs: decision.proofRefs,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
// Record closure stage event
|
|
518
|
+
await recordLoopStageEvent(db, {
|
|
519
|
+
id: `evt_${cycleId}_closure`,
|
|
520
|
+
cycleId,
|
|
521
|
+
cycleSequence,
|
|
522
|
+
stage: "closure",
|
|
523
|
+
status: closureDegraded ? "failed" : "completed",
|
|
524
|
+
occurredAt: new Date().toISOString(),
|
|
525
|
+
reason: closureDegraded?.reason ?? noActionReason,
|
|
526
|
+
sourceRefs: closureRef ? [closureRef, cycleRef] : [cycleRef],
|
|
527
|
+
});
|
|
528
|
+
// Final safety net: if somehow nothing was recorded, write a degraded no-action
|
|
529
|
+
if (!closureRef && !noActionReason && !closureDegraded) {
|
|
530
|
+
const fallback = await recordNoActionClosure(db, cycleId, "proposal_no_action", { now });
|
|
531
|
+
if ("closureId" in fallback) {
|
|
532
|
+
closureRef = {
|
|
533
|
+
uri: `sn://closure/${fallback.closureId}`,
|
|
534
|
+
family: "action_closure",
|
|
535
|
+
id: fallback.closureId,
|
|
536
|
+
redactionClass: "none",
|
|
537
|
+
resolveStatus: "resolvable",
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
noActionReason = "proposal_no_action";
|
|
541
|
+
}
|
|
542
|
+
// T-CP.R.3: Advance daily rhythm after closure/no-action
|
|
543
|
+
const { rhythmState } = await advanceAndRecordDailyRhythm(db, cycleId, cycleSequence, cycleRef, now);
|
|
544
|
+
return {
|
|
545
|
+
cycleId,
|
|
546
|
+
cycleSequence,
|
|
547
|
+
closureRef,
|
|
548
|
+
noActionReason,
|
|
549
|
+
degraded: closureDegraded,
|
|
550
|
+
rhythmState,
|
|
164
551
|
};
|
|
165
552
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RealRuntimeSpine — Bridge real workspace heartbeat into v8 action-closure spine.
|
|
3
|
+
*
|
|
4
|
+
* Core logic: Wrap v8 heartbeat orchestrator for CLI/OpenClaw consumption.
|
|
5
|
+
* Ensures every real heartbeat cycle writes exactly one closure/no-action
|
|
6
|
+
* with state-backed persistence and canonical stage events.
|
|
7
|
+
*
|
|
8
|
+
* Design authority:
|
|
9
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/control-plane-system.md §4`
|
|
10
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/action-closure-policy-system.md §4.3`
|
|
11
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/runtime-ops-system.md §4`
|
|
12
|
+
*
|
|
13
|
+
* Boundary:
|
|
14
|
+
* - Does NOT execute real external writes (T-CP.R.2).
|
|
15
|
+
* - Does NOT register fake context-engines.
|
|
16
|
+
* - Delegates all semantic decisions to action-closure-policy-system.
|
|
17
|
+
*/
|
|
18
|
+
import type { StateDatabase } from "../../../storage/db/index.js";
|
|
19
|
+
import type { SourceRef, DegradedOperationResult, V8ReasonCode } from "../../../shared/types/v8-contracts.js";
|
|
20
|
+
import type { DailyRhythmState } from "../quiet-dream/daily-rhythm-scheduler.js";
|
|
21
|
+
export interface RealRuntimeSpineOptions {
|
|
22
|
+
workspaceRoot: string;
|
|
23
|
+
state: StateDatabase;
|
|
24
|
+
requestedAt?: string;
|
|
25
|
+
trigger?: "scheduled" | "manual" | "host";
|
|
26
|
+
}
|
|
27
|
+
export interface RealRuntimeSpineResult {
|
|
28
|
+
cycleId: string;
|
|
29
|
+
cycleSequence: number;
|
|
30
|
+
closureRef?: SourceRef;
|
|
31
|
+
noActionReason?: V8ReasonCode;
|
|
32
|
+
degraded?: DegradedOperationResult;
|
|
33
|
+
rhythmState?: DailyRhythmState;
|
|
34
|
+
}
|
|
35
|
+
export declare function runRealRuntimeHeartbeatCycle(options: RealRuntimeSpineOptions): Promise<RealRuntimeSpineResult | DegradedOperationResult>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RealRuntimeSpine — Bridge real workspace heartbeat into v8 action-closure spine.
|
|
3
|
+
*
|
|
4
|
+
* Core logic: Wrap v8 heartbeat orchestrator for CLI/OpenClaw consumption.
|
|
5
|
+
* Ensures every real heartbeat cycle writes exactly one closure/no-action
|
|
6
|
+
* with state-backed persistence and canonical stage events.
|
|
7
|
+
*
|
|
8
|
+
* Design authority:
|
|
9
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/control-plane-system.md §4`
|
|
10
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/action-closure-policy-system.md §4.3`
|
|
11
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/runtime-ops-system.md §4`
|
|
12
|
+
*
|
|
13
|
+
* Boundary:
|
|
14
|
+
* - Does NOT execute real external writes (T-CP.R.2).
|
|
15
|
+
* - Does NOT register fake context-engines.
|
|
16
|
+
* - Delegates all semantic decisions to action-closure-policy-system.
|
|
17
|
+
*/
|
|
18
|
+
import { runHeartbeatCycle, } from "./heartbeat-orchestrator.js";
|
|
19
|
+
// ───────────────────────────────────────────────────────────────
|
|
20
|
+
// Public API
|
|
21
|
+
// ───────────────────────────────────────────────────────────────
|
|
22
|
+
export async function runRealRuntimeHeartbeatCycle(options) {
|
|
23
|
+
const request = {
|
|
24
|
+
workspaceRoot: options.workspaceRoot,
|
|
25
|
+
requestedAt: options.requestedAt,
|
|
26
|
+
trigger: options.trigger ?? "scheduled",
|
|
27
|
+
};
|
|
28
|
+
const result = await runHeartbeatCycle(options.state, request);
|
|
29
|
+
// Pass through degraded results directly
|
|
30
|
+
if ("status" in result && result.status === "degraded") {
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
const orchestrationResult = result;
|
|
34
|
+
return {
|
|
35
|
+
cycleId: orchestrationResult.cycleId,
|
|
36
|
+
cycleSequence: orchestrationResult.cycleSequence,
|
|
37
|
+
closureRef: orchestrationResult.closureRef,
|
|
38
|
+
noActionReason: orchestrationResult.noActionReason,
|
|
39
|
+
degraded: orchestrationResult.degraded,
|
|
40
|
+
rhythmState: orchestrationResult.rhythmState,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImpulseContextReader — Read agent-facing impulse context artifact from state.
|
|
3
|
+
*
|
|
4
|
+
* Core logic: Retrieve the latest persisted artifact for a given scene/capability
|
|
5
|
+
* combo, with freshness diagnostics and explicit missing-artifact reasons.
|
|
6
|
+
*
|
|
7
|
+
* Design authority:
|
|
8
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/guidance-voice-system.md §1`
|
|
9
|
+
* - `docs/validation/openclaw-plugin-classification.md §5`
|
|
10
|
+
*
|
|
11
|
+
* Dependencies:
|
|
12
|
+
* - `src/storage/v8-state-stores.js` (readImpulseContextArtifact)
|
|
13
|
+
*
|
|
14
|
+
* Boundary:
|
|
15
|
+
* - Does NOT fall back to real-time assembly; returns missing reason when absent.
|
|
16
|
+
* - Does NOT register a fake OpenClaw context-engine.
|
|
17
|
+
*/
|
|
18
|
+
import type { StateDatabase } from "../../../storage/db/index.js";
|
|
19
|
+
export interface ImpulseContextArtifactView {
|
|
20
|
+
id: string;
|
|
21
|
+
sceneType: string;
|
|
22
|
+
capabilityIntent: string | null;
|
|
23
|
+
platformId: string | null;
|
|
24
|
+
capabilityClass: string | null;
|
|
25
|
+
impulseSource: string;
|
|
26
|
+
impulseText: string | null;
|
|
27
|
+
atmosphereText: string | null;
|
|
28
|
+
expressionBoundaryConstraints: string[];
|
|
29
|
+
expressionBoundaryStyle: string | null;
|
|
30
|
+
freshnessVersion: number;
|
|
31
|
+
createdAt: string;
|
|
32
|
+
updatedAt: string;
|
|
33
|
+
}
|
|
34
|
+
export interface MissingArtifactReason {
|
|
35
|
+
available: false;
|
|
36
|
+
reason: "artifact_not_persisted" | "artifact_expired" | "state_unreadable" | "scene_capability_mismatch";
|
|
37
|
+
operatorNextAction: string;
|
|
38
|
+
}
|
|
39
|
+
export type ReadImpulseContextResult = {
|
|
40
|
+
available: true;
|
|
41
|
+
artifact: ImpulseContextArtifactView;
|
|
42
|
+
freshnessMs: number;
|
|
43
|
+
} | MissingArtifactReason;
|
|
44
|
+
export declare function readImpulseContext(db: StateDatabase, sceneType: string, capabilityIntent?: string, platformId?: string): Promise<ReadImpulseContextResult>;
|