@haaaiawd/second-nature 0.2.2 → 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.
Files changed (42) 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 +115 -31
  6. package/runtime/connectors/base/contract.d.ts +10 -0
  7. package/runtime/connectors/base/policy-bound-write-dispatch.d.ts +29 -0
  8. package/runtime/connectors/base/policy-bound-write-dispatch.js +127 -0
  9. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.js +336 -25
  10. package/runtime/core/second-nature/control-plane/real-runtime-spine.d.ts +33 -0
  11. package/runtime/core/second-nature/control-plane/real-runtime-spine.js +41 -0
  12. package/runtime/core/second-nature/guidance/impulse-context-reader.d.ts +44 -0
  13. package/runtime/core/second-nature/guidance/impulse-context-reader.js +84 -0
  14. package/runtime/core/second-nature/guidance/impulse-context-writer.d.ts +39 -0
  15. package/runtime/core/second-nature/guidance/impulse-context-writer.js +70 -0
  16. package/runtime/core/second-nature/perception/judgment-engine.d.ts +2 -0
  17. package/runtime/core/second-nature/perception/judgment-engine.js +11 -1
  18. package/runtime/core/second-nature/perception/perception-builder.d.ts +6 -2
  19. package/runtime/core/second-nature/perception/perception-builder.js +18 -7
  20. package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.d.ts +43 -0
  21. package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.js +157 -0
  22. package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.js +17 -16
  23. package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.d.ts +3 -0
  24. package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.js +4 -0
  25. package/runtime/observability/living-loop-health-gate.d.ts +45 -0
  26. package/runtime/observability/living-loop-health-gate.js +94 -0
  27. package/runtime/observability/loop-status.d.ts +11 -0
  28. package/runtime/observability/loop-status.js +49 -3
  29. package/runtime/observability/services/heartbeat-digest-assembler.d.ts +12 -0
  30. package/runtime/observability/services/heartbeat-digest-assembler.js +9 -0
  31. package/runtime/shared/types/v8-contracts.d.ts +2 -2
  32. package/runtime/storage/db/index.js +34 -0
  33. package/runtime/storage/db/migrations/index.js +4 -0
  34. package/runtime/storage/db/migrations/v8-001-living-perception-loop.js +119 -119
  35. package/runtime/storage/db/migrations/v8-002-perception-contract-alignment.d.ts +12 -0
  36. package/runtime/storage/db/migrations/v8-002-perception-contract-alignment.js +14 -0
  37. package/runtime/storage/db/migrations/v8-003-quiet-closure-refs.d.ts +10 -0
  38. package/runtime/storage/db/migrations/v8-003-quiet-closure-refs.js +12 -0
  39. package/runtime/storage/db/schema/v8-entities.d.ts +586 -0
  40. package/runtime/storage/db/schema/v8-entities.js +39 -0
  41. package/runtime/storage/v8-state-stores.d.ts +32 -2
  42. 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
- // Return cycle result
151
- return {
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
- closureRef: judgmentResult.succeeded.length > 0
155
- ? {
156
- uri: `sn://judgment/${cycleId}`,
157
- family: "judgment",
158
- id: cycleId,
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
- : undefined,
163
- noActionReason: judgmentResult.succeeded.length === 0 ? "proposal_no_action" : undefined,
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
+ }