@haaaiawd/second-nature 0.2.12 → 0.2.13
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/index.js +96 -6
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/runtime/cli/commands/index.js +85 -11
- package/runtime/cli/host-capability/host-discovery-port.d.ts +85 -0
- package/runtime/cli/host-capability/host-discovery-port.js +137 -0
- package/runtime/cli/ops/heartbeat-surface.d.ts +3 -3
- package/runtime/cli/ops/heartbeat-surface.js +6 -5
- package/runtime/cli/ops/ops-router.d.ts +6 -2
- package/runtime/cli/ops/ops-router.js +1273 -1145
- package/runtime/connectors/base/normalized-evidence-content.d.ts +4 -0
- package/runtime/connectors/base/normalized-evidence-content.js +21 -2
- package/runtime/connectors/evidence-normalizer.js +32 -1
- package/runtime/core/second-nature/action/action-closure-recorder.d.ts +2 -0
- package/runtime/core/second-nature/action/action-closure-recorder.js +49 -34
- package/runtime/core/second-nature/action/action-proposal-builder.js +3 -2
- package/runtime/core/second-nature/action/policy-bound-dispatch.d.ts +2 -0
- package/runtime/core/second-nature/action/policy-bound-dispatch.js +7 -3
- package/runtime/core/second-nature/control-plane/cycle-finalizer.d.ts +82 -0
- package/runtime/core/second-nature/control-plane/cycle-finalizer.js +187 -0
- package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.js +13 -9
- package/runtime/core/second-nature/control-plane/real-runtime-spine.js +1 -1
- package/runtime/core/second-nature/guidance/guidance-proposal-consumer.d.ts +2 -1
- package/runtime/core/second-nature/guidance/guidance-proposal-consumer.js +4 -2
- package/runtime/core/second-nature/perception/judgment-engine.js +8 -4
- package/runtime/core/second-nature/perception/perception-builder.js +14 -2
- package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.js +30 -3
- package/runtime/core/second-nature/quiet-dream/dream-consolidation-runner.d.ts +5 -1
- package/runtime/core/second-nature/quiet-dream/dream-consolidation-runner.js +68 -29
- package/runtime/core/second-nature/quiet-dream/dream-scheduler.js +2 -1
- package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.js +2 -1
- package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.d.ts +1 -0
- package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.js +33 -0
- package/runtime/observability/causal-loop-health.d.ts +2 -1
- package/runtime/observability/causal-loop-health.js +7 -0
- package/runtime/observability/loop-stage-event-sink.js +6 -1
- package/runtime/observability/loop-status.d.ts +2 -0
- package/runtime/observability/loop-status.js +14 -1
- package/runtime/observability/services/heartbeat-digest-assembler.d.ts +3 -0
- package/runtime/observability/services/heartbeat-digest-assembler.js +9 -0
- package/runtime/shared/degraded-status-classifier.d.ts +16 -0
- package/runtime/shared/degraded-status-classifier.js +68 -0
- package/runtime/shared/evidence-level-classifier.d.ts +61 -0
- package/runtime/shared/evidence-level-classifier.js +116 -0
- package/runtime/shared/provenance-tier.d.ts +37 -0
- package/runtime/shared/provenance-tier.js +97 -0
- package/runtime/shared/setup-ack.d.ts +54 -0
- package/runtime/shared/setup-ack.js +108 -0
- package/runtime/shared/source-ref-compat.js +5 -2
- package/runtime/shared/types/v8-contracts.d.ts +13 -2
- package/runtime/storage/db/index.js +71 -28
- package/runtime/storage/db/migrations/v8-005-single-status-schema.js +2 -2
- package/runtime/storage/db/migrations/v8-006-loop-stage-event-proof-trace-columns.d.ts +9 -0
- package/runtime/storage/db/migrations/v8-006-loop-stage-event-proof-trace-columns.js +15 -0
- package/runtime/storage/db/schema/v8-entities.d.ts +76 -0
- package/runtime/storage/db/schema/v8-entities.js +4 -0
- package/runtime/storage/services/write-validation-gate.js +1 -1
- package/runtime/storage/v8-state-stores.d.ts +7 -2
- package/runtime/storage/v8-state-stores.js +37 -19
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
export declare const NORMALIZED_EVIDENCE_SCHEMA_VERSION = 1;
|
|
24
24
|
export type EvidenceSourceKind = "post" | "comment" | "profile" | "task" | "event" | "game_state" | "notification" | "document" | "unknown";
|
|
25
25
|
export type SummaryProducer = "connector_rules" | "model_assist" | "operator_supplied";
|
|
26
|
+
export type EvidenceContentStatus = "content_present" | "content_missing" | "content_redacted";
|
|
27
|
+
export type EvidenceContentMissingReason = "id_only" | "empty_payload" | "unsupported_shape" | "redacted_private";
|
|
26
28
|
export interface EvidenceActor {
|
|
27
29
|
id?: string;
|
|
28
30
|
displayName?: string;
|
|
@@ -36,6 +38,8 @@ export interface NormalizedEvidenceContent {
|
|
|
36
38
|
externalId?: string;
|
|
37
39
|
title?: string;
|
|
38
40
|
summary: string;
|
|
41
|
+
contentStatus: EvidenceContentStatus;
|
|
42
|
+
contentMissingReason?: EvidenceContentMissingReason;
|
|
39
43
|
excerpt?: string;
|
|
40
44
|
canonicalText?: string;
|
|
41
45
|
actor?: EvidenceActor;
|
|
@@ -226,8 +226,25 @@ function normalizeSingleItem(item, options) {
|
|
|
226
226
|
const tags = extractTags(item);
|
|
227
227
|
const entities = extractEntities(item);
|
|
228
228
|
const metrics = extractMetrics(item);
|
|
229
|
-
const
|
|
230
|
-
const
|
|
229
|
+
const hasReadableContent = isNonEmptyString(bodyText) || isNonEmptyString(title);
|
|
230
|
+
const hasContentMarkers = isNonEmptyString(url) ||
|
|
231
|
+
(actor?.displayName && actor.displayName.length > 0) ||
|
|
232
|
+
tags.length > 0 ||
|
|
233
|
+
entities.length > 0 ||
|
|
234
|
+
(metrics && Object.keys(metrics).length > 0);
|
|
235
|
+
let contentStatus = "content_present";
|
|
236
|
+
let contentMissingReason;
|
|
237
|
+
let summary;
|
|
238
|
+
if (!hasReadableContent && !hasContentMarkers) {
|
|
239
|
+
contentStatus = "content_missing";
|
|
240
|
+
contentMissingReason = externalId ? "id_only" : "empty_payload";
|
|
241
|
+
summary = `Content missing: ${contentMissingReason === "id_only" ? "id-only evidence" : "empty payload"}`;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
const rawText = bodyText ?? title ?? "[no readable content]";
|
|
245
|
+
summary = truncate(rawText, 160);
|
|
246
|
+
}
|
|
247
|
+
const rawText = bodyText ?? title ?? "";
|
|
231
248
|
const excerpt = rawText.length > summary.length ? truncate(rawText, options.excerptMaxChars ?? 240) : undefined;
|
|
232
249
|
const canonicalText = truncate(rawText, options.canonicalTextMaxChars ?? 2000);
|
|
233
250
|
return {
|
|
@@ -238,6 +255,8 @@ function normalizeSingleItem(item, options) {
|
|
|
238
255
|
externalId,
|
|
239
256
|
title,
|
|
240
257
|
summary,
|
|
258
|
+
contentStatus,
|
|
259
|
+
contentMissingReason,
|
|
241
260
|
excerpt,
|
|
242
261
|
canonicalText,
|
|
243
262
|
actor,
|
|
@@ -31,6 +31,11 @@ import { extractNormalizedEvidenceItems, computeEvidenceContentHashSync, } from
|
|
|
31
31
|
function computeLegacyContentHash(content) {
|
|
32
32
|
return crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
33
33
|
}
|
|
34
|
+
function truncate(text, maxChars) {
|
|
35
|
+
if (text.length <= maxChars)
|
|
36
|
+
return text;
|
|
37
|
+
return `${text.slice(0, maxChars)}…`;
|
|
38
|
+
}
|
|
34
39
|
function buildSourceRef(platformId, capabilityId, itemId, observedAt) {
|
|
35
40
|
return {
|
|
36
41
|
uri: `sn://connector_result/${platformId}/${capabilityId}/${itemId}`,
|
|
@@ -191,6 +196,32 @@ async function normalizeLegacyConnectorEvidence(db, result, now) {
|
|
|
191
196
|
const observedAt = result.observedAt ?? now;
|
|
192
197
|
const sourceRef = buildSourceRef(result.platformId, result.capabilityId, itemId, observedAt);
|
|
193
198
|
const sensitivityHint = mergeSensitivityHint(inferSensitivityHint(item.content), item.sensitivityHint);
|
|
199
|
+
const trimmedContent = item.content.trim();
|
|
200
|
+
const looksLikeIdOnly = /^[a-z0-9_-]+$/i.test(trimmedContent) && trimmedContent.length < 64;
|
|
201
|
+
const contentStatus = looksLikeIdOnly || trimmedContent.length === 0
|
|
202
|
+
? "content_missing"
|
|
203
|
+
: "content_present";
|
|
204
|
+
const contentMissingReason = contentStatus === "content_missing"
|
|
205
|
+
? trimmedContent.length === 0
|
|
206
|
+
? "empty_payload"
|
|
207
|
+
: "id_only"
|
|
208
|
+
: undefined;
|
|
209
|
+
const normalized = {
|
|
210
|
+
schemaVersion: 1,
|
|
211
|
+
sourceKind: "unknown",
|
|
212
|
+
platformId: result.platformId,
|
|
213
|
+
capabilityId: result.capabilityId,
|
|
214
|
+
externalId: item.id,
|
|
215
|
+
summary: contentStatus === "content_missing"
|
|
216
|
+
? `Content missing: ${contentMissingReason === "id_only" ? "id-only evidence" : "empty payload"}`
|
|
217
|
+
: truncate(trimmedContent, 160),
|
|
218
|
+
contentStatus,
|
|
219
|
+
contentMissingReason,
|
|
220
|
+
excerpt: contentStatus === "content_present" ? truncate(trimmedContent, 240) : undefined,
|
|
221
|
+
canonicalText: contentStatus === "content_present" ? truncate(trimmedContent, 2000) : undefined,
|
|
222
|
+
observedAt,
|
|
223
|
+
summaryProducer: "connector_rules",
|
|
224
|
+
};
|
|
194
225
|
const writeResult = await writeEvidenceItem(db, {
|
|
195
226
|
id: `ev_${result.platformId}_${itemId}_${observedAt.replace(/[:.]/g, "")}`,
|
|
196
227
|
createdAt: now,
|
|
@@ -201,7 +232,7 @@ async function normalizeLegacyConnectorEvidence(db, result, now) {
|
|
|
201
232
|
sourceRefs: [sourceRef],
|
|
202
233
|
redactionClass: sensitivityHint === "sensitive" ? "blocked" : "none",
|
|
203
234
|
lifecycleStatus: "pending",
|
|
204
|
-
payloadJson:
|
|
235
|
+
payloadJson: JSON.stringify(normalized),
|
|
205
236
|
});
|
|
206
237
|
if ("id" in writeResult) {
|
|
207
238
|
evidenceIds.push(writeResult.id);
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* Test coverage: tests/unit/action/action-closure-recorder.test.ts
|
|
22
22
|
*/
|
|
23
23
|
import { writeActionClosureRecord, readActionClosuresByCycle, } from "../../../storage/v8-state-stores.js";
|
|
24
|
+
import { buildClosureProvenance, cycleTraceRef, closureTraceRef, decisionProofRef, } from "../../../shared/provenance-tier.js";
|
|
24
25
|
// ───────────────────────────────────────────────────────────────
|
|
25
26
|
// Helpers
|
|
26
27
|
// ───────────────────────────────────────────────────────────────
|
|
@@ -50,7 +51,8 @@ export async function recordNoActionClosure(db, cycleId, noActionReason, options
|
|
|
50
51
|
status: "no_action",
|
|
51
52
|
reason: noActionReason,
|
|
52
53
|
nextState: "await_next_cycle",
|
|
53
|
-
sourceRefs: [
|
|
54
|
+
sourceRefs: [cycleTraceRef(cycleId)],
|
|
55
|
+
proofRefs: [
|
|
54
56
|
{
|
|
55
57
|
uri: `sn://closure/no_action/${cycleId}`,
|
|
56
58
|
family: "action_closure",
|
|
@@ -59,8 +61,9 @@ export async function recordNoActionClosure(db, cycleId, noActionReason, options
|
|
|
59
61
|
resolveStatus: "resolvable",
|
|
60
62
|
},
|
|
61
63
|
],
|
|
64
|
+
traceRefs: [cycleTraceRef(cycleId)],
|
|
62
65
|
redactionClass: "none",
|
|
63
|
-
|
|
66
|
+
payload: { dispatchAttempt: 0, inputSummary: "no-action" },
|
|
64
67
|
});
|
|
65
68
|
if ("reason" in result) {
|
|
66
69
|
return result;
|
|
@@ -73,6 +76,10 @@ export async function recordNoActionClosure(db, cycleId, noActionReason, options
|
|
|
73
76
|
export async function recordRememberClosure(db, cycleId, memoryReviewCandidate, options) {
|
|
74
77
|
const now = options?.now ?? new Date().toISOString();
|
|
75
78
|
const closureId = `cls_remember_${cycleId}_${now.replace(/[:.]/g, "")}`;
|
|
79
|
+
const provenance = buildClosureProvenance({
|
|
80
|
+
sourceRefs: memoryReviewCandidate.sourceRefs,
|
|
81
|
+
traceRefs: [cycleTraceRef(cycleId)],
|
|
82
|
+
});
|
|
76
83
|
const result = await writeActionClosureRecord(db, {
|
|
77
84
|
id: closureId,
|
|
78
85
|
createdAt: now,
|
|
@@ -82,13 +89,15 @@ export async function recordRememberClosure(db, cycleId, memoryReviewCandidate,
|
|
|
82
89
|
status: "completed",
|
|
83
90
|
reason: "remember_for_review",
|
|
84
91
|
nextState: "pending_daily_review",
|
|
85
|
-
sourceRefs:
|
|
92
|
+
sourceRefs: provenance.sourceRefs,
|
|
93
|
+
proofRefs: provenance.proofRefs,
|
|
94
|
+
traceRefs: provenance.traceRefs,
|
|
86
95
|
redactionClass: "none",
|
|
87
|
-
|
|
96
|
+
payload: {
|
|
88
97
|
memoryReviewCandidate,
|
|
89
98
|
dispatchAttempt: 1,
|
|
90
99
|
inputSummary: `remember_for_review topic=${memoryReviewCandidate.topicKey}`,
|
|
91
|
-
}
|
|
100
|
+
},
|
|
92
101
|
});
|
|
93
102
|
if ("reason" in result) {
|
|
94
103
|
return result;
|
|
@@ -101,24 +110,21 @@ export async function recordRememberClosure(db, cycleId, memoryReviewCandidate,
|
|
|
101
110
|
export async function recordPolicyOutcomeClosure(db, cycleId, closureStatus, reason, params, options) {
|
|
102
111
|
const now = options?.now ?? new Date().toISOString();
|
|
103
112
|
const closureId = `cls_${closureStatus}_${cycleId}_${now.replace(/[:.]/g, "")}`;
|
|
104
|
-
const sourceRefs =
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
const sourceRefs = params.proposalId
|
|
114
|
+
? [
|
|
115
|
+
{
|
|
116
|
+
uri: `sn://proposal/${params.proposalId}`,
|
|
117
|
+
family: "action_closure",
|
|
118
|
+
id: params.proposalId,
|
|
119
|
+
redactionClass: "none",
|
|
120
|
+
resolveStatus: "resolvable",
|
|
121
|
+
},
|
|
122
|
+
]
|
|
123
|
+
: [];
|
|
124
|
+
const proofRefs = [
|
|
125
|
+
closureTraceRef(closureId),
|
|
126
|
+
...(params.decisionId ? [decisionProofRef(params.decisionId)] : []),
|
|
112
127
|
];
|
|
113
|
-
if (params.decisionId) {
|
|
114
|
-
sourceRefs.push({
|
|
115
|
-
uri: `sn://decision/${params.decisionId}`,
|
|
116
|
-
family: "action_closure",
|
|
117
|
-
id: params.decisionId,
|
|
118
|
-
redactionClass: "none",
|
|
119
|
-
resolveStatus: "resolvable",
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
128
|
const result = await writeActionClosureRecord(db, {
|
|
123
129
|
id: closureId,
|
|
124
130
|
createdAt: now,
|
|
@@ -131,13 +137,15 @@ export async function recordPolicyOutcomeClosure(db, cycleId, closureStatus, rea
|
|
|
131
137
|
reason,
|
|
132
138
|
nextState: params.nextState ?? "await_next_cycle",
|
|
133
139
|
sourceRefs,
|
|
140
|
+
proofRefs,
|
|
141
|
+
traceRefs: [cycleTraceRef(cycleId)],
|
|
134
142
|
redactionClass: "none",
|
|
135
|
-
|
|
143
|
+
payload: {
|
|
136
144
|
dispatchAttempt: 1,
|
|
137
145
|
inputSummary: buildInputSummary(params.proposalId, params.decisionId),
|
|
138
146
|
postProcessing: params.postProcessing ?? [],
|
|
139
147
|
downgradedActionKind: params.downgradedActionKind,
|
|
140
|
-
}
|
|
148
|
+
},
|
|
141
149
|
});
|
|
142
150
|
if ("reason" in result) {
|
|
143
151
|
return result;
|
|
@@ -150,14 +158,19 @@ export async function recordPolicyOutcomeClosure(db, cycleId, closureStatus, rea
|
|
|
150
158
|
export async function recordExecutionClosure(db, cycleId, closureStatus, reason, params, options) {
|
|
151
159
|
const now = options?.now ?? new Date().toISOString();
|
|
152
160
|
const closureId = `cls_exec_${closureStatus}_${cycleId}_${now.replace(/[:.]/g, "")}`;
|
|
153
|
-
const sourceRefs =
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
+
const sourceRefs = params.executionResultRef
|
|
162
|
+
? [
|
|
163
|
+
{
|
|
164
|
+
uri: params.executionResultRef,
|
|
165
|
+
family: "connector_result",
|
|
166
|
+
id: params.executionResultRef,
|
|
167
|
+
redactionClass: "none",
|
|
168
|
+
resolveStatus: "resolvable",
|
|
169
|
+
},
|
|
170
|
+
]
|
|
171
|
+
: [];
|
|
172
|
+
const proofRefs = [
|
|
173
|
+
closureTraceRef(closureId),
|
|
161
174
|
];
|
|
162
175
|
const result = await writeActionClosureRecord(db, {
|
|
163
176
|
id: closureId,
|
|
@@ -171,14 +184,16 @@ export async function recordExecutionClosure(db, cycleId, closureStatus, reason,
|
|
|
171
184
|
reason,
|
|
172
185
|
nextState: params.nextState ?? (closureStatus === "completed" ? "await_next_cycle" : "retryable"),
|
|
173
186
|
sourceRefs,
|
|
187
|
+
proofRefs,
|
|
188
|
+
traceRefs: [cycleTraceRef(cycleId)],
|
|
174
189
|
redactionClass: "none",
|
|
175
|
-
|
|
190
|
+
payload: {
|
|
176
191
|
dispatchAttempt: 1,
|
|
177
192
|
executionResultRef: params.executionResultRef,
|
|
178
193
|
outputSummary: params.outputSummary,
|
|
179
194
|
inputSummary: buildInputSummary(params.proposalId, params.decisionId),
|
|
180
195
|
retryable: params.retryable ?? closureStatus === "failed",
|
|
181
|
-
}
|
|
196
|
+
},
|
|
182
197
|
});
|
|
183
198
|
if ("reason" in result) {
|
|
184
199
|
return result;
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import { readJudgmentVerdictById, } from "../../../storage/v8-state-stores.js";
|
|
26
26
|
import { parseSourceRefs } from "../../../shared/serialization.js";
|
|
27
27
|
import { ACTION_KIND_REGISTRY } from "../../../shared/types/v8-contracts.js";
|
|
28
|
+
import { classifyDegradedStatus } from "../../../shared/degraded-status-classifier.js";
|
|
28
29
|
// ───────────────────────────────────────────────────────────────
|
|
29
30
|
// Helpers
|
|
30
31
|
// ───────────────────────────────────────────────────────────────
|
|
@@ -70,7 +71,7 @@ export async function buildActionProposal(db, judgmentVerdictId, options) {
|
|
|
70
71
|
const verdict = readResult.row;
|
|
71
72
|
if (!verdict) {
|
|
72
73
|
return {
|
|
73
|
-
status: "
|
|
74
|
+
status: classifyDegradedStatus("state_unreadable"),
|
|
74
75
|
reason: "state_unreadable",
|
|
75
76
|
ownerStage: "policy",
|
|
76
77
|
sourceRefs: [],
|
|
@@ -174,7 +175,7 @@ export async function buildActionProposals(db, judgmentVerdictIds, options) {
|
|
|
174
175
|
else if ("status" in result && result.status === "remember_for_review") {
|
|
175
176
|
rememberForReviews.push(result);
|
|
176
177
|
}
|
|
177
|
-
else if ("
|
|
178
|
+
else if ("operatorNextAction" in result) {
|
|
178
179
|
failed.push({
|
|
179
180
|
judgmentVerdictId,
|
|
180
181
|
degraded: result,
|
|
@@ -33,6 +33,7 @@ export interface ConnectorDispatchRequest {
|
|
|
33
33
|
decision: string;
|
|
34
34
|
};
|
|
35
35
|
sourceRefs: string;
|
|
36
|
+
proofRefs: string;
|
|
36
37
|
}
|
|
37
38
|
export interface GuidanceDispatchRequest {
|
|
38
39
|
type: "guidance";
|
|
@@ -43,6 +44,7 @@ export interface GuidanceDispatchRequest {
|
|
|
43
44
|
decision: string;
|
|
44
45
|
};
|
|
45
46
|
sourceRefs: string;
|
|
47
|
+
proofRefs: string;
|
|
46
48
|
}
|
|
47
49
|
export interface NoDispatchResult {
|
|
48
50
|
type: "none";
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* Test coverage: tests/unit/action/policy-bound-dispatch.test.ts
|
|
22
22
|
*/
|
|
23
23
|
import { serializeSourceRefs } from "../../../shared/serialization.js";
|
|
24
|
+
import { classifyDegradedStatus } from "../../../shared/degraded-status-classifier.js";
|
|
24
25
|
// ───────────────────────────────────────────────────────────────
|
|
25
26
|
// Helpers
|
|
26
27
|
// ───────────────────────────────────────────────────────────────
|
|
@@ -61,7 +62,8 @@ export function dispatchAllowedAction(proposal, decision, options) {
|
|
|
61
62
|
actionKind: target,
|
|
62
63
|
draftType,
|
|
63
64
|
policyProof: { decisionId: decision.id, decision: decision.decision },
|
|
64
|
-
sourceRefs: serializeSourceRefs(
|
|
65
|
+
sourceRefs: serializeSourceRefs(proposal.sourceRefs),
|
|
66
|
+
proofRefs: serializeSourceRefs(decision.proofRefs),
|
|
65
67
|
},
|
|
66
68
|
};
|
|
67
69
|
}
|
|
@@ -77,6 +79,7 @@ export function dispatchAllowedAction(proposal, decision, options) {
|
|
|
77
79
|
idempotencyKey: proposal.idempotencyKey,
|
|
78
80
|
policyProof: { decisionId: decision.id, decision: decision.decision },
|
|
79
81
|
sourceRefs: serializeSourceRefs(proposal.sourceRefs),
|
|
82
|
+
proofRefs: serializeSourceRefs(decision.proofRefs),
|
|
80
83
|
},
|
|
81
84
|
};
|
|
82
85
|
}
|
|
@@ -93,6 +96,7 @@ export function dispatchAllowedAction(proposal, decision, options) {
|
|
|
93
96
|
draftType,
|
|
94
97
|
policyProof: { decisionId: decision.id, decision: decision.decision },
|
|
95
98
|
sourceRefs: serializeSourceRefs(proposal.sourceRefs),
|
|
99
|
+
proofRefs: serializeSourceRefs(decision.proofRefs),
|
|
96
100
|
},
|
|
97
101
|
};
|
|
98
102
|
}
|
|
@@ -102,10 +106,10 @@ export function dispatchAllowedAction(proposal, decision, options) {
|
|
|
102
106
|
return {
|
|
103
107
|
type: "degraded",
|
|
104
108
|
degraded: {
|
|
105
|
-
status: "
|
|
109
|
+
status: classifyDegradedStatus("closure_failed"),
|
|
106
110
|
reason: "closure_failed",
|
|
107
111
|
ownerStage: "execution",
|
|
108
|
-
sourceRefs:
|
|
112
|
+
sourceRefs: proposal.sourceRefs,
|
|
109
113
|
operatorNextAction: "Unexpected policy decision shape",
|
|
110
114
|
retryable: false,
|
|
111
115
|
},
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CycleFinalizer — v8 exactly-one closure invariant (T-AC.R.2, T-AC.R.3)
|
|
3
|
+
*
|
|
4
|
+
* Core logic: provide a single boundary that records exactly one
|
|
5
|
+
* ActionClosureRecord or no-action closure per heartbeat cycle.
|
|
6
|
+
* Enforces idempotency key = cycleId, write order (closure row first),
|
|
7
|
+
* and restart reconcile for orphaned closure/event rows.
|
|
8
|
+
*
|
|
9
|
+
* Design authority:
|
|
10
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/action-closure-policy-system.md §6.1a`
|
|
11
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/action-closure-policy-system.detail.md §3.4`
|
|
12
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/control-plane-system.md §3.4`
|
|
13
|
+
*
|
|
14
|
+
* Dependencies:
|
|
15
|
+
* - `src/core/second-nature/action/action-closure-recorder.js`
|
|
16
|
+
* - `src/storage/v8-state-stores.js`
|
|
17
|
+
*
|
|
18
|
+
* Boundary:
|
|
19
|
+
* - One cycle → one closure row.
|
|
20
|
+
* - Duplicate terminal closure for same cycleId → `unsafe` idempotency conflict.
|
|
21
|
+
* - On partial failure, returns degraded diagnostic; caller records stage event.
|
|
22
|
+
* - Reconcile detects orphaned closure (event missing) or orphaned event (closure missing).
|
|
23
|
+
*
|
|
24
|
+
* Test coverage: tests/unit/control-plane/cycle-finalizer.test.ts
|
|
25
|
+
*/
|
|
26
|
+
import type { StateDatabase } from "../../../storage/db/index.js";
|
|
27
|
+
import type { DegradedOperationResult, V8ReasonCode, SourceRef } from "../../../shared/types/v8-contracts.js";
|
|
28
|
+
export interface CycleFinalizerResult {
|
|
29
|
+
closureRef?: SourceRef;
|
|
30
|
+
noActionReason?: V8ReasonCode;
|
|
31
|
+
degraded?: DegradedOperationResult;
|
|
32
|
+
}
|
|
33
|
+
export type ClosureKind = {
|
|
34
|
+
kind: "no_action";
|
|
35
|
+
reason: V8ReasonCode;
|
|
36
|
+
} | {
|
|
37
|
+
kind: "policy";
|
|
38
|
+
closureStatus: "completed" | "denied" | "deferred" | "downgraded";
|
|
39
|
+
reason: V8ReasonCode;
|
|
40
|
+
proposalId: string;
|
|
41
|
+
decisionId: string;
|
|
42
|
+
platformId?: string;
|
|
43
|
+
capabilityId?: string;
|
|
44
|
+
downgradedActionKind?: string;
|
|
45
|
+
} | {
|
|
46
|
+
kind: "execution";
|
|
47
|
+
closureStatus: "completed" | "failed";
|
|
48
|
+
reason: V8ReasonCode;
|
|
49
|
+
proposalId: string;
|
|
50
|
+
decisionId: string;
|
|
51
|
+
platformId?: string;
|
|
52
|
+
capabilityId?: string;
|
|
53
|
+
executionResultRef?: string;
|
|
54
|
+
outputSummary?: string;
|
|
55
|
+
};
|
|
56
|
+
export declare function finalizeCycle(db: StateDatabase, cycleId: string, closure: ClosureKind, options?: {
|
|
57
|
+
now?: string;
|
|
58
|
+
}): Promise<CycleFinalizerResult>;
|
|
59
|
+
export interface ReconcileResult {
|
|
60
|
+
/** Closure row exists but no closure stage event — event should be replayed. */
|
|
61
|
+
orphanedClosure?: {
|
|
62
|
+
closureId: string;
|
|
63
|
+
cycleId: string;
|
|
64
|
+
};
|
|
65
|
+
/** Stage event exists but no closure row — closure is missing, do not fabricate. */
|
|
66
|
+
orphanedEvent?: {
|
|
67
|
+
cycleId: string;
|
|
68
|
+
stage: string;
|
|
69
|
+
};
|
|
70
|
+
/** No orphan detected — cycle is consistent. */
|
|
71
|
+
consistent: boolean;
|
|
72
|
+
degraded?: DegradedOperationResult;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Reconcile a cycle's closure and stage event rows.
|
|
76
|
+
* Called at cycle start or by `loop_status` to detect partial-failure leftovers.
|
|
77
|
+
*
|
|
78
|
+
* Per design §6.1a:
|
|
79
|
+
* - closure row written, event missing → replay event with traceRefs
|
|
80
|
+
* - event written, closure row missing → report closure_unavailable / unsafe
|
|
81
|
+
*/
|
|
82
|
+
export declare function reconcileCycleClosure(db: StateDatabase, cycleId: string): Promise<ReconcileResult>;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CycleFinalizer — v8 exactly-one closure invariant (T-AC.R.2, T-AC.R.3)
|
|
3
|
+
*
|
|
4
|
+
* Core logic: provide a single boundary that records exactly one
|
|
5
|
+
* ActionClosureRecord or no-action closure per heartbeat cycle.
|
|
6
|
+
* Enforces idempotency key = cycleId, write order (closure row first),
|
|
7
|
+
* and restart reconcile for orphaned closure/event rows.
|
|
8
|
+
*
|
|
9
|
+
* Design authority:
|
|
10
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/action-closure-policy-system.md §6.1a`
|
|
11
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/action-closure-policy-system.detail.md §3.4`
|
|
12
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/control-plane-system.md §3.4`
|
|
13
|
+
*
|
|
14
|
+
* Dependencies:
|
|
15
|
+
* - `src/core/second-nature/action/action-closure-recorder.js`
|
|
16
|
+
* - `src/storage/v8-state-stores.js`
|
|
17
|
+
*
|
|
18
|
+
* Boundary:
|
|
19
|
+
* - One cycle → one closure row.
|
|
20
|
+
* - Duplicate terminal closure for same cycleId → `unsafe` idempotency conflict.
|
|
21
|
+
* - On partial failure, returns degraded diagnostic; caller records stage event.
|
|
22
|
+
* - Reconcile detects orphaned closure (event missing) or orphaned event (closure missing).
|
|
23
|
+
*
|
|
24
|
+
* Test coverage: tests/unit/control-plane/cycle-finalizer.test.ts
|
|
25
|
+
*/
|
|
26
|
+
import { recordNoActionClosure, recordPolicyOutcomeClosure, recordExecutionClosure, } from "../action/action-closure-recorder.js";
|
|
27
|
+
import { readActionClosuresByCycle, readLoopStageEventsByCycle, } from "../../../storage/v8-state-stores.js";
|
|
28
|
+
import { classifyDegradedStatus } from "../../../shared/degraded-status-classifier.js";
|
|
29
|
+
// Terminal closure statuses — a cycle with any of these is considered finalized.
|
|
30
|
+
const TERMINAL_CLOSURE_STATUSES = new Set([
|
|
31
|
+
"completed",
|
|
32
|
+
"no_action",
|
|
33
|
+
"denied",
|
|
34
|
+
"deferred",
|
|
35
|
+
"downgraded",
|
|
36
|
+
"failed",
|
|
37
|
+
]);
|
|
38
|
+
/**
|
|
39
|
+
* Check whether a cycle already has a terminal closure row.
|
|
40
|
+
* This is the idempotency gate for `finalizeCycle`.
|
|
41
|
+
*/
|
|
42
|
+
async function checkExistingTerminalClosure(db, cycleId) {
|
|
43
|
+
const existing = await readActionClosuresByCycle(db, cycleId);
|
|
44
|
+
if (existing.degraded)
|
|
45
|
+
return { hasTerminal: false };
|
|
46
|
+
const terminal = existing.rows.find((r) => TERMINAL_CLOSURE_STATUSES.has(r.status));
|
|
47
|
+
if (terminal)
|
|
48
|
+
return { hasTerminal: true, existingClosureId: terminal.id };
|
|
49
|
+
return { hasTerminal: false };
|
|
50
|
+
}
|
|
51
|
+
export async function finalizeCycle(db, cycleId, closure, options) {
|
|
52
|
+
const now = options?.now ?? new Date().toISOString();
|
|
53
|
+
// Idempotency gate: check if this cycle already has a terminal closure.
|
|
54
|
+
// recordNoActionClosure has its own idempotency check, but policy/execution do not.
|
|
55
|
+
// We check uniformly here to enforce the exactly-one invariant.
|
|
56
|
+
if (closure.kind !== "no_action") {
|
|
57
|
+
const existing = await checkExistingTerminalClosure(db, cycleId);
|
|
58
|
+
if (existing.hasTerminal) {
|
|
59
|
+
return {
|
|
60
|
+
degraded: {
|
|
61
|
+
status: "unsafe",
|
|
62
|
+
reason: "closure_idempotency_conflict",
|
|
63
|
+
ownerStage: "closure",
|
|
64
|
+
sourceRefs: [],
|
|
65
|
+
operatorNextAction: `Cycle ${cycleId} already has terminal closure ${existing.existingClosureId}; duplicate finalize blocked`,
|
|
66
|
+
retryable: false,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
switch (closure.kind) {
|
|
72
|
+
case "no_action": {
|
|
73
|
+
const result = await recordNoActionClosure(db, cycleId, closure.reason, { now });
|
|
74
|
+
if ("closureId" in result) {
|
|
75
|
+
return {
|
|
76
|
+
closureRef: {
|
|
77
|
+
uri: `sn://closure/${result.closureId}`,
|
|
78
|
+
family: "action_closure",
|
|
79
|
+
id: result.closureId,
|
|
80
|
+
redactionClass: "none",
|
|
81
|
+
resolveStatus: "resolvable",
|
|
82
|
+
},
|
|
83
|
+
noActionReason: closure.reason,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
noActionReason: closure.reason,
|
|
88
|
+
degraded: result,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
case "policy": {
|
|
92
|
+
const result = await recordPolicyOutcomeClosure(db, cycleId, closure.closureStatus, closure.reason, {
|
|
93
|
+
proposalId: closure.proposalId,
|
|
94
|
+
decisionId: closure.decisionId,
|
|
95
|
+
platformId: closure.platformId,
|
|
96
|
+
capabilityId: closure.capabilityId,
|
|
97
|
+
downgradedActionKind: closure.downgradedActionKind,
|
|
98
|
+
}, { now });
|
|
99
|
+
if ("closureId" in result) {
|
|
100
|
+
return {
|
|
101
|
+
closureRef: {
|
|
102
|
+
uri: `sn://closure/${result.closureId}`,
|
|
103
|
+
family: "action_closure",
|
|
104
|
+
id: result.closureId,
|
|
105
|
+
redactionClass: "none",
|
|
106
|
+
resolveStatus: "resolvable",
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return { degraded: result };
|
|
111
|
+
}
|
|
112
|
+
case "execution": {
|
|
113
|
+
const result = await recordExecutionClosure(db, cycleId, closure.closureStatus, closure.reason, {
|
|
114
|
+
proposalId: closure.proposalId,
|
|
115
|
+
decisionId: closure.decisionId,
|
|
116
|
+
platformId: closure.platformId,
|
|
117
|
+
capabilityId: closure.capabilityId,
|
|
118
|
+
executionResultRef: closure.executionResultRef,
|
|
119
|
+
outputSummary: closure.outputSummary,
|
|
120
|
+
}, { now });
|
|
121
|
+
if ("closureId" in result) {
|
|
122
|
+
return {
|
|
123
|
+
closureRef: {
|
|
124
|
+
uri: `sn://closure/${result.closureId}`,
|
|
125
|
+
family: "action_closure",
|
|
126
|
+
id: result.closureId,
|
|
127
|
+
redactionClass: "none",
|
|
128
|
+
resolveStatus: "resolvable",
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return { degraded: result };
|
|
133
|
+
}
|
|
134
|
+
default: {
|
|
135
|
+
const exhaustive = closure;
|
|
136
|
+
return {
|
|
137
|
+
degraded: {
|
|
138
|
+
status: classifyDegradedStatus("closure_failed"),
|
|
139
|
+
reason: "closure_failed",
|
|
140
|
+
ownerStage: "closure",
|
|
141
|
+
sourceRefs: [],
|
|
142
|
+
operatorNextAction: `Unknown closure kind: ${JSON.stringify(exhaustive)}`,
|
|
143
|
+
retryable: false,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Reconcile a cycle's closure and stage event rows.
|
|
151
|
+
* Called at cycle start or by `loop_status` to detect partial-failure leftovers.
|
|
152
|
+
*
|
|
153
|
+
* Per design §6.1a:
|
|
154
|
+
* - closure row written, event missing → replay event with traceRefs
|
|
155
|
+
* - event written, closure row missing → report closure_unavailable / unsafe
|
|
156
|
+
*/
|
|
157
|
+
export async function reconcileCycleClosure(db, cycleId) {
|
|
158
|
+
const closures = await readActionClosuresByCycle(db, cycleId);
|
|
159
|
+
if (closures.degraded) {
|
|
160
|
+
return {
|
|
161
|
+
consistent: false,
|
|
162
|
+
degraded: closures.degraded,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const events = await readLoopStageEventsByCycle(db, cycleId);
|
|
166
|
+
if (events.degraded) {
|
|
167
|
+
return {
|
|
168
|
+
consistent: false,
|
|
169
|
+
degraded: events.degraded,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const terminalClosure = closures.rows.find((r) => TERMINAL_CLOSURE_STATUSES.has(r.status));
|
|
173
|
+
const closureEvent = events.rows.find((r) => r.stage === "closure" || r.stage === "action_closure");
|
|
174
|
+
if (terminalClosure && !closureEvent) {
|
|
175
|
+
return {
|
|
176
|
+
orphanedClosure: { closureId: terminalClosure.id, cycleId },
|
|
177
|
+
consistent: false,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (!terminalClosure && closureEvent) {
|
|
181
|
+
return {
|
|
182
|
+
orphanedEvent: { cycleId, stage: closureEvent.stage },
|
|
183
|
+
consistent: false,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
return { consistent: true };
|
|
187
|
+
}
|