@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.
Files changed (53) hide show
  1. package/openclaw.plugin.json +1 -1
  2. package/package.json +1 -1
  3. package/runtime/cli/ops/heartbeat-surface.d.ts +20 -0
  4. package/runtime/cli/ops/heartbeat-surface.js +72 -1
  5. package/runtime/cli/ops/ops-router.js +119 -31
  6. package/runtime/connectors/base/contract.d.ts +11 -0
  7. package/runtime/connectors/base/failure-taxonomy.js +45 -26
  8. package/runtime/connectors/base/policy-bound-write-dispatch.d.ts +29 -0
  9. package/runtime/connectors/base/policy-bound-write-dispatch.js +127 -0
  10. package/runtime/connectors/services/connector-cooldown-port.d.ts +22 -0
  11. package/runtime/connectors/services/connector-cooldown-port.js +123 -0
  12. package/runtime/connectors/services/connector-executor-adapter.js +10 -4
  13. package/runtime/connectors/services/credential-route-context.d.ts +3 -2
  14. package/runtime/connectors/services/credential-route-context.js +19 -3
  15. package/runtime/core/second-nature/action/action-closure-recorder.d.ts +4 -0
  16. package/runtime/core/second-nature/action/action-closure-recorder.js +5 -0
  17. package/runtime/core/second-nature/action/action-proposal-builder.js +1 -0
  18. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.d.ts +2 -0
  19. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.js +412 -25
  20. package/runtime/core/second-nature/control-plane/real-runtime-spine.d.ts +35 -0
  21. package/runtime/core/second-nature/control-plane/real-runtime-spine.js +42 -0
  22. package/runtime/core/second-nature/guidance/impulse-context-reader.d.ts +44 -0
  23. package/runtime/core/second-nature/guidance/impulse-context-reader.js +84 -0
  24. package/runtime/core/second-nature/guidance/impulse-context-writer.d.ts +39 -0
  25. package/runtime/core/second-nature/guidance/impulse-context-writer.js +70 -0
  26. package/runtime/core/second-nature/perception/judgment-engine.d.ts +2 -0
  27. package/runtime/core/second-nature/perception/judgment-engine.js +11 -1
  28. package/runtime/core/second-nature/perception/perception-builder.d.ts +6 -2
  29. package/runtime/core/second-nature/perception/perception-builder.js +18 -7
  30. package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.d.ts +43 -0
  31. package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.js +162 -0
  32. package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.d.ts +2 -2
  33. package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.js +27 -44
  34. package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.d.ts +3 -0
  35. package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.js +4 -0
  36. package/runtime/observability/living-loop-health-gate.d.ts +49 -0
  37. package/runtime/observability/living-loop-health-gate.js +141 -0
  38. package/runtime/observability/loop-status.d.ts +30 -0
  39. package/runtime/observability/loop-status.js +167 -7
  40. package/runtime/observability/services/heartbeat-digest-assembler.d.ts +21 -0
  41. package/runtime/observability/services/heartbeat-digest-assembler.js +44 -0
  42. package/runtime/shared/types/v8-contracts.d.ts +2 -2
  43. package/runtime/storage/db/index.js +60 -6
  44. package/runtime/storage/db/migrations/index.js +4 -0
  45. package/runtime/storage/db/migrations/v8-001-living-perception-loop.js +119 -119
  46. package/runtime/storage/db/migrations/v8-002-perception-contract-alignment.d.ts +12 -0
  47. package/runtime/storage/db/migrations/v8-002-perception-contract-alignment.js +14 -0
  48. package/runtime/storage/db/migrations/v8-003-quiet-closure-refs.d.ts +10 -0
  49. package/runtime/storage/db/migrations/v8-003-quiet-closure-refs.js +12 -0
  50. package/runtime/storage/db/schema/v8-entities.d.ts +874 -0
  51. package/runtime/storage/db/schema/v8-entities.js +62 -1
  52. package/runtime/storage/v8-state-stores.d.ts +41 -2
  53. 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
- // Return cycle result
151
- return {
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
- closureRef: judgmentResult.succeeded.length > 0
155
- ? {
156
- uri: `sn://judgment/${cycleId}`,
157
- family: "judgment",
158
- id: cycleId,
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
- : undefined,
163
- noActionReason: judgmentResult.succeeded.length === 0 ? "proposal_no_action" : undefined,
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>;