@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.
- package/SKILL.md +35 -33
- package/agent-inner-guide.md +144 -124
- package/index.js +76 -1
- package/openclaw.plugin.json +2 -2
- package/package.json +2 -1
- package/runtime/cli/commands/connector-behavior.d.ts +20 -0
- package/runtime/cli/commands/connector-behavior.js +160 -0
- package/runtime/cli/commands/index.js +8 -0
- package/runtime/cli/index.js +9 -2
- package/runtime/cli/ops/manual-run-dispatcher.d.ts +79 -0
- package/runtime/cli/ops/manual-run-dispatcher.js +110 -0
- package/runtime/cli/ops/ops-router.d.ts +45 -4
- package/runtime/cli/ops/ops-router.js +543 -2
- package/runtime/cli/read-models/index.js +35 -18
- package/runtime/cli/read-models/types.d.ts +1 -0
- package/runtime/connectors/agent-network/agent-world/adapter.d.ts +1 -0
- package/runtime/connectors/agent-network/agent-world/adapter.js +2 -2
- package/runtime/connectors/base/contract.d.ts +4 -1
- package/runtime/connectors/base/contract.js +5 -1
- package/runtime/connectors/base/effect-commit-ledger-sqlite.d.ts +31 -0
- package/runtime/connectors/base/effect-commit-ledger-sqlite.js +86 -0
- package/runtime/connectors/base/failure-taxonomy.js +5 -0
- package/runtime/connectors/base/manifest-v7.d.ts +151 -0
- package/runtime/connectors/base/manifest-v7.js +170 -0
- package/runtime/connectors/base/manifest.d.ts +67 -77
- package/runtime/connectors/base/manifest.js +7 -7
- package/runtime/connectors/base/route-planner.js +11 -8
- package/runtime/connectors/base/structured-unavailable-reason.d.ts +59 -0
- package/runtime/connectors/base/structured-unavailable-reason.js +113 -0
- package/runtime/connectors/base/wet-probe-runner.d.ts +40 -0
- package/runtime/connectors/base/wet-probe-runner.js +132 -0
- package/runtime/connectors/manifest/manifest-schema.d.ts +4 -0
- package/runtime/connectors/manifest/manifest-schema.js +2 -0
- package/runtime/connectors/services/connector-executor-adapter.d.ts +1 -0
- package/runtime/connectors/services/connector-executor-adapter.js +132 -26
- package/runtime/core/second-nature/body/behavior-promotion/behavior-promotion-loop.d.ts +45 -0
- package/runtime/core/second-nature/body/behavior-promotion/behavior-promotion-loop.js +132 -0
- package/runtime/core/second-nature/body/circuit-breaker/circuit-breaker-manager.d.ts +60 -0
- package/runtime/core/second-nature/body/circuit-breaker/circuit-breaker-manager.js +174 -0
- package/runtime/core/second-nature/body/probe-signal-adapter.d.ts +38 -0
- package/runtime/core/second-nature/body/probe-signal-adapter.js +60 -0
- package/runtime/core/second-nature/body/tool-affordance/affordance-assembler.d.ts +51 -0
- package/runtime/core/second-nature/body/tool-affordance/affordance-assembler.js +129 -0
- package/runtime/core/second-nature/body/tool-affordance/affordance-context-scope.d.ts +30 -0
- package/runtime/core/second-nature/body/tool-affordance/affordance-context-scope.js +92 -0
- package/runtime/core/second-nature/body/tool-experience/experience-writer.d.ts +34 -0
- package/runtime/core/second-nature/body/tool-experience/experience-writer.js +67 -0
- package/runtime/core/second-nature/body/tool-experience/pain-signal-query.d.ts +37 -0
- package/runtime/core/second-nature/body/tool-experience/pain-signal-query.js +62 -0
- package/runtime/core/second-nature/heartbeat/decision-trace-emitter.d.ts +29 -0
- package/runtime/core/second-nature/heartbeat/decision-trace-emitter.js +28 -0
- package/runtime/core/second-nature/heartbeat/embodied-context-assembler.d.ts +54 -0
- package/runtime/core/second-nature/heartbeat/embodied-context-assembler.js +164 -0
- package/runtime/core/second-nature/heartbeat/goal-lifecycle-policy.d.ts +37 -0
- package/runtime/core/second-nature/heartbeat/goal-lifecycle-policy.js +61 -0
- package/runtime/core/second-nature/heartbeat/idle-curiosity-policy.d.ts +37 -0
- package/runtime/core/second-nature/heartbeat/idle-curiosity-policy.js +60 -0
- package/runtime/core/second-nature/heartbeat/index.d.ts +4 -0
- package/runtime/core/second-nature/heartbeat/index.js +5 -0
- package/runtime/core/second-nature/heartbeat/run-heartbeat-cycle-v7.d.ts +63 -0
- package/runtime/core/second-nature/heartbeat/run-heartbeat-cycle-v7.js +118 -0
- package/runtime/core/second-nature/orchestrator/downstream-intent-orchestrator.d.ts +41 -0
- package/runtime/core/second-nature/orchestrator/downstream-intent-orchestrator.js +43 -0
- package/runtime/core/second-nature/orchestrator/effect-dispatcher.d.ts +2 -1
- package/runtime/core/second-nature/orchestrator/effect-dispatcher.js +2 -0
- package/runtime/core/second-nature/orchestrator/hard-guard-evaluator.d.ts +31 -0
- package/runtime/core/second-nature/orchestrator/hard-guard-evaluator.js +102 -0
- package/runtime/core/second-nature/orchestrator/index.d.ts +5 -0
- package/runtime/core/second-nature/orchestrator/index.js +7 -0
- package/runtime/core/second-nature/quiet/claim-synthesizer.d.ts +53 -0
- package/runtime/core/second-nature/quiet/claim-synthesizer.js +153 -0
- package/runtime/core/second-nature/quiet/daily-diary-writer.d.ts +29 -0
- package/runtime/core/second-nature/quiet/daily-diary-writer.js +92 -0
- package/runtime/core/second-nature/quiet/index.d.ts +5 -0
- package/runtime/core/second-nature/quiet/index.js +5 -0
- package/runtime/core/second-nature/quiet/run-source-backed-quiet.js +19 -12
- package/runtime/core/second-nature/types.d.ts +2 -0
- package/runtime/guidance/channel-feedback-ingestion-service.d.ts +88 -0
- package/runtime/guidance/channel-feedback-ingestion-service.js +231 -0
- package/runtime/guidance/guidance-draft-service.d.ts +60 -0
- package/runtime/guidance/guidance-draft-service.js +80 -0
- package/runtime/guidance/index.d.ts +3 -0
- package/runtime/guidance/index.js +3 -0
- package/runtime/guidance/outreach-draft-schema.d.ts +8 -8
- package/runtime/guidance/outreach-strategy-selector.d.ts +77 -0
- package/runtime/guidance/outreach-strategy-selector.js +211 -0
- package/runtime/observability/audit/append-only-audit-store.d.ts +20 -2
- package/runtime/observability/audit/append-only-audit-store.js +32 -6
- package/runtime/observability/audit/audit-envelope.d.ts +2 -1
- package/runtime/observability/audit/audit-envelope.js +8 -7
- package/runtime/observability/audit/audit-family-registry.json +66 -0
- package/runtime/observability/audit/family-registry.d.ts +43 -0
- package/runtime/observability/audit/family-registry.js +70 -0
- package/runtime/observability/index.d.ts +6 -1
- package/runtime/observability/index.js +6 -1
- package/runtime/observability/redaction/policy.d.ts +24 -3
- package/runtime/observability/redaction/policy.js +74 -0
- package/runtime/observability/services/heartbeat-digest-assembler.d.ts +152 -0
- package/runtime/observability/services/heartbeat-digest-assembler.js +248 -0
- package/runtime/observability/services/lived-experience-audit.js +6 -6
- package/runtime/observability/services/narrative-timeline-query-service.d.ts +136 -0
- package/runtime/observability/services/narrative-timeline-query-service.js +169 -0
- package/runtime/observability/services/restore-audit-service.d.ts +74 -0
- package/runtime/observability/services/restore-audit-service.js +79 -0
- package/runtime/observability/services/runtime-secret-anchor-view.d.ts +77 -0
- package/runtime/observability/services/runtime-secret-anchor-view.js +168 -0
- package/runtime/observability/services/self-health-snapshot.d.ts +92 -0
- package/runtime/observability/services/self-health-snapshot.js +251 -0
- package/runtime/shared/types/goal.d.ts +62 -0
- package/runtime/shared/types/goal.js +20 -0
- package/runtime/shared/types/index.d.ts +3 -0
- package/runtime/shared/types/index.js +3 -0
- package/runtime/shared/types/source-ref.d.ts +14 -0
- package/runtime/shared/types/source-ref.js +1 -0
- package/runtime/shared/types/v7-entities.d.ts +206 -0
- package/runtime/shared/types/v7-entities.js +27 -0
- package/runtime/storage/db/index.js +3 -0
- package/runtime/storage/db/migration-runner.d.ts +30 -0
- package/runtime/storage/db/migration-runner.js +93 -0
- package/runtime/storage/db/migrations/index.d.ts +5 -0
- package/runtime/storage/db/migrations/index.js +13 -0
- package/runtime/storage/db/migrations/v7-001-foundation.d.ts +13 -0
- package/runtime/storage/db/migrations/v7-001-foundation.js +144 -0
- package/runtime/storage/db/migrations/v7-002-effect-commit-ledger.d.ts +8 -0
- package/runtime/storage/db/migrations/v7-002-effect-commit-ledger.js +27 -0
- package/runtime/storage/db/migrations/v7-003-circuit-breaker.d.ts +7 -0
- package/runtime/storage/db/migrations/v7-003-circuit-breaker.js +26 -0
- package/runtime/storage/db/migrations/v7-004-behavior-promotion.d.ts +7 -0
- package/runtime/storage/db/migrations/v7-004-behavior-promotion.js +26 -0
- package/runtime/storage/db/schema/agent-goal.d.ts +38 -0
- package/runtime/storage/db/schema/agent-goal.js +2 -0
- package/runtime/storage/db/transaction-utils.d.ts +14 -0
- package/runtime/storage/db/transaction-utils.js +29 -0
- package/runtime/storage/db/write-queue.d.ts +38 -0
- package/runtime/storage/db/write-queue.js +97 -0
- package/runtime/storage/quiet/persist-quiet-artifact.js +2 -1
- package/runtime/storage/services/diary-dream-store.d.ts +35 -0
- package/runtime/storage/services/diary-dream-store.js +165 -0
- package/runtime/storage/services/embodied-context-state-port.d.ts +77 -0
- package/runtime/storage/services/embodied-context-state-port.js +115 -0
- package/runtime/storage/services/goal-lifecycle-store.d.ts +42 -0
- package/runtime/storage/services/goal-lifecycle-store.js +181 -0
- package/runtime/storage/services/history-digest-store.d.ts +33 -0
- package/runtime/storage/services/history-digest-store.js +140 -0
- package/runtime/storage/services/identity-profile-store.d.ts +25 -0
- package/runtime/storage/services/identity-profile-store.js +81 -0
- package/runtime/storage/services/interaction-snapshot-projector.d.ts +15 -0
- package/runtime/storage/services/interaction-snapshot-projector.js +35 -0
- package/runtime/storage/services/restore-snapshot-store.d.ts +52 -0
- package/runtime/storage/services/restore-snapshot-store.js +193 -0
- package/runtime/storage/services/runtime-secret-anchor-store.d.ts +26 -0
- package/runtime/storage/services/runtime-secret-anchor-store.js +82 -0
- package/runtime/storage/services/tool-experience-store.d.ts +25 -0
- package/runtime/storage/services/tool-experience-store.js +116 -0
- package/runtime/storage/services/write-validation-gate.d.ts +46 -0
- package/runtime/storage/services/write-validation-gate.js +200 -0
- 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
|
});
|