@haaaiawd/second-nature 0.1.27 → 0.1.29

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 (157) hide show
  1. package/SKILL.md +35 -33
  2. package/agent-inner-guide.md +144 -124
  3. package/index.js +76 -1
  4. package/openclaw.plugin.json +2 -2
  5. package/package.json +2 -1
  6. package/runtime/cli/commands/connector-behavior.d.ts +20 -0
  7. package/runtime/cli/commands/connector-behavior.js +160 -0
  8. package/runtime/cli/commands/index.js +8 -0
  9. package/runtime/cli/index.js +9 -2
  10. package/runtime/cli/ops/manual-run-dispatcher.d.ts +79 -0
  11. package/runtime/cli/ops/manual-run-dispatcher.js +110 -0
  12. package/runtime/cli/ops/ops-router.d.ts +45 -4
  13. package/runtime/cli/ops/ops-router.js +543 -2
  14. package/runtime/cli/read-models/index.js +35 -18
  15. package/runtime/cli/read-models/types.d.ts +1 -0
  16. package/runtime/connectors/agent-network/agent-world/adapter.d.ts +1 -0
  17. package/runtime/connectors/agent-network/agent-world/adapter.js +2 -2
  18. package/runtime/connectors/base/contract.d.ts +4 -1
  19. package/runtime/connectors/base/contract.js +5 -1
  20. package/runtime/connectors/base/effect-commit-ledger-sqlite.d.ts +31 -0
  21. package/runtime/connectors/base/effect-commit-ledger-sqlite.js +86 -0
  22. package/runtime/connectors/base/failure-taxonomy.js +5 -0
  23. package/runtime/connectors/base/manifest-v7.d.ts +151 -0
  24. package/runtime/connectors/base/manifest-v7.js +170 -0
  25. package/runtime/connectors/base/manifest.d.ts +67 -77
  26. package/runtime/connectors/base/manifest.js +7 -7
  27. package/runtime/connectors/base/route-planner.js +11 -8
  28. package/runtime/connectors/base/structured-unavailable-reason.d.ts +59 -0
  29. package/runtime/connectors/base/structured-unavailable-reason.js +113 -0
  30. package/runtime/connectors/base/wet-probe-runner.d.ts +40 -0
  31. package/runtime/connectors/base/wet-probe-runner.js +132 -0
  32. package/runtime/connectors/manifest/manifest-schema.d.ts +4 -0
  33. package/runtime/connectors/manifest/manifest-schema.js +2 -0
  34. package/runtime/connectors/services/connector-executor-adapter.d.ts +1 -0
  35. package/runtime/connectors/services/connector-executor-adapter.js +132 -26
  36. package/runtime/core/second-nature/body/behavior-promotion/behavior-promotion-loop.d.ts +45 -0
  37. package/runtime/core/second-nature/body/behavior-promotion/behavior-promotion-loop.js +132 -0
  38. package/runtime/core/second-nature/body/circuit-breaker/circuit-breaker-manager.d.ts +60 -0
  39. package/runtime/core/second-nature/body/circuit-breaker/circuit-breaker-manager.js +174 -0
  40. package/runtime/core/second-nature/body/probe-signal-adapter.d.ts +38 -0
  41. package/runtime/core/second-nature/body/probe-signal-adapter.js +60 -0
  42. package/runtime/core/second-nature/body/tool-affordance/affordance-assembler.d.ts +51 -0
  43. package/runtime/core/second-nature/body/tool-affordance/affordance-assembler.js +129 -0
  44. package/runtime/core/second-nature/body/tool-affordance/affordance-context-scope.d.ts +30 -0
  45. package/runtime/core/second-nature/body/tool-affordance/affordance-context-scope.js +92 -0
  46. package/runtime/core/second-nature/body/tool-experience/experience-writer.d.ts +34 -0
  47. package/runtime/core/second-nature/body/tool-experience/experience-writer.js +67 -0
  48. package/runtime/core/second-nature/body/tool-experience/pain-signal-query.d.ts +37 -0
  49. package/runtime/core/second-nature/body/tool-experience/pain-signal-query.js +62 -0
  50. package/runtime/core/second-nature/heartbeat/decision-trace-emitter.d.ts +29 -0
  51. package/runtime/core/second-nature/heartbeat/decision-trace-emitter.js +28 -0
  52. package/runtime/core/second-nature/heartbeat/embodied-context-assembler.d.ts +54 -0
  53. package/runtime/core/second-nature/heartbeat/embodied-context-assembler.js +164 -0
  54. package/runtime/core/second-nature/heartbeat/goal-lifecycle-policy.d.ts +37 -0
  55. package/runtime/core/second-nature/heartbeat/goal-lifecycle-policy.js +61 -0
  56. package/runtime/core/second-nature/heartbeat/idle-curiosity-policy.d.ts +37 -0
  57. package/runtime/core/second-nature/heartbeat/idle-curiosity-policy.js +60 -0
  58. package/runtime/core/second-nature/heartbeat/index.d.ts +4 -0
  59. package/runtime/core/second-nature/heartbeat/index.js +5 -0
  60. package/runtime/core/second-nature/heartbeat/run-heartbeat-cycle-v7.d.ts +63 -0
  61. package/runtime/core/second-nature/heartbeat/run-heartbeat-cycle-v7.js +118 -0
  62. package/runtime/core/second-nature/orchestrator/downstream-intent-orchestrator.d.ts +41 -0
  63. package/runtime/core/second-nature/orchestrator/downstream-intent-orchestrator.js +43 -0
  64. package/runtime/core/second-nature/orchestrator/effect-dispatcher.d.ts +2 -1
  65. package/runtime/core/second-nature/orchestrator/effect-dispatcher.js +2 -0
  66. package/runtime/core/second-nature/orchestrator/hard-guard-evaluator.d.ts +31 -0
  67. package/runtime/core/second-nature/orchestrator/hard-guard-evaluator.js +102 -0
  68. package/runtime/core/second-nature/orchestrator/index.d.ts +5 -0
  69. package/runtime/core/second-nature/orchestrator/index.js +7 -0
  70. package/runtime/core/second-nature/quiet/claim-synthesizer.d.ts +53 -0
  71. package/runtime/core/second-nature/quiet/claim-synthesizer.js +153 -0
  72. package/runtime/core/second-nature/quiet/daily-diary-writer.d.ts +29 -0
  73. package/runtime/core/second-nature/quiet/daily-diary-writer.js +92 -0
  74. package/runtime/core/second-nature/quiet/index.d.ts +5 -0
  75. package/runtime/core/second-nature/quiet/index.js +5 -0
  76. package/runtime/core/second-nature/quiet/run-source-backed-quiet.js +19 -12
  77. package/runtime/core/second-nature/types.d.ts +2 -0
  78. package/runtime/guidance/channel-feedback-ingestion-service.d.ts +88 -0
  79. package/runtime/guidance/channel-feedback-ingestion-service.js +231 -0
  80. package/runtime/guidance/guidance-draft-service.d.ts +60 -0
  81. package/runtime/guidance/guidance-draft-service.js +80 -0
  82. package/runtime/guidance/index.d.ts +3 -0
  83. package/runtime/guidance/index.js +3 -0
  84. package/runtime/guidance/outreach-draft-schema.d.ts +8 -8
  85. package/runtime/guidance/outreach-strategy-selector.d.ts +77 -0
  86. package/runtime/guidance/outreach-strategy-selector.js +211 -0
  87. package/runtime/observability/audit/append-only-audit-store.d.ts +20 -2
  88. package/runtime/observability/audit/append-only-audit-store.js +32 -6
  89. package/runtime/observability/audit/audit-envelope.d.ts +2 -1
  90. package/runtime/observability/audit/audit-envelope.js +8 -7
  91. package/runtime/observability/audit/audit-family-registry.json +66 -0
  92. package/runtime/observability/audit/family-registry.d.ts +43 -0
  93. package/runtime/observability/audit/family-registry.js +70 -0
  94. package/runtime/observability/index.d.ts +6 -1
  95. package/runtime/observability/index.js +6 -1
  96. package/runtime/observability/redaction/policy.d.ts +24 -3
  97. package/runtime/observability/redaction/policy.js +74 -0
  98. package/runtime/observability/services/heartbeat-digest-assembler.d.ts +152 -0
  99. package/runtime/observability/services/heartbeat-digest-assembler.js +248 -0
  100. package/runtime/observability/services/lived-experience-audit.js +6 -6
  101. package/runtime/observability/services/narrative-timeline-query-service.d.ts +136 -0
  102. package/runtime/observability/services/narrative-timeline-query-service.js +169 -0
  103. package/runtime/observability/services/restore-audit-service.d.ts +74 -0
  104. package/runtime/observability/services/restore-audit-service.js +79 -0
  105. package/runtime/observability/services/runtime-secret-anchor-view.d.ts +77 -0
  106. package/runtime/observability/services/runtime-secret-anchor-view.js +168 -0
  107. package/runtime/observability/services/self-health-snapshot.d.ts +92 -0
  108. package/runtime/observability/services/self-health-snapshot.js +251 -0
  109. package/runtime/shared/types/goal.d.ts +62 -0
  110. package/runtime/shared/types/goal.js +20 -0
  111. package/runtime/shared/types/index.d.ts +3 -0
  112. package/runtime/shared/types/index.js +3 -0
  113. package/runtime/shared/types/source-ref.d.ts +14 -0
  114. package/runtime/shared/types/source-ref.js +1 -0
  115. package/runtime/shared/types/v7-entities.d.ts +206 -0
  116. package/runtime/shared/types/v7-entities.js +27 -0
  117. package/runtime/storage/db/index.js +3 -0
  118. package/runtime/storage/db/migration-runner.d.ts +30 -0
  119. package/runtime/storage/db/migration-runner.js +93 -0
  120. package/runtime/storage/db/migrations/index.d.ts +5 -0
  121. package/runtime/storage/db/migrations/index.js +13 -0
  122. package/runtime/storage/db/migrations/v7-001-foundation.d.ts +13 -0
  123. package/runtime/storage/db/migrations/v7-001-foundation.js +144 -0
  124. package/runtime/storage/db/migrations/v7-002-effect-commit-ledger.d.ts +8 -0
  125. package/runtime/storage/db/migrations/v7-002-effect-commit-ledger.js +27 -0
  126. package/runtime/storage/db/migrations/v7-003-circuit-breaker.d.ts +7 -0
  127. package/runtime/storage/db/migrations/v7-003-circuit-breaker.js +26 -0
  128. package/runtime/storage/db/migrations/v7-004-behavior-promotion.d.ts +7 -0
  129. package/runtime/storage/db/migrations/v7-004-behavior-promotion.js +26 -0
  130. package/runtime/storage/db/schema/agent-goal.d.ts +38 -0
  131. package/runtime/storage/db/schema/agent-goal.js +2 -0
  132. package/runtime/storage/db/transaction-utils.d.ts +14 -0
  133. package/runtime/storage/db/transaction-utils.js +29 -0
  134. package/runtime/storage/db/write-queue.d.ts +38 -0
  135. package/runtime/storage/db/write-queue.js +97 -0
  136. package/runtime/storage/quiet/persist-quiet-artifact.js +2 -1
  137. package/runtime/storage/services/diary-dream-store.d.ts +35 -0
  138. package/runtime/storage/services/diary-dream-store.js +165 -0
  139. package/runtime/storage/services/embodied-context-state-port.d.ts +77 -0
  140. package/runtime/storage/services/embodied-context-state-port.js +115 -0
  141. package/runtime/storage/services/goal-lifecycle-store.d.ts +42 -0
  142. package/runtime/storage/services/goal-lifecycle-store.js +181 -0
  143. package/runtime/storage/services/history-digest-store.d.ts +33 -0
  144. package/runtime/storage/services/history-digest-store.js +140 -0
  145. package/runtime/storage/services/identity-profile-store.d.ts +25 -0
  146. package/runtime/storage/services/identity-profile-store.js +81 -0
  147. package/runtime/storage/services/interaction-snapshot-projector.d.ts +15 -0
  148. package/runtime/storage/services/interaction-snapshot-projector.js +35 -0
  149. package/runtime/storage/services/restore-snapshot-store.d.ts +52 -0
  150. package/runtime/storage/services/restore-snapshot-store.js +193 -0
  151. package/runtime/storage/services/runtime-secret-anchor-store.d.ts +26 -0
  152. package/runtime/storage/services/runtime-secret-anchor-store.js +82 -0
  153. package/runtime/storage/services/tool-experience-store.d.ts +25 -0
  154. package/runtime/storage/services/tool-experience-store.js +116 -0
  155. package/runtime/storage/services/write-validation-gate.d.ts +46 -0
  156. package/runtime/storage/services/write-validation-gate.js +200 -0
  157. package/workspace-ops-bridge.js +16 -1
@@ -10,6 +10,8 @@ export const REDACTION_CONFIG = {
10
10
  "bearer_token",
11
11
  "authorization",
12
12
  "node_secret",
13
+ "encryption_key", // v7
14
+ "key_material", // v7
13
15
  ],
14
16
  eraseFieldNames: [
15
17
  "full_message",
@@ -19,12 +21,16 @@ export const REDACTION_CONFIG = {
19
21
  "system_prompt",
20
22
  "completion",
21
23
  "response_content",
24
+ "raw_payload", // v7
25
+ "credential_value", // v7
26
+ "raw_prompt", // v7
22
27
  ],
23
28
  hashFieldNames: [
24
29
  "user_id",
25
30
  "session_id",
26
31
  "trace_id",
27
32
  "content_hash",
33
+ "message_hash", // v7
28
34
  ],
29
35
  sensitivityLevels: ["public", "internal", "confidential", "restricted"],
30
36
  };
@@ -40,6 +46,8 @@ export const DEFAULT_REDACTION_POLICY = {
40
46
  { fieldName: "bearer_token", action: "mask" },
41
47
  { fieldName: "authorization", action: "mask" },
42
48
  { fieldName: "node_secret", action: "mask" },
49
+ { fieldName: "encryption_key", action: "mask" },
50
+ { fieldName: "key_material", action: "mask" },
43
51
  { fieldName: "full_message", action: "erase" },
44
52
  { fieldName: "full_post", action: "erase" },
45
53
  { fieldName: "private_message", action: "erase" },
@@ -47,7 +55,11 @@ export const DEFAULT_REDACTION_POLICY = {
47
55
  { fieldName: "system_prompt", action: "erase" },
48
56
  { fieldName: "completion", action: "erase" },
49
57
  { fieldName: "response_content", action: "erase" },
58
+ { fieldName: "raw_payload", action: "erase" },
59
+ { fieldName: "credential_value", action: "erase" },
60
+ { fieldName: "raw_prompt", action: "erase" },
50
61
  { fieldName: "content_hash", action: "hash" },
62
+ { fieldName: "message_hash", action: "hash" },
51
63
  ],
52
64
  fieldOverrides: {},
53
65
  maxFieldLength: 500,
@@ -69,3 +81,65 @@ export function getFieldRedactionRule(fieldName, policy = DEFAULT_REDACTION_POLI
69
81
  }
70
82
  return { fieldName, action: "keep" };
71
83
  }
84
+ // ─── Unified Redaction Gate (T-OBS.C.1 / DR-033) ────────────────────────────
85
+ import * as crypto from "node:crypto";
86
+ /**
87
+ * Unified redaction gate — all audit-bound payloads must pass through this
88
+ * before persistence. Recursively applies mask/erase/hash rules from the
89
+ * active RedactionPolicy, preserving object shape (erase → null, not delete).
90
+ *
91
+ * Boundary:
92
+ * - Arrays are not recursed (avoid unbounded complexity).
93
+ * - erase fields become null so downstream JSON schema stays stable.
94
+ * - hash uses SHA-256 of the stringified original value.
95
+ */
96
+ export function redactPayload(payload, policy = DEFAULT_REDACTION_POLICY) {
97
+ const maskedPaths = [];
98
+ const erasedPaths = [];
99
+ const hashedPaths = [];
100
+ function processValue(obj, path) {
101
+ const result = {};
102
+ for (const [key, value] of Object.entries(obj)) {
103
+ const fullPath = path ? `${path}.${key}` : key;
104
+ const rule = getFieldRedactionRule(key, policy);
105
+ if (rule.action === "mask") {
106
+ result[key] = "[MASKED]";
107
+ maskedPaths.push(fullPath);
108
+ }
109
+ else if (rule.action === "erase") {
110
+ result[key] = null;
111
+ erasedPaths.push(fullPath);
112
+ }
113
+ else if (rule.action === "hash") {
114
+ result[key] = typeof value === "string"
115
+ ? crypto.createHash("sha256").update(value).digest("hex")
116
+ : value;
117
+ hashedPaths.push(fullPath);
118
+ }
119
+ else if (value !== null && typeof value === "object" && !Array.isArray(value)) {
120
+ result[key] = processValue(value, fullPath);
121
+ }
122
+ else {
123
+ result[key] = value;
124
+ }
125
+ }
126
+ return result;
127
+ }
128
+ const redactedPayload = processValue(payload, "");
129
+ return {
130
+ payload: redactedPayload,
131
+ manifest: {
132
+ maskedPaths,
133
+ erasedPaths,
134
+ hashedPaths,
135
+ sensitivity: inferSensitivity(maskedPaths, erasedPaths),
136
+ },
137
+ };
138
+ }
139
+ function inferSensitivity(masked, erased) {
140
+ if (erased.length > 0)
141
+ return "restricted";
142
+ if (masked.length > 0)
143
+ return "confidential";
144
+ return "internal";
145
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * HeartbeatDigestAssembler — T-OBS.C.3 / T-OBS.C.4
3
+ *
4
+ * Core logic: aggregate one day's audit events from AppendOnlyAuditStore
5
+ * into a dashboard-style HeartbeatDigest (connector counts / goal changes /
6
+ * quiet-dream status / health summary). No outreach phrasing. No raw payload.
7
+ * No credential content. If no significant events, isNothingSignificant = true.
8
+ *
9
+ * T-OBS.C.4 delivery hook:
10
+ * An optional DigestDeliveryAdapter can be injected via deps.deliveryAdapter.
11
+ * After digest assembly, generateHeartbeatDigest calls adapter.deliver(digest).
12
+ * On success: digest.deliveredAt and digest.deliveryProof are populated.
13
+ * On failure: digest.deliveryFallbackReason is set; deliveredAt is NOT set.
14
+ * Honesty constraint: not_sent is never reported as sent (ADR-007).
15
+ *
16
+ * DR-032 degradation:
17
+ * If state-memory port is unavailable, goalSummary + quietDreamSummary
18
+ * return degraded = true with reason. Other sections (connector / health) unaffected.
19
+ *
20
+ * Boundary:
21
+ * - Reads AppendOnlyAuditStore.list() (in-memory) for connector.attempt + heartbeat.decision
22
+ * + dream.trace + delivery audit events.
23
+ * - Reads optional StateMemoryDigestPort for goal transitions + quiet/dream scheduling state.
24
+ * - Does NOT write to state-memory (persistence is runtime-ops' responsibility).
25
+ * - Does NOT use outreach language (NG2 from PRD: not a "reach out to you" message).
26
+ * - Does NOT push digest itself; delivery is triggered by runtime-ops (NG5 from L0).
27
+ * The adapter here is an injected hook used during assembly, not an autonomous push.
28
+ *
29
+ * Test coverage:
30
+ * tests/unit/observability/heartbeat-digest-assembler.test.ts (T-OBS.C.3)
31
+ * tests/integration/observability/digest-delivery.test.ts (T-OBS.C.4)
32
+ */
33
+ import type { AppendOnlyAuditStore } from "../audit/append-only-audit-store.js";
34
+ export interface ConnectorDaySummary {
35
+ platformId: string;
36
+ capability: string;
37
+ successCount: number;
38
+ failureCount: number;
39
+ circuitOpenCount: number;
40
+ blockedCount: number;
41
+ }
42
+ export interface GoalDaySummary {
43
+ newGoals: number;
44
+ completedGoals: number;
45
+ expiredGoals: number;
46
+ replacedGoals: number;
47
+ activeGoals: number;
48
+ degraded?: boolean;
49
+ degradedReason?: string;
50
+ }
51
+ export interface QuietDreamDaySummary {
52
+ quietRuns: number;
53
+ quietSucceeded: number;
54
+ dreamRuns: number;
55
+ dreamAccepted: number;
56
+ dreamSkipped: number;
57
+ dreamSkipReasons: string[];
58
+ degraded?: boolean;
59
+ degradedReason?: string;
60
+ }
61
+ export interface HealthDaySummary {
62
+ circuitBreakerChanges: number;
63
+ deliverySuccessCount: number;
64
+ deliveryFailureCount: number;
65
+ auditChainHealthy: boolean;
66
+ }
67
+ export interface DeliveryProofRef {
68
+ channelId: string;
69
+ messageHash: string;
70
+ }
71
+ export interface HeartbeatDigest {
72
+ date: string;
73
+ generatedAt: string;
74
+ isNothingSignificant: boolean;
75
+ connectorSummary: ConnectorDaySummary[];
76
+ goalSummary: GoalDaySummary;
77
+ quietDreamSummary: QuietDreamDaySummary;
78
+ healthSummary: HealthDaySummary;
79
+ /** Set when delivery succeeded */
80
+ deliveredAt?: string;
81
+ /** Proof of successful delivery (channel + message hash, no raw content) */
82
+ deliveryProof?: DeliveryProofRef;
83
+ /** Set when delivery failed; status is always "not_sent" in this case */
84
+ deliveryFallbackReason?: string;
85
+ }
86
+ /** Result from a delivery attempt */
87
+ export interface DigestDeliveryResult {
88
+ /**
89
+ * "sent" — delivery succeeded; proof is populated.
90
+ * "not_sent" — delivery failed or was skipped; fallbackReason is populated.
91
+ */
92
+ status: "sent" | "not_sent";
93
+ proof?: DeliveryProofRef;
94
+ /** Human-readable reason why delivery was not sent */
95
+ fallbackReason?: string;
96
+ deliveredAt?: string;
97
+ }
98
+ /**
99
+ * Adapter injected by runtime-ops to perform channel-specific delivery.
100
+ * The adapter is responsible for the actual push (Feishu DM / dashboard / etc.).
101
+ * It must never declare "sent" without a verifiable proof.
102
+ */
103
+ export interface DigestDeliveryAdapter {
104
+ deliver(digest: HeartbeatDigest): Promise<DigestDeliveryResult>;
105
+ }
106
+ /** Port for reading goal and quiet/dream scheduling state from state-memory. */
107
+ export interface StateMemoryDigestPort {
108
+ queryGoalTransitions(date: string): Promise<{
109
+ newGoals: number;
110
+ completedGoals: number;
111
+ expiredGoals: number;
112
+ replacedGoals: number;
113
+ activeGoals: number;
114
+ }>;
115
+ queryQuietDreamStatus(date: string): Promise<{
116
+ quietRuns: number;
117
+ quietSucceeded: number;
118
+ dreamRuns: number;
119
+ dreamAccepted: number;
120
+ dreamSkipped: number;
121
+ dreamSkipReasons: string[];
122
+ }>;
123
+ }
124
+ export interface HeartbeatDigestAssemblerDeps {
125
+ auditStore: AppendOnlyAuditStore;
126
+ stateMemoryPort?: StateMemoryDigestPort;
127
+ /**
128
+ * Optional delivery adapter (T-OBS.C.4).
129
+ * When provided, the assembled digest is passed to adapter.deliver() after assembly.
130
+ * Delivery result (proof / fallback) is merged back into the returned digest.
131
+ * Delivery failure does NOT throw — the assembled digest is still returned,
132
+ * with deliveryFallbackReason set.
133
+ */
134
+ deliveryAdapter?: DigestDeliveryAdapter;
135
+ /** Override for testability */
136
+ now?: () => string;
137
+ }
138
+ /**
139
+ * Generate a HeartbeatDigest for the given date (YYYY-MM-DD).
140
+ *
141
+ * Aggregates connector attempts, heartbeat decisions, dream traces, and delivery
142
+ * audit events from the in-memory audit store. Goal transitions and quiet/dream
143
+ * scheduling state are loaded from state-memory via the optional port (DR-032
144
+ * degradation applied if unavailable).
145
+ *
146
+ * If deps.deliveryAdapter is provided (T-OBS.C.4), the assembled digest is
147
+ * passed to the adapter after assembly. Delivery proof or fallback reason is
148
+ * merged into the returned digest. Delivery failure never causes a throw.
149
+ *
150
+ * Does NOT contain outreach language, raw payloads, credentials, or private content.
151
+ */
152
+ export declare function generateHeartbeatDigest(date: string, deps: HeartbeatDigestAssemblerDeps): Promise<HeartbeatDigest>;
@@ -0,0 +1,248 @@
1
+ /**
2
+ * HeartbeatDigestAssembler — T-OBS.C.3 / T-OBS.C.4
3
+ *
4
+ * Core logic: aggregate one day's audit events from AppendOnlyAuditStore
5
+ * into a dashboard-style HeartbeatDigest (connector counts / goal changes /
6
+ * quiet-dream status / health summary). No outreach phrasing. No raw payload.
7
+ * No credential content. If no significant events, isNothingSignificant = true.
8
+ *
9
+ * T-OBS.C.4 delivery hook:
10
+ * An optional DigestDeliveryAdapter can be injected via deps.deliveryAdapter.
11
+ * After digest assembly, generateHeartbeatDigest calls adapter.deliver(digest).
12
+ * On success: digest.deliveredAt and digest.deliveryProof are populated.
13
+ * On failure: digest.deliveryFallbackReason is set; deliveredAt is NOT set.
14
+ * Honesty constraint: not_sent is never reported as sent (ADR-007).
15
+ *
16
+ * DR-032 degradation:
17
+ * If state-memory port is unavailable, goalSummary + quietDreamSummary
18
+ * return degraded = true with reason. Other sections (connector / health) unaffected.
19
+ *
20
+ * Boundary:
21
+ * - Reads AppendOnlyAuditStore.list() (in-memory) for connector.attempt + heartbeat.decision
22
+ * + dream.trace + delivery audit events.
23
+ * - Reads optional StateMemoryDigestPort for goal transitions + quiet/dream scheduling state.
24
+ * - Does NOT write to state-memory (persistence is runtime-ops' responsibility).
25
+ * - Does NOT use outreach language (NG2 from PRD: not a "reach out to you" message).
26
+ * - Does NOT push digest itself; delivery is triggered by runtime-ops (NG5 from L0).
27
+ * The adapter here is an injected hook used during assembly, not an autonomous push.
28
+ *
29
+ * Test coverage:
30
+ * tests/unit/observability/heartbeat-digest-assembler.test.ts (T-OBS.C.3)
31
+ * tests/integration/observability/digest-delivery.test.ts (T-OBS.C.4)
32
+ */
33
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
34
+ function isSameDayUtc(isoTimestamp, dateStr) {
35
+ // dateStr: "YYYY-MM-DD"
36
+ return isoTimestamp.startsWith(dateStr);
37
+ }
38
+ function filterByDate(events, family, dateStr) {
39
+ return events.filter((e) => e.family === family && isSameDayUtc(e.createdAt, dateStr));
40
+ }
41
+ // ─── Aggregation ──────────────────────────────────────────────────────────────
42
+ function aggregateConnectors(events, dateStr) {
43
+ const connectorEvents = filterByDate(events, "connector.attempt", dateStr);
44
+ const byKey = new Map();
45
+ for (const ev of connectorEvents) {
46
+ const payload = ev.payload;
47
+ const platformId = payload.platformId ?? "unknown";
48
+ const capability = payload.capability ?? "unknown";
49
+ const key = `${platformId}::${capability}`;
50
+ if (!byKey.has(key)) {
51
+ byKey.set(key, {
52
+ platformId,
53
+ capability,
54
+ successCount: 0,
55
+ failureCount: 0,
56
+ circuitOpenCount: 0,
57
+ blockedCount: 0,
58
+ });
59
+ }
60
+ const entry = byKey.get(key);
61
+ switch (payload.outcome) {
62
+ case "success":
63
+ entry.successCount++;
64
+ break;
65
+ case "failure":
66
+ entry.failureCount++;
67
+ break;
68
+ case "circuit_open":
69
+ entry.circuitOpenCount++;
70
+ break;
71
+ case "blocked":
72
+ entry.blockedCount++;
73
+ break;
74
+ default:
75
+ // Unknown outcome — still count as an attempt (no-op for summaries)
76
+ break;
77
+ }
78
+ }
79
+ return Array.from(byKey.values());
80
+ }
81
+ function aggregateHealthSummary(events, dateStr) {
82
+ const deliveryEvents = filterByDate(events, "delivery", dateStr);
83
+ const heartbeatEvents = filterByDate(events, "heartbeat.decision", dateStr);
84
+ const deliverySuccessCount = deliveryEvents.filter((e) => e.payload.outcome === "sent").length;
85
+ const deliveryFailureCount = deliveryEvents.filter((e) => e.payload.outcome === "failed" ||
86
+ e.payload.outcome === "not_sent").length;
87
+ // Count circuit-breaker change events in heartbeat decisions
88
+ const circuitBreakerChanges = heartbeatEvents.filter((e) => e.payload.outcome === "deferred").length;
89
+ // Audit chain is healthy if we have consecutive events (simplified: always true here)
90
+ const auditChainHealthy = true;
91
+ return {
92
+ circuitBreakerChanges,
93
+ deliverySuccessCount,
94
+ deliveryFailureCount,
95
+ auditChainHealthy,
96
+ };
97
+ }
98
+ function aggregateQuietDreamFromAudit(events, dateStr) {
99
+ const dreamEvents = filterByDate(events, "dream.trace", dateStr);
100
+ let dreamRuns = 0;
101
+ let dreamAccepted = 0;
102
+ let dreamSkipped = 0;
103
+ const dreamSkipReasons = [];
104
+ for (const ev of dreamEvents) {
105
+ const payload = ev.payload;
106
+ if (payload.event === "dream_started")
107
+ dreamRuns++;
108
+ if (payload.event === "dream_accepted")
109
+ dreamAccepted++;
110
+ if (payload.event === "dream_skipped") {
111
+ dreamSkipped++;
112
+ if (payload.skipReason)
113
+ dreamSkipReasons.push(payload.skipReason);
114
+ }
115
+ }
116
+ // Quiet stats not yet in audit (narrative.trace not fully wired) — default to 0
117
+ return {
118
+ quietRuns: 0,
119
+ quietSucceeded: 0,
120
+ dreamRuns,
121
+ dreamAccepted,
122
+ dreamSkipped,
123
+ dreamSkipReasons: [...new Set(dreamSkipReasons)],
124
+ };
125
+ }
126
+ function isNothingSignificant(connectorSummary, goalSummary, quietDreamSummary, healthSummary) {
127
+ const hasConnectorActivity = connectorSummary.some((c) => c.successCount + c.failureCount + c.circuitOpenCount + c.blockedCount > 0);
128
+ const hasGoalActivity = !goalSummary.degraded &&
129
+ (goalSummary.newGoals +
130
+ goalSummary.completedGoals +
131
+ goalSummary.expiredGoals +
132
+ goalSummary.replacedGoals >
133
+ 0);
134
+ const hasDreamActivity = !quietDreamSummary.degraded &&
135
+ (quietDreamSummary.dreamRuns > 0 || quietDreamSummary.quietRuns > 0);
136
+ const hasDeliveryActivity = healthSummary.deliverySuccessCount + healthSummary.deliveryFailureCount > 0;
137
+ return (!hasConnectorActivity &&
138
+ !hasGoalActivity &&
139
+ !hasDreamActivity &&
140
+ !hasDeliveryActivity);
141
+ }
142
+ /**
143
+ * Generate a HeartbeatDigest for the given date (YYYY-MM-DD).
144
+ *
145
+ * Aggregates connector attempts, heartbeat decisions, dream traces, and delivery
146
+ * audit events from the in-memory audit store. Goal transitions and quiet/dream
147
+ * scheduling state are loaded from state-memory via the optional port (DR-032
148
+ * degradation applied if unavailable).
149
+ *
150
+ * If deps.deliveryAdapter is provided (T-OBS.C.4), the assembled digest is
151
+ * passed to the adapter after assembly. Delivery proof or fallback reason is
152
+ * merged into the returned digest. Delivery failure never causes a throw.
153
+ *
154
+ * Does NOT contain outreach language, raw payloads, credentials, or private content.
155
+ */
156
+ export async function generateHeartbeatDigest(date, deps) {
157
+ const generatedAt = (deps.now ?? (() => new Date().toISOString()))();
158
+ const { auditStore, stateMemoryPort, deliveryAdapter } = deps;
159
+ const events = auditStore.list();
160
+ // Aggregate connector and health from audit
161
+ const connectorSummary = aggregateConnectors(events, date);
162
+ const healthSummary = aggregateHealthSummary(events, date);
163
+ // Goal transitions — via state-memory port (DR-032 degradation)
164
+ let goalSummary;
165
+ if (stateMemoryPort) {
166
+ try {
167
+ const g = await stateMemoryPort.queryGoalTransitions(date);
168
+ goalSummary = { ...g };
169
+ }
170
+ catch {
171
+ goalSummary = {
172
+ newGoals: 0,
173
+ completedGoals: 0,
174
+ expiredGoals: 0,
175
+ replacedGoals: 0,
176
+ activeGoals: 0,
177
+ degraded: true,
178
+ degradedReason: "state_memory_unavailable",
179
+ };
180
+ }
181
+ }
182
+ else {
183
+ goalSummary = {
184
+ newGoals: 0,
185
+ completedGoals: 0,
186
+ expiredGoals: 0,
187
+ replacedGoals: 0,
188
+ activeGoals: 0,
189
+ };
190
+ }
191
+ // Quiet/Dream status — prefer state-memory port; fallback to audit-based aggregation
192
+ let quietDreamSummary;
193
+ if (stateMemoryPort) {
194
+ try {
195
+ const qd = await stateMemoryPort.queryQuietDreamStatus(date);
196
+ quietDreamSummary = { ...qd };
197
+ }
198
+ catch {
199
+ // DR-032: degrade gracefully
200
+ const fromAudit = aggregateQuietDreamFromAudit(events, date);
201
+ quietDreamSummary = {
202
+ ...fromAudit,
203
+ degraded: true,
204
+ degradedReason: "state_memory_unavailable",
205
+ };
206
+ }
207
+ }
208
+ else {
209
+ quietDreamSummary = aggregateQuietDreamFromAudit(events, date);
210
+ }
211
+ const nothingSignificant = isNothingSignificant(connectorSummary, goalSummary, quietDreamSummary, healthSummary);
212
+ const digest = {
213
+ date,
214
+ generatedAt,
215
+ isNothingSignificant: nothingSignificant,
216
+ connectorSummary,
217
+ goalSummary,
218
+ quietDreamSummary,
219
+ healthSummary,
220
+ };
221
+ // T-OBS.C.4: delivery hook — attempt delivery if adapter is provided
222
+ if (deliveryAdapter) {
223
+ try {
224
+ const result = await deliveryAdapter.deliver(digest);
225
+ if (result.status === "sent") {
226
+ // Proof must be present when claiming "sent"
227
+ if (result.proof) {
228
+ digest.deliveredAt = result.deliveredAt ?? generatedAt;
229
+ digest.deliveryProof = result.proof;
230
+ }
231
+ else {
232
+ // Adapter declared "sent" without proof — treat as not_sent (honesty constraint)
233
+ digest.deliveryFallbackReason = "delivery_proof_missing";
234
+ }
235
+ }
236
+ else {
237
+ // status === "not_sent": record fallback reason, never claim sent
238
+ digest.deliveryFallbackReason = result.fallbackReason ?? "delivery_failed";
239
+ }
240
+ }
241
+ catch (err) {
242
+ // Delivery threw — absorb error, record fallback, do not rethrow
243
+ const message = err instanceof Error ? err.message : String(err);
244
+ digest.deliveryFallbackReason = `delivery_error: ${message}`;
245
+ }
246
+ }
247
+ return digest;
248
+ }
@@ -57,7 +57,7 @@ export class LivedExperienceAuditRecorder {
57
57
  traceId: payload.traceId,
58
58
  sequence: seq,
59
59
  payload,
60
- previousHash: this.store.lastRecordHash(),
60
+ previousHash: this.store.lastRecordHash("heartbeat.decision"),
61
61
  eventId: crypto.randomUUID(),
62
62
  createdAt: payload.createdAt,
63
63
  });
@@ -78,7 +78,7 @@ export class LivedExperienceAuditRecorder {
78
78
  traceId: payload.traceId,
79
79
  sequence: seq,
80
80
  payload,
81
- previousHash: this.store.lastRecordHash(),
81
+ previousHash: this.store.lastRecordHash("delivery"),
82
82
  eventId: payload.auditId,
83
83
  createdAt: payload.createdAt,
84
84
  });
@@ -105,7 +105,7 @@ export class LivedExperienceAuditRecorder {
105
105
  traceId: payload.traceId,
106
106
  sequence: seq,
107
107
  payload,
108
- previousHash: this.store.lastRecordHash(),
108
+ previousHash: this.store.lastRecordHash("source_coverage"),
109
109
  eventId: payload.auditId,
110
110
  createdAt: payload.createdAt,
111
111
  });
@@ -123,7 +123,7 @@ export class LivedExperienceAuditRecorder {
123
123
  traceId: payload.traceId,
124
124
  sequence: seq,
125
125
  payload,
126
- previousHash: this.store.lastRecordHash(),
126
+ previousHash: this.store.lastRecordHash("guidance.grounding"),
127
127
  eventId: payload.auditId,
128
128
  createdAt: payload.createdAt,
129
129
  });
@@ -141,7 +141,7 @@ export class LivedExperienceAuditRecorder {
141
141
  traceId: payload.traceId,
142
142
  sequence: seq,
143
143
  payload,
144
- previousHash: this.store.lastRecordHash(),
144
+ previousHash: this.store.lastRecordHash("narrative.trace"),
145
145
  eventId: crypto.randomUUID(),
146
146
  createdAt: payload.createdAt,
147
147
  });
@@ -156,7 +156,7 @@ export class LivedExperienceAuditRecorder {
156
156
  traceId: payload.traceId,
157
157
  sequence: seq,
158
158
  payload,
159
- previousHash: this.store.lastRecordHash(),
159
+ previousHash: this.store.lastRecordHash("dream.trace"),
160
160
  eventId: crypto.randomUUID(),
161
161
  createdAt: payload.finishedAt,
162
162
  });