@haaaiawd/second-nature 0.2.1 → 0.2.4
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/index.js +5 -1
- package/runtime/cli/ops/heartbeat-surface.d.ts +23 -0
- package/runtime/cli/ops/heartbeat-surface.js +73 -1
- package/runtime/cli/ops/manual-run-dispatcher.d.ts +2 -0
- package/runtime/cli/ops/manual-run-dispatcher.js +10 -0
- package/runtime/cli/ops/ops-router.js +117 -31
- package/runtime/cli/ops/workspace-heartbeat-runner.d.ts +3 -0
- package/runtime/cli/ops/workspace-heartbeat-runner.js +2 -0
- package/runtime/connectors/base/contract.d.ts +10 -0
- 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/core/second-nature/control-plane/heartbeat-orchestrator.js +336 -25
- package/runtime/core/second-nature/control-plane/real-runtime-spine.d.ts +33 -0
- package/runtime/core/second-nature/control-plane/real-runtime-spine.js +41 -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/heartbeat/heartbeat-loop.d.ts +6 -1
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +11 -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/run-source-backed-quiet.d.ts +3 -0
- package/runtime/core/second-nature/quiet/run-source-backed-quiet.js +42 -1
- 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 +157 -0
- package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.js +17 -16
- 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 +45 -0
- package/runtime/observability/living-loop-health-gate.js +94 -0
- package/runtime/observability/loop-status.d.ts +11 -0
- package/runtime/observability/loop-status.js +49 -3
- package/runtime/observability/services/audit-closure-recorders.d.ts +31 -0
- package/runtime/observability/services/audit-closure-recorders.js +87 -0
- package/runtime/observability/services/heartbeat-digest-assembler.d.ts +12 -0
- package/runtime/observability/services/heartbeat-digest-assembler.js +22 -3
- package/runtime/shared/types/v8-contracts.d.ts +2 -2
- package/runtime/storage/db/index.js +34 -0
- 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 +586 -0
- package/runtime/storage/db/schema/v8-entities.js +39 -0
- package/runtime/storage/v8-state-stores.d.ts +32 -2
- package/runtime/storage/v8-state-stores.js +121 -2
|
@@ -25,6 +25,11 @@ 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";
|
|
28
33
|
// ───────────────────────────────────────────────────────────────
|
|
29
34
|
// Helpers
|
|
30
35
|
// ───────────────────────────────────────────────────────────────
|
|
@@ -45,6 +50,13 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
45
50
|
const now = request.requestedAt ?? new Date().toISOString();
|
|
46
51
|
const cycleSequence = await nextCycleSequence(db);
|
|
47
52
|
const cycleId = buildCycleId(cycleSequence, now);
|
|
53
|
+
const cycleRef = {
|
|
54
|
+
uri: `sn://heartbeat/${cycleId}`,
|
|
55
|
+
family: "audit",
|
|
56
|
+
id: cycleId,
|
|
57
|
+
redactionClass: "none",
|
|
58
|
+
resolveStatus: "resolvable",
|
|
59
|
+
};
|
|
48
60
|
// Write cycle trace — started
|
|
49
61
|
const traceResult = await writeHeartbeatCycleTrace(db, {
|
|
50
62
|
id: cycleId,
|
|
@@ -53,22 +65,14 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
53
65
|
inputCount: 0,
|
|
54
66
|
outputCount: 0,
|
|
55
67
|
status: "started",
|
|
56
|
-
sourceRefs: [
|
|
57
|
-
{
|
|
58
|
-
uri: `sn://heartbeat/${cycleId}`,
|
|
59
|
-
family: "audit",
|
|
60
|
-
id: cycleId,
|
|
61
|
-
redactionClass: "none",
|
|
62
|
-
resolveStatus: "resolvable",
|
|
63
|
-
},
|
|
64
|
-
],
|
|
68
|
+
sourceRefs: [cycleRef],
|
|
65
69
|
});
|
|
66
70
|
if ("reason" in traceResult) {
|
|
67
71
|
return {
|
|
68
72
|
status: "degraded",
|
|
69
73
|
reason: "state_unreadable",
|
|
70
74
|
ownerStage: "ingestion",
|
|
71
|
-
sourceRefs: [],
|
|
75
|
+
sourceRefs: [cycleRef],
|
|
72
76
|
operatorNextAction: "Retry heartbeat after DB recovery",
|
|
73
77
|
retryable: true,
|
|
74
78
|
};
|
|
@@ -81,7 +85,7 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
81
85
|
stage: "ingestion",
|
|
82
86
|
status: "started",
|
|
83
87
|
occurredAt: now,
|
|
84
|
-
sourceRefs: [],
|
|
88
|
+
sourceRefs: [cycleRef],
|
|
85
89
|
});
|
|
86
90
|
// ── Perception stage ──
|
|
87
91
|
const perceptionResult = await buildPerceptionCards(db, { cycleId, now });
|
|
@@ -98,18 +102,45 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
98
102
|
reason: perceptionDegraded
|
|
99
103
|
? perceptionResult.reason
|
|
100
104
|
: undefined,
|
|
101
|
-
sourceRefs: [],
|
|
105
|
+
sourceRefs: [cycleRef],
|
|
102
106
|
});
|
|
103
107
|
if (perceptionDegraded || !("cards" in perceptionResult)) {
|
|
108
|
+
// Degraded path must still write a closure for observability
|
|
109
|
+
const degradedReason = perceptionDegraded
|
|
110
|
+
? (perceptionResult.reason ?? "state_unreadable")
|
|
111
|
+
: "perception_failed";
|
|
112
|
+
const closureResult = await recordNoActionClosure(db, cycleId, degradedReason, { now });
|
|
113
|
+
let degradedClosureRef;
|
|
114
|
+
if ("closureId" in closureResult) {
|
|
115
|
+
degradedClosureRef = {
|
|
116
|
+
uri: `sn://closure/${closureResult.closureId}`,
|
|
117
|
+
family: "action_closure",
|
|
118
|
+
id: closureResult.closureId,
|
|
119
|
+
redactionClass: "none",
|
|
120
|
+
resolveStatus: "resolvable",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
await recordLoopStageEvent(db, {
|
|
124
|
+
id: `evt_${cycleId}_closure`,
|
|
125
|
+
cycleId,
|
|
126
|
+
cycleSequence,
|
|
127
|
+
stage: "closure",
|
|
128
|
+
status: "failed",
|
|
129
|
+
occurredAt: new Date().toISOString(),
|
|
130
|
+
reason: degradedReason,
|
|
131
|
+
sourceRefs: degradedClosureRef ? [degradedClosureRef, cycleRef] : [cycleRef],
|
|
132
|
+
});
|
|
104
133
|
return {
|
|
105
134
|
cycleId,
|
|
106
135
|
cycleSequence,
|
|
136
|
+
closureRef: degradedClosureRef,
|
|
137
|
+
noActionReason: degradedReason,
|
|
107
138
|
degraded: perceptionDegraded
|
|
108
139
|
? {
|
|
109
140
|
status: "degraded",
|
|
110
141
|
reason: perceptionResult.reason ?? "state_unreadable",
|
|
111
142
|
ownerStage: "perception",
|
|
112
|
-
sourceRefs: [],
|
|
143
|
+
sourceRefs: [cycleRef],
|
|
113
144
|
operatorNextAction: "Retry heartbeat after perception recovery",
|
|
114
145
|
retryable: true,
|
|
115
146
|
}
|
|
@@ -127,16 +158,42 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
127
158
|
status: "skipped",
|
|
128
159
|
occurredAt: new Date().toISOString(),
|
|
129
160
|
reason: "evidence_batch_empty",
|
|
130
|
-
sourceRefs: [],
|
|
161
|
+
sourceRefs: [cycleRef],
|
|
162
|
+
});
|
|
163
|
+
// Write no-action closure — every cycle must produce exactly one
|
|
164
|
+
const closureResult = await recordNoActionClosure(db, cycleId, "evidence_batch_empty", { now });
|
|
165
|
+
let emptyClosureRef;
|
|
166
|
+
if ("closureId" in closureResult) {
|
|
167
|
+
emptyClosureRef = {
|
|
168
|
+
uri: `sn://closure/${closureResult.closureId}`,
|
|
169
|
+
family: "action_closure",
|
|
170
|
+
id: closureResult.closureId,
|
|
171
|
+
redactionClass: "none",
|
|
172
|
+
resolveStatus: "resolvable",
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
await recordLoopStageEvent(db, {
|
|
176
|
+
id: `evt_${cycleId}_closure`,
|
|
177
|
+
cycleId,
|
|
178
|
+
cycleSequence,
|
|
179
|
+
stage: "closure",
|
|
180
|
+
status: "completed",
|
|
181
|
+
occurredAt: new Date().toISOString(),
|
|
182
|
+
reason: "evidence_batch_empty",
|
|
183
|
+
sourceRefs: emptyClosureRef ? [emptyClosureRef, cycleRef] : [cycleRef],
|
|
131
184
|
});
|
|
132
185
|
return {
|
|
133
186
|
cycleId,
|
|
134
187
|
cycleSequence,
|
|
188
|
+
closureRef: emptyClosureRef,
|
|
135
189
|
noActionReason: "evidence_batch_empty",
|
|
136
190
|
};
|
|
137
191
|
}
|
|
192
|
+
// ── Context assembly: load accepted projections (T-DQ.R.3) ──
|
|
193
|
+
const projectionResult = await loadAcceptedProjections(db);
|
|
194
|
+
const acceptedProjections = projectionResult.ok ? projectionResult.slice.projections : [];
|
|
138
195
|
// ── Judgment stage ──
|
|
139
|
-
const judgmentResult = await runAgentJudgments(db, cards.map((c) => c.id), { now });
|
|
196
|
+
const judgmentResult = await runAgentJudgments(db, cards.map((c) => c.id), { now, acceptedProjections });
|
|
140
197
|
const judgmentFailed = judgmentResult.failed.length > 0;
|
|
141
198
|
await recordLoopStageEvent(db, {
|
|
142
199
|
id: `evt_${cycleId}_judgment`,
|
|
@@ -145,21 +202,275 @@ export async function runHeartbeatCycle(db, request) {
|
|
|
145
202
|
stage: "judgment",
|
|
146
203
|
status: judgmentFailed ? "failed" : "completed",
|
|
147
204
|
occurredAt: new Date().toISOString(),
|
|
148
|
-
sourceRefs: [],
|
|
205
|
+
sourceRefs: [cycleRef],
|
|
149
206
|
});
|
|
150
|
-
//
|
|
151
|
-
|
|
207
|
+
// ── Action/Closure stage (T-CP.R.2) ──
|
|
208
|
+
// Every cycle must produce exactly one closure or no-action record.
|
|
209
|
+
let closureRef;
|
|
210
|
+
let noActionReason;
|
|
211
|
+
let closureDegraded;
|
|
212
|
+
// Record policy stage started
|
|
213
|
+
await recordLoopStageEvent(db, {
|
|
214
|
+
id: `evt_${cycleId}_policy`,
|
|
152
215
|
cycleId,
|
|
153
216
|
cycleSequence,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
217
|
+
stage: "policy",
|
|
218
|
+
status: "started",
|
|
219
|
+
occurredAt: new Date().toISOString(),
|
|
220
|
+
sourceRefs: [cycleRef],
|
|
221
|
+
});
|
|
222
|
+
if (judgmentResult.succeeded.length === 0) {
|
|
223
|
+
// No actionable verdicts → no-action closure
|
|
224
|
+
const closureResult = await recordNoActionClosure(db, cycleId, "proposal_no_action", { now });
|
|
225
|
+
if ("closureId" in closureResult) {
|
|
226
|
+
closureRef = {
|
|
227
|
+
uri: `sn://closure/${closureResult.closureId}`,
|
|
228
|
+
family: "action_closure",
|
|
229
|
+
id: closureResult.closureId,
|
|
159
230
|
redactionClass: "none",
|
|
160
231
|
resolveStatus: "resolvable",
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
else if ("reason" in closureResult) {
|
|
235
|
+
closureDegraded = closureResult;
|
|
236
|
+
}
|
|
237
|
+
noActionReason = "proposal_no_action";
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
// Find first actionable verdict (non-ignore, non-watch)
|
|
241
|
+
const actionableVerdict = judgmentResult.succeeded.find((v) => v.actionKind !== "ignore" && v.actionKind !== "watch");
|
|
242
|
+
if (!actionableVerdict) {
|
|
243
|
+
// All verdicts are ignore/watch → no-action
|
|
244
|
+
const closureResult = await recordNoActionClosure(db, cycleId, "proposal_no_action", { now });
|
|
245
|
+
if ("closureId" in closureResult) {
|
|
246
|
+
closureRef = {
|
|
247
|
+
uri: `sn://closure/${closureResult.closureId}`,
|
|
248
|
+
family: "action_closure",
|
|
249
|
+
id: closureResult.closureId,
|
|
250
|
+
redactionClass: "none",
|
|
251
|
+
resolveStatus: "resolvable",
|
|
252
|
+
};
|
|
161
253
|
}
|
|
162
|
-
|
|
163
|
-
|
|
254
|
+
else if ("reason" in closureResult) {
|
|
255
|
+
closureDegraded = closureResult;
|
|
256
|
+
}
|
|
257
|
+
noActionReason = "proposal_no_action";
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
// Build proposal for the actionable verdict
|
|
261
|
+
const proposalResult = await buildActionProposal(db, actionableVerdict.id, { now });
|
|
262
|
+
if ("status" in proposalResult && proposalResult.status === "degraded") {
|
|
263
|
+
// Proposal build failed — still need a closure
|
|
264
|
+
closureDegraded = proposalResult;
|
|
265
|
+
const closureResult = await recordNoActionClosure(db, cycleId, closureDegraded.reason, { now });
|
|
266
|
+
if ("closureId" in closureResult) {
|
|
267
|
+
closureRef = {
|
|
268
|
+
uri: `sn://closure/${closureResult.closureId}`,
|
|
269
|
+
family: "action_closure",
|
|
270
|
+
id: closureResult.closureId,
|
|
271
|
+
redactionClass: "none",
|
|
272
|
+
resolveStatus: "resolvable",
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
noActionReason = closureDegraded.reason;
|
|
276
|
+
await recordLoopStageEvent(db, {
|
|
277
|
+
id: `evt_${cycleId}_policy`,
|
|
278
|
+
cycleId,
|
|
279
|
+
cycleSequence,
|
|
280
|
+
stage: "policy",
|
|
281
|
+
status: "failed",
|
|
282
|
+
occurredAt: new Date().toISOString(),
|
|
283
|
+
reason: closureDegraded.reason,
|
|
284
|
+
sourceRefs: closureDegraded.sourceRefs.length > 0 ? closureDegraded.sourceRefs : [cycleRef],
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
else if (proposalResult.status === "no_action") {
|
|
288
|
+
const noAction = proposalResult;
|
|
289
|
+
const closureResult = await recordNoActionClosure(db, cycleId, noAction.reason, { now });
|
|
290
|
+
if ("closureId" in closureResult) {
|
|
291
|
+
closureRef = {
|
|
292
|
+
uri: `sn://closure/${closureResult.closureId}`,
|
|
293
|
+
family: "action_closure",
|
|
294
|
+
id: closureResult.closureId,
|
|
295
|
+
redactionClass: "none",
|
|
296
|
+
resolveStatus: "resolvable",
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
noActionReason = noAction.reason;
|
|
300
|
+
}
|
|
301
|
+
else if (proposalResult.status === "remember_for_review") {
|
|
302
|
+
const remember = proposalResult;
|
|
303
|
+
const closureResult = await recordRememberClosure(db, cycleId, remember.memoryReviewCandidate, { now });
|
|
304
|
+
if ("closureId" in closureResult) {
|
|
305
|
+
closureRef = {
|
|
306
|
+
uri: `sn://closure/${closureResult.closureId}`,
|
|
307
|
+
family: "action_closure",
|
|
308
|
+
id: closureResult.closureId,
|
|
309
|
+
redactionClass: "none",
|
|
310
|
+
resolveStatus: "resolvable",
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
else if ("reason" in closureResult) {
|
|
314
|
+
closureDegraded = closureResult;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
else if (proposalResult.status === "proposal") {
|
|
318
|
+
const { proposal } = proposalResult;
|
|
319
|
+
// Evaluate policy — conservative defaults: no real platform permission, no auto-allow
|
|
320
|
+
const decision = evaluateActionPolicy(proposal, {
|
|
321
|
+
breakerStatus: "closed",
|
|
322
|
+
platformPermissionDeclared: false,
|
|
323
|
+
ownerPreferenceAllowAuto: false,
|
|
324
|
+
}, { now });
|
|
325
|
+
await recordLoopStageEvent(db, {
|
|
326
|
+
id: `evt_${cycleId}_policy`,
|
|
327
|
+
cycleId,
|
|
328
|
+
cycleSequence,
|
|
329
|
+
stage: "policy",
|
|
330
|
+
status: "completed",
|
|
331
|
+
occurredAt: new Date().toISOString(),
|
|
332
|
+
reason: decision.decisionReason,
|
|
333
|
+
sourceRefs: decision.proofRefs,
|
|
334
|
+
});
|
|
335
|
+
// Record execution stage started
|
|
336
|
+
await recordLoopStageEvent(db, {
|
|
337
|
+
id: `evt_${cycleId}_execution`,
|
|
338
|
+
cycleId,
|
|
339
|
+
cycleSequence,
|
|
340
|
+
stage: "execution",
|
|
341
|
+
status: "started",
|
|
342
|
+
occurredAt: new Date().toISOString(),
|
|
343
|
+
sourceRefs: decision.proofRefs,
|
|
344
|
+
});
|
|
345
|
+
// Dispatch — no real external write in T-CP.R.2
|
|
346
|
+
const dispatchResult = dispatchAllowedAction(proposal, decision, { guidanceAvailable: false });
|
|
347
|
+
// Record closure based on dispatch outcome
|
|
348
|
+
if (dispatchResult.type === "none") {
|
|
349
|
+
const closureStatus = decision.decision === "deny" ? "denied" : "deferred";
|
|
350
|
+
const closureResult = await recordPolicyOutcomeClosure(db, cycleId, closureStatus, decision.decisionReason, {
|
|
351
|
+
proposalId: proposal.id,
|
|
352
|
+
decisionId: decision.id,
|
|
353
|
+
nextState: "await_next_cycle",
|
|
354
|
+
}, { 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
|
+
else if ("reason" in closureResult) {
|
|
365
|
+
closureDegraded = closureResult;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
else if (dispatchResult.type === "guidance_unavailable") {
|
|
369
|
+
const closureResult = await recordPolicyOutcomeClosure(db, cycleId, "downgraded", "guidance_unavailable", {
|
|
370
|
+
proposalId: proposal.id,
|
|
371
|
+
decisionId: decision.id,
|
|
372
|
+
downgradedActionKind: dispatchResult.downgradedActionKind,
|
|
373
|
+
nextState: "await_guidance_recovery",
|
|
374
|
+
}, { now });
|
|
375
|
+
if ("closureId" in closureResult) {
|
|
376
|
+
closureRef = {
|
|
377
|
+
uri: `sn://closure/${closureResult.closureId}`,
|
|
378
|
+
family: "action_closure",
|
|
379
|
+
id: closureResult.closureId,
|
|
380
|
+
redactionClass: "none",
|
|
381
|
+
resolveStatus: "resolvable",
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
else if ("reason" in closureResult) {
|
|
385
|
+
closureDegraded = closureResult;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
else if (dispatchResult.type === "guidance") {
|
|
389
|
+
// Guidance draft dispatch — no external write
|
|
390
|
+
const closureResult = await recordExecutionClosure(db, cycleId, "completed", "policy_allowed", {
|
|
391
|
+
proposalId: proposal.id,
|
|
392
|
+
decisionId: decision.id,
|
|
393
|
+
outputSummary: "Guidance draft dispatched (simulated)",
|
|
394
|
+
nextState: "await_next_cycle",
|
|
395
|
+
}, { now });
|
|
396
|
+
if ("closureId" in closureResult) {
|
|
397
|
+
closureRef = {
|
|
398
|
+
uri: `sn://closure/${closureResult.closureId}`,
|
|
399
|
+
family: "action_closure",
|
|
400
|
+
id: closureResult.closureId,
|
|
401
|
+
redactionClass: "none",
|
|
402
|
+
resolveStatus: "resolvable",
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
else if ("reason" in closureResult) {
|
|
406
|
+
closureDegraded = closureResult;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
else if (dispatchResult.type === "connector") {
|
|
410
|
+
// Connector dispatch — simulated, no real platform write (T-CP.R.2)
|
|
411
|
+
const closureResult = await recordExecutionClosure(db, cycleId, "completed", "policy_allowed", {
|
|
412
|
+
proposalId: proposal.id,
|
|
413
|
+
decisionId: decision.id,
|
|
414
|
+
outputSummary: "Connector dispatch prepared (simulated — T-CP.R.2)",
|
|
415
|
+
nextState: "await_real_execution",
|
|
416
|
+
}, { now });
|
|
417
|
+
if ("closureId" in closureResult) {
|
|
418
|
+
closureRef = {
|
|
419
|
+
uri: `sn://closure/${closureResult.closureId}`,
|
|
420
|
+
family: "action_closure",
|
|
421
|
+
id: closureResult.closureId,
|
|
422
|
+
redactionClass: "none",
|
|
423
|
+
resolveStatus: "resolvable",
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
else if ("reason" in closureResult) {
|
|
427
|
+
closureDegraded = closureResult;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// Record execution stage completed
|
|
431
|
+
await recordLoopStageEvent(db, {
|
|
432
|
+
id: `evt_${cycleId}_execution`,
|
|
433
|
+
cycleId,
|
|
434
|
+
cycleSequence,
|
|
435
|
+
stage: "execution",
|
|
436
|
+
status: closureDegraded ? "failed" : "completed",
|
|
437
|
+
occurredAt: new Date().toISOString(),
|
|
438
|
+
reason: closureDegraded?.reason,
|
|
439
|
+
sourceRefs: decision.proofRefs,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
// Record closure stage event
|
|
445
|
+
await recordLoopStageEvent(db, {
|
|
446
|
+
id: `evt_${cycleId}_closure`,
|
|
447
|
+
cycleId,
|
|
448
|
+
cycleSequence,
|
|
449
|
+
stage: "closure",
|
|
450
|
+
status: closureDegraded ? "failed" : "completed",
|
|
451
|
+
occurredAt: new Date().toISOString(),
|
|
452
|
+
reason: closureDegraded?.reason ?? noActionReason,
|
|
453
|
+
sourceRefs: closureRef ? [closureRef, cycleRef] : [cycleRef],
|
|
454
|
+
});
|
|
455
|
+
// Final safety net: if somehow nothing was recorded, write a degraded no-action
|
|
456
|
+
if (!closureRef && !noActionReason && !closureDegraded) {
|
|
457
|
+
const fallback = await recordNoActionClosure(db, cycleId, "proposal_no_action", { now });
|
|
458
|
+
if ("closureId" in fallback) {
|
|
459
|
+
closureRef = {
|
|
460
|
+
uri: `sn://closure/${fallback.closureId}`,
|
|
461
|
+
family: "action_closure",
|
|
462
|
+
id: fallback.closureId,
|
|
463
|
+
redactionClass: "none",
|
|
464
|
+
resolveStatus: "resolvable",
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
noActionReason = "proposal_no_action";
|
|
468
|
+
}
|
|
469
|
+
return {
|
|
470
|
+
cycleId,
|
|
471
|
+
cycleSequence,
|
|
472
|
+
closureRef,
|
|
473
|
+
noActionReason,
|
|
474
|
+
degraded: closureDegraded,
|
|
164
475
|
};
|
|
165
476
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
export interface RealRuntimeSpineOptions {
|
|
21
|
+
workspaceRoot: string;
|
|
22
|
+
state: StateDatabase;
|
|
23
|
+
requestedAt?: string;
|
|
24
|
+
trigger?: "scheduled" | "manual" | "host";
|
|
25
|
+
}
|
|
26
|
+
export interface RealRuntimeSpineResult {
|
|
27
|
+
cycleId: string;
|
|
28
|
+
cycleSequence: number;
|
|
29
|
+
closureRef?: SourceRef;
|
|
30
|
+
noActionReason?: V8ReasonCode;
|
|
31
|
+
degraded?: DegradedOperationResult;
|
|
32
|
+
}
|
|
33
|
+
export declare function runRealRuntimeHeartbeatCycle(options: RealRuntimeSpineOptions): Promise<RealRuntimeSpineResult | DegradedOperationResult>;
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
};
|
|
41
|
+
}
|
|
@@ -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>;
|
|
@@ -0,0 +1,84 @@
|
|
|
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 { readImpulseContextArtifact } from "../../../storage/v8-state-stores.js";
|
|
19
|
+
// ───────────────────────────────────────────────────────────────
|
|
20
|
+
// Helpers
|
|
21
|
+
// ───────────────────────────────────────────────────────────────
|
|
22
|
+
function parseConstraints(json) {
|
|
23
|
+
if (!json)
|
|
24
|
+
return [];
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(json);
|
|
27
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// ───────────────────────────────────────────────────────────────
|
|
34
|
+
// Public API
|
|
35
|
+
// ───────────────────────────────────────────────────────────────
|
|
36
|
+
export async function readImpulseContext(db, sceneType, capabilityIntent, platformId) {
|
|
37
|
+
const result = await readImpulseContextArtifact(db, sceneType, capabilityIntent, platformId);
|
|
38
|
+
if (result.degraded) {
|
|
39
|
+
return {
|
|
40
|
+
available: false,
|
|
41
|
+
reason: "state_unreadable",
|
|
42
|
+
operatorNextAction: "Check state database connectivity",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const row = result.row;
|
|
46
|
+
if (!row) {
|
|
47
|
+
return {
|
|
48
|
+
available: false,
|
|
49
|
+
reason: "artifact_not_persisted",
|
|
50
|
+
operatorNextAction: `Run guidance_payload for scene=${sceneType} cap=${capabilityIntent ?? "any"} to generate artifact`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
const updatedAt = new Date(row.updatedAt).getTime();
|
|
55
|
+
const freshnessMs = now - updatedAt;
|
|
56
|
+
// Expire artifacts older than 24 hours
|
|
57
|
+
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
58
|
+
if (freshnessMs > ONE_DAY_MS) {
|
|
59
|
+
return {
|
|
60
|
+
available: false,
|
|
61
|
+
reason: "artifact_expired",
|
|
62
|
+
operatorNextAction: `Re-run guidance_payload for scene=${sceneType} — artifact is stale (${Math.round(freshnessMs / 3600000)}h old)`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
available: true,
|
|
67
|
+
artifact: {
|
|
68
|
+
id: row.id,
|
|
69
|
+
sceneType: row.sceneType,
|
|
70
|
+
capabilityIntent: row.capabilityIntent,
|
|
71
|
+
platformId: row.platformId,
|
|
72
|
+
capabilityClass: row.capabilityClass,
|
|
73
|
+
impulseSource: row.impulseSource,
|
|
74
|
+
impulseText: row.impulseText,
|
|
75
|
+
atmosphereText: row.atmosphereText,
|
|
76
|
+
expressionBoundaryConstraints: parseConstraints(row.expressionBoundaryConstraintsJson),
|
|
77
|
+
expressionBoundaryStyle: row.expressionBoundaryStyle,
|
|
78
|
+
freshnessVersion: row.freshnessVersion,
|
|
79
|
+
createdAt: row.createdAt,
|
|
80
|
+
updatedAt: row.updatedAt,
|
|
81
|
+
},
|
|
82
|
+
freshnessMs,
|
|
83
|
+
};
|
|
84
|
+
}
|