@haaaiawd/second-nature 0.1.39 → 0.1.40

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 (49) hide show
  1. package/index.js +1270 -1270
  2. package/openclaw.plugin.json +29 -29
  3. package/package.json +55 -55
  4. package/runtime/cli/commands/connector-init.js +11 -4
  5. package/runtime/cli/index.js +6 -1
  6. package/runtime/cli/ops/heartbeat-surface.d.ts +75 -75
  7. package/runtime/cli/ops/heartbeat-surface.js +97 -97
  8. package/runtime/cli/ops/ops-router.js +1428 -1428
  9. package/runtime/cli/ops/workspace-heartbeat-runner.js +236 -236
  10. package/runtime/connectors/services/connector-executor-adapter.js +192 -41
  11. package/runtime/core/second-nature/guidance/apply-guidance.d.ts +12 -12
  12. package/runtime/core/second-nature/guidance/apply-guidance.js +15 -15
  13. package/runtime/core/second-nature/guidance/user-reply-continuity.d.ts +50 -50
  14. package/runtime/core/second-nature/guidance/user-reply-continuity.js +89 -89
  15. package/runtime/core/second-nature/orchestrator/intent-planner.js +15 -0
  16. package/runtime/core/second-nature/runtime/service-entry.d.ts +39 -39
  17. package/runtime/core/second-nature/runtime/service-entry.js +44 -44
  18. package/runtime/dream/dream-engine.d.ts +14 -14
  19. package/runtime/dream/dream-engine.js +306 -306
  20. package/runtime/dream/dream-input-loader.d.ts +37 -37
  21. package/runtime/dream/dream-input-loader.js +150 -150
  22. package/runtime/dream/dream-scheduler.d.ts +75 -75
  23. package/runtime/dream/dream-scheduler.js +131 -131
  24. package/runtime/dream/index.d.ts +16 -16
  25. package/runtime/dream/index.js +14 -14
  26. package/runtime/dream/insight-extractor.d.ts +32 -32
  27. package/runtime/dream/insight-extractor.js +135 -135
  28. package/runtime/dream/memory-consolidator.d.ts +45 -45
  29. package/runtime/dream/memory-consolidator.js +140 -140
  30. package/runtime/dream/narrative-update-proposal.d.ts +34 -34
  31. package/runtime/dream/narrative-update-proposal.js +83 -83
  32. package/runtime/dream/output-validator.d.ts +20 -20
  33. package/runtime/dream/output-validator.js +110 -110
  34. package/runtime/dream/redaction-gate.d.ts +31 -31
  35. package/runtime/dream/redaction-gate.js +109 -109
  36. package/runtime/dream/relationship-update-proposal.d.ts +27 -27
  37. package/runtime/dream/relationship-update-proposal.js +119 -119
  38. package/runtime/dream/sampler.d.ts +30 -30
  39. package/runtime/dream/sampler.js +65 -65
  40. package/runtime/dream/types.d.ts +187 -187
  41. package/runtime/dream/types.js +11 -11
  42. package/runtime/guidance/fallback.js +20 -20
  43. package/runtime/guidance/guidance-assembler.js +76 -76
  44. package/runtime/guidance/output-guard.d.ts +13 -13
  45. package/runtime/guidance/output-guard.js +53 -53
  46. package/runtime/guidance/template-registry.d.ts +20 -20
  47. package/runtime/guidance/template-registry.js +93 -93
  48. package/runtime/guidance/types.d.ts +98 -98
  49. package/runtime/observability/projections/guidance-audit.js +38 -38
@@ -1,306 +1,306 @@
1
- /**
2
- * Dream Engine — orchestrates the hybrid memory consolidation pipeline.
3
- *
4
- * Pipeline: load inputs → consolidate (rules) → sample → redact →
5
- * optional model insights → merge → validate → write output + trace.
6
- *
7
- * Contract:
8
- * - Input store is never modified.
9
- * - Output is always candidate until validation passes and lifecycle port accepts it.
10
- * - Budget/redaction/timeout failures degrade gracefully with trace.
11
- * Test coverage: tests/integration/dream/t7-1-1-dream-pipeline.test.ts
12
- */
13
- import { consolidateMemory } from "./memory-consolidator.js";
14
- import { sampleDreamInput } from "./sampler.js";
15
- import { redactDreamInput } from "./redaction-gate.js";
16
- import { validateDreamOutput } from "./output-validator.js";
17
- const DEFAULT_TIMEOUT_MS = 30 * 60 * 1000; // 30min
18
- const DEFAULT_MAX_CANONICAL = 200;
19
- export async function runDream(input) {
20
- const startedAt = new Date().toISOString();
21
- const runId = input.runId;
22
- const traceId = input.traceId;
23
- const triggerKind = input.triggerKind;
24
- const options = input.options ?? {};
25
- const operatorTimeoutMs = options.operatorTimeoutMs ?? DEFAULT_TIMEOUT_MS;
26
- // ─── 1. Load inputs ────────────────────────────────────────────────────────
27
- const inputBundle = await input.statePort.loadDreamInputs({
28
- timeWindowDays: options.timeWindowDays ?? 30,
29
- evidenceLimit: options.evidenceLimit ?? 1000,
30
- });
31
- if (inputBundle.evidenceRefs.length === 0 &&
32
- inputBundle.chronicleEntryIds.length === 0 &&
33
- (inputBundle.inputCounts.memoryEntries ?? 0) === 0) {
34
- const trace = buildTrace({
35
- traceId,
36
- runId,
37
- startedAt,
38
- inputCounts: inputBundle.inputCounts,
39
- fallbackReason: "no_inputs",
40
- });
41
- await input.tracePort?.recordDreamTrace(trace);
42
- return {
43
- runId,
44
- status: "skipped",
45
- trace,
46
- fallbackReason: "no_inputs",
47
- };
48
- }
49
- // ─── 2. Rules consolidate ──────────────────────────────────────────────────
50
- // For the rules stage we use placeholder summaries derived from refs.
51
- // Real integration would load actual summaries from state ports.
52
- const evidenceSummaries = inputBundle.evidenceRefs.map((ref, i) => ({
53
- id: ref,
54
- summary: `evidence:${ref}`,
55
- sourceRefs: [{ sourceId: ref, kind: "evidence", url: undefined }],
56
- createdAt: new Date().toISOString(),
57
- }));
58
- const chronicleSummaries = inputBundle.chronicleEntryIds.map((id) => ({
59
- id,
60
- summary: `chronicle:${id}`,
61
- sourceRefs: [{ sourceId: id, kind: "chronicle", url: undefined }],
62
- createdAt: new Date().toISOString(),
63
- }));
64
- const toolExperienceSummaries = (inputBundle.toolExperienceSummaries ?? []).map((te) => ({
65
- id: `${te.connectorId}:${te.capabilityId}:${te.outcome}`,
66
- summary: `tool_experience:${te.connectorId}:${te.capabilityId}:${te.outcome}:count=${te.count}`,
67
- sourceRefs: [{ sourceId: `tool_exp:${te.connectorId}:${te.capabilityId}`, kind: "tool_experience", url: undefined }],
68
- createdAt: te.lastRecordedAt,
69
- }));
70
- const consolidation = consolidateMemory({
71
- evidenceSummaries,
72
- chronicleSummaries,
73
- toolExperienceSummaries,
74
- existingEntries: [], // In real use, load from activeMemoryStoreId
75
- });
76
- // ─── 3. Sample ─────────────────────────────────────────────────────────────
77
- const sampling = sampleDreamInput({
78
- evidenceSummaries: evidenceSummaries.map((e) => ({
79
- id: e.id,
80
- summary: e.summary,
81
- createdAt: e.createdAt,
82
- })),
83
- chronicleSummaries: chronicleSummaries.map((c) => ({
84
- id: c.id,
85
- summary: c.summary,
86
- createdAt: c.createdAt,
87
- })),
88
- evidenceLimit: options.evidenceLimit,
89
- });
90
- // ─── 4. Redaction ──────────────────────────────────────────────────────────
91
- const redaction = redactDreamInput({
92
- evidenceSummaries: sampling.sampledEvidenceIds.map((id) => evidenceSummaries.find((e) => e.id === id)?.summary ?? id),
93
- chronicleSummaries: sampling.sampledChronicleIds.map((id) => chronicleSummaries.find((c) => c.id === id)?.summary ?? id),
94
- });
95
- if (!redaction.allowed) {
96
- const output = buildOutput({
97
- runId,
98
- inputMemoryStoreId: inputBundle.activeMemoryStoreId,
99
- canonicalEntries: consolidation.entries.slice(0, options.maxCanonicalEntries ?? DEFAULT_MAX_CANONICAL),
100
- insights: [],
101
- validation: {
102
- schemaValid: true,
103
- sourceGrounded: true,
104
- sensitivityClean: false,
105
- unsupportedClaims: [],
106
- errors: [redaction.blockedReason ?? "redaction_failed"],
107
- checkedAt: new Date().toISOString(),
108
- },
109
- });
110
- output.status = "archived";
111
- await input.statePort.writeDreamOutput(output);
112
- // DR-023: redaction failure is a validation failure → archived lifecycle.
113
- await input.statePort.markDreamOutputLifecycle({
114
- outputId: output.outputId,
115
- newStatus: "archived",
116
- validation: output.validation,
117
- updatedAt: new Date().toISOString(),
118
- });
119
- const trace = buildTrace({
120
- traceId,
121
- runId,
122
- startedAt,
123
- inputCounts: inputBundle.inputCounts,
124
- fallbackReason: redaction.blockedReason ?? "redaction_failed",
125
- sensitivityFailure: true,
126
- });
127
- await input.tracePort?.recordDreamTrace(trace);
128
- return {
129
- runId,
130
- status: "completed",
131
- output,
132
- trace,
133
- fallbackReason: redaction.blockedReason ?? "redaction_failed",
134
- };
135
- }
136
- // ─── 5. Budget gate ────────────────────────────────────────────────────────
137
- let modelResult;
138
- let mode = "rules_only";
139
- let fallbackReason;
140
- let llmCostUsd;
141
- if ((input.modelAssistPort || input.modelPort) && input.budgetPort) {
142
- const budgetCheck = await input.budgetPort.checkBudget(0.5);
143
- if (budgetCheck.allowed) {
144
- // ─── 6. Model insights ─────────────────────────────────────────────────
145
- try {
146
- let modelPromise;
147
- if (input.modelAssistPort) {
148
- // DR-027: ModelAssistPort requires RedactedEvidenceBundle brand type.
149
- // Evidence already redacted by redactDreamInput above; construct brand
150
- // bundle directly to avoid double-redaction.
151
- const redactedBundle = {
152
- _brand: "redacted",
153
- evidence: redaction.redactedEvidence,
154
- chronicle: redaction.redactedChronicle,
155
- memory: redaction.redactedMemory,
156
- };
157
- modelPromise = input.modelAssistPort.extractInsights(redactedBundle);
158
- }
159
- else if (input.modelPort) {
160
- // Deprecated path: DreamModelPort accepts plain object (backward compat).
161
- modelPromise = input.modelPort.extractInsights({
162
- sampledEvidence: redaction.redactedEvidence,
163
- chronicleSummary: redaction.redactedChronicle.join("\n"),
164
- redacted: true,
165
- });
166
- }
167
- if (!fallbackReason) {
168
- const timeoutPromise = new Promise((_, reject) => {
169
- setTimeout(() => reject(new Error("model_timeout")), operatorTimeoutMs);
170
- });
171
- modelResult = await Promise.race([modelPromise, timeoutPromise]);
172
- mode = "hybrid_llm";
173
- llmCostUsd = modelResult.costUsd;
174
- }
175
- }
176
- catch (err) {
177
- const msg = err instanceof Error ? err.message : String(err);
178
- if (msg.includes("timeout")) {
179
- fallbackReason = "model_timeout";
180
- mode = "model_skipped";
181
- }
182
- else {
183
- fallbackReason = "model_error";
184
- mode = "model_skipped";
185
- }
186
- }
187
- }
188
- else {
189
- fallbackReason = "budget_exceeded";
190
- mode = "rules_only";
191
- }
192
- }
193
- else {
194
- fallbackReason = "model_port_unavailable";
195
- mode = "rules_only";
196
- }
197
- // ─── 7. Merge ──────────────────────────────────────────────────────────────
198
- const canonicalEntries = consolidation.entries.slice(0, options.maxCanonicalEntries ?? DEFAULT_MAX_CANONICAL);
199
- const insights = modelResult?.insights ?? [];
200
- const output = buildOutput({
201
- runId,
202
- inputMemoryStoreId: inputBundle.activeMemoryStoreId,
203
- canonicalEntries,
204
- insights,
205
- narrativeUpdate: modelResult?.narrativeUpdate,
206
- relationshipUpdate: modelResult?.relationshipUpdate,
207
- validation: {
208
- schemaValid: true,
209
- sourceGrounded: true,
210
- sensitivityClean: true,
211
- unsupportedClaims: modelResult?.unsupportedClaims ?? [],
212
- errors: [],
213
- checkedAt: new Date().toISOString(),
214
- },
215
- });
216
- // ─── 8. Validate ───────────────────────────────────────────────────────────
217
- const toolExperienceIds = toolExperienceSummaries.map((t) => t.sourceRefs[0].sourceId);
218
- const validation = validateDreamOutput({
219
- output,
220
- inputEvidenceIds: inputBundle.evidenceRefs,
221
- inputChronicleIds: inputBundle.chronicleEntryIds,
222
- inputToolExperienceIds: toolExperienceIds,
223
- });
224
- // Update output with validation result
225
- output.validation = validation.validation;
226
- let outputStatus = "candidate";
227
- if (!validation.eligible) {
228
- outputStatus = "archived";
229
- // If model failed but rules produced something, mark partial
230
- if (fallbackReason === "model_timeout") {
231
- outputStatus = "partial";
232
- }
233
- }
234
- output.status = outputStatus;
235
- // ─── 9. Write output + lifecycle transition ──────────────────────────────────
236
- await input.statePort.writeDreamOutput(output);
237
- // DR-023: validation pass triggers accepted transition.
238
- if (validation.eligible) {
239
- try {
240
- await input.statePort.markDreamOutputLifecycle({
241
- outputId: output.outputId,
242
- newStatus: "accepted",
243
- validation: validation.validation,
244
- updatedAt: new Date().toISOString(),
245
- });
246
- output.status = "accepted";
247
- }
248
- catch {
249
- // Transition failed (e.g., concurrent modification); keep candidate in memory
250
- // but DB remains candidate since transition was rolled back.
251
- output.status = "candidate";
252
- }
253
- }
254
- const finishedAt = new Date().toISOString();
255
- const durationMs = new Date(finishedAt).getTime() - new Date(startedAt).getTime();
256
- const trace = buildTrace({
257
- traceId,
258
- runId,
259
- startedAt,
260
- finishedAt,
261
- durationMs,
262
- inputCounts: inputBundle.inputCounts,
263
- fallbackReason,
264
- llmCostUsd,
265
- validationErrors: validation.validation.errors,
266
- timeoutMs: fallbackReason === "model_timeout" ? operatorTimeoutMs : undefined,
267
- sensitivityFailure: false,
268
- });
269
- await input.tracePort?.recordDreamTrace(trace);
270
- return {
271
- runId,
272
- status: "completed",
273
- output,
274
- trace,
275
- fallbackReason,
276
- };
277
- }
278
- // ─── Helpers ──────────────────────────────────────────────────────────────────
279
- function buildOutput(params) {
280
- return {
281
- outputId: `dream_output:${crypto.randomUUID()}`,
282
- runId: params.runId,
283
- status: "candidate",
284
- inputMemoryStoreId: params.inputMemoryStoreId,
285
- canonicalEntries: params.canonicalEntries,
286
- insights: params.insights,
287
- narrativeUpdate: params.narrativeUpdate,
288
- relationshipUpdate: params.relationshipUpdate,
289
- validation: params.validation,
290
- };
291
- }
292
- function buildTrace(params) {
293
- return {
294
- traceId: params.traceId,
295
- runId: params.runId,
296
- startedAt: params.startedAt,
297
- finishedAt: params.finishedAt ?? params.startedAt,
298
- durationMs: params.durationMs ?? 0,
299
- inputCounts: params.inputCounts,
300
- fallbackReason: params.fallbackReason,
301
- llmCostUsd: params.llmCostUsd,
302
- validationErrors: params.validationErrors,
303
- timeoutMs: params.timeoutMs,
304
- sensitivityFailure: params.sensitivityFailure,
305
- };
306
- }
1
+ /**
2
+ * Dream Engine — orchestrates the hybrid memory consolidation pipeline.
3
+ *
4
+ * Pipeline: load inputs → consolidate (rules) → sample → redact →
5
+ * optional model insights → merge → validate → write output + trace.
6
+ *
7
+ * Contract:
8
+ * - Input store is never modified.
9
+ * - Output is always candidate until validation passes and lifecycle port accepts it.
10
+ * - Budget/redaction/timeout failures degrade gracefully with trace.
11
+ * Test coverage: tests/integration/dream/t7-1-1-dream-pipeline.test.ts
12
+ */
13
+ import { consolidateMemory } from "./memory-consolidator.js";
14
+ import { sampleDreamInput } from "./sampler.js";
15
+ import { redactDreamInput } from "./redaction-gate.js";
16
+ import { validateDreamOutput } from "./output-validator.js";
17
+ const DEFAULT_TIMEOUT_MS = 30 * 60 * 1000; // 30min
18
+ const DEFAULT_MAX_CANONICAL = 200;
19
+ export async function runDream(input) {
20
+ const startedAt = new Date().toISOString();
21
+ const runId = input.runId;
22
+ const traceId = input.traceId;
23
+ const triggerKind = input.triggerKind;
24
+ const options = input.options ?? {};
25
+ const operatorTimeoutMs = options.operatorTimeoutMs ?? DEFAULT_TIMEOUT_MS;
26
+ // ─── 1. Load inputs ────────────────────────────────────────────────────────
27
+ const inputBundle = await input.statePort.loadDreamInputs({
28
+ timeWindowDays: options.timeWindowDays ?? 30,
29
+ evidenceLimit: options.evidenceLimit ?? 1000,
30
+ });
31
+ if (inputBundle.evidenceRefs.length === 0 &&
32
+ inputBundle.chronicleEntryIds.length === 0 &&
33
+ (inputBundle.inputCounts.memoryEntries ?? 0) === 0) {
34
+ const trace = buildTrace({
35
+ traceId,
36
+ runId,
37
+ startedAt,
38
+ inputCounts: inputBundle.inputCounts,
39
+ fallbackReason: "no_inputs",
40
+ });
41
+ await input.tracePort?.recordDreamTrace(trace);
42
+ return {
43
+ runId,
44
+ status: "skipped",
45
+ trace,
46
+ fallbackReason: "no_inputs",
47
+ };
48
+ }
49
+ // ─── 2. Rules consolidate ──────────────────────────────────────────────────
50
+ // For the rules stage we use placeholder summaries derived from refs.
51
+ // Real integration would load actual summaries from state ports.
52
+ const evidenceSummaries = inputBundle.evidenceRefs.map((ref, i) => ({
53
+ id: ref,
54
+ summary: `evidence:${ref}`,
55
+ sourceRefs: [{ sourceId: ref, kind: "evidence", url: undefined }],
56
+ createdAt: new Date().toISOString(),
57
+ }));
58
+ const chronicleSummaries = inputBundle.chronicleEntryIds.map((id) => ({
59
+ id,
60
+ summary: `chronicle:${id}`,
61
+ sourceRefs: [{ sourceId: id, kind: "chronicle", url: undefined }],
62
+ createdAt: new Date().toISOString(),
63
+ }));
64
+ const toolExperienceSummaries = (inputBundle.toolExperienceSummaries ?? []).map((te) => ({
65
+ id: `${te.connectorId}:${te.capabilityId}:${te.outcome}`,
66
+ summary: `tool_experience:${te.connectorId}:${te.capabilityId}:${te.outcome}:count=${te.count}`,
67
+ sourceRefs: [{ sourceId: `tool_exp:${te.connectorId}:${te.capabilityId}`, kind: "tool_experience", url: undefined }],
68
+ createdAt: te.lastRecordedAt,
69
+ }));
70
+ const consolidation = consolidateMemory({
71
+ evidenceSummaries,
72
+ chronicleSummaries,
73
+ toolExperienceSummaries,
74
+ existingEntries: [], // In real use, load from activeMemoryStoreId
75
+ });
76
+ // ─── 3. Sample ─────────────────────────────────────────────────────────────
77
+ const sampling = sampleDreamInput({
78
+ evidenceSummaries: evidenceSummaries.map((e) => ({
79
+ id: e.id,
80
+ summary: e.summary,
81
+ createdAt: e.createdAt,
82
+ })),
83
+ chronicleSummaries: chronicleSummaries.map((c) => ({
84
+ id: c.id,
85
+ summary: c.summary,
86
+ createdAt: c.createdAt,
87
+ })),
88
+ evidenceLimit: options.evidenceLimit,
89
+ });
90
+ // ─── 4. Redaction ──────────────────────────────────────────────────────────
91
+ const redaction = redactDreamInput({
92
+ evidenceSummaries: sampling.sampledEvidenceIds.map((id) => evidenceSummaries.find((e) => e.id === id)?.summary ?? id),
93
+ chronicleSummaries: sampling.sampledChronicleIds.map((id) => chronicleSummaries.find((c) => c.id === id)?.summary ?? id),
94
+ });
95
+ if (!redaction.allowed) {
96
+ const output = buildOutput({
97
+ runId,
98
+ inputMemoryStoreId: inputBundle.activeMemoryStoreId,
99
+ canonicalEntries: consolidation.entries.slice(0, options.maxCanonicalEntries ?? DEFAULT_MAX_CANONICAL),
100
+ insights: [],
101
+ validation: {
102
+ schemaValid: true,
103
+ sourceGrounded: true,
104
+ sensitivityClean: false,
105
+ unsupportedClaims: [],
106
+ errors: [redaction.blockedReason ?? "redaction_failed"],
107
+ checkedAt: new Date().toISOString(),
108
+ },
109
+ });
110
+ output.status = "archived";
111
+ await input.statePort.writeDreamOutput(output);
112
+ // DR-023: redaction failure is a validation failure → archived lifecycle.
113
+ await input.statePort.markDreamOutputLifecycle({
114
+ outputId: output.outputId,
115
+ newStatus: "archived",
116
+ validation: output.validation,
117
+ updatedAt: new Date().toISOString(),
118
+ });
119
+ const trace = buildTrace({
120
+ traceId,
121
+ runId,
122
+ startedAt,
123
+ inputCounts: inputBundle.inputCounts,
124
+ fallbackReason: redaction.blockedReason ?? "redaction_failed",
125
+ sensitivityFailure: true,
126
+ });
127
+ await input.tracePort?.recordDreamTrace(trace);
128
+ return {
129
+ runId,
130
+ status: "completed",
131
+ output,
132
+ trace,
133
+ fallbackReason: redaction.blockedReason ?? "redaction_failed",
134
+ };
135
+ }
136
+ // ─── 5. Budget gate ────────────────────────────────────────────────────────
137
+ let modelResult;
138
+ let mode = "rules_only";
139
+ let fallbackReason;
140
+ let llmCostUsd;
141
+ if ((input.modelAssistPort || input.modelPort) && input.budgetPort) {
142
+ const budgetCheck = await input.budgetPort.checkBudget(0.5);
143
+ if (budgetCheck.allowed) {
144
+ // ─── 6. Model insights ─────────────────────────────────────────────────
145
+ try {
146
+ let modelPromise;
147
+ if (input.modelAssistPort) {
148
+ // DR-027: ModelAssistPort requires RedactedEvidenceBundle brand type.
149
+ // Evidence already redacted by redactDreamInput above; construct brand
150
+ // bundle directly to avoid double-redaction.
151
+ const redactedBundle = {
152
+ _brand: "redacted",
153
+ evidence: redaction.redactedEvidence,
154
+ chronicle: redaction.redactedChronicle,
155
+ memory: redaction.redactedMemory,
156
+ };
157
+ modelPromise = input.modelAssistPort.extractInsights(redactedBundle);
158
+ }
159
+ else if (input.modelPort) {
160
+ // Deprecated path: DreamModelPort accepts plain object (backward compat).
161
+ modelPromise = input.modelPort.extractInsights({
162
+ sampledEvidence: redaction.redactedEvidence,
163
+ chronicleSummary: redaction.redactedChronicle.join("\n"),
164
+ redacted: true,
165
+ });
166
+ }
167
+ if (!fallbackReason) {
168
+ const timeoutPromise = new Promise((_, reject) => {
169
+ setTimeout(() => reject(new Error("model_timeout")), operatorTimeoutMs);
170
+ });
171
+ modelResult = await Promise.race([modelPromise, timeoutPromise]);
172
+ mode = "hybrid_llm";
173
+ llmCostUsd = modelResult.costUsd;
174
+ }
175
+ }
176
+ catch (err) {
177
+ const msg = err instanceof Error ? err.message : String(err);
178
+ if (msg.includes("timeout")) {
179
+ fallbackReason = "model_timeout";
180
+ mode = "model_skipped";
181
+ }
182
+ else {
183
+ fallbackReason = "model_error";
184
+ mode = "model_skipped";
185
+ }
186
+ }
187
+ }
188
+ else {
189
+ fallbackReason = "budget_exceeded";
190
+ mode = "rules_only";
191
+ }
192
+ }
193
+ else {
194
+ fallbackReason = "model_port_unavailable";
195
+ mode = "rules_only";
196
+ }
197
+ // ─── 7. Merge ──────────────────────────────────────────────────────────────
198
+ const canonicalEntries = consolidation.entries.slice(0, options.maxCanonicalEntries ?? DEFAULT_MAX_CANONICAL);
199
+ const insights = modelResult?.insights ?? [];
200
+ const output = buildOutput({
201
+ runId,
202
+ inputMemoryStoreId: inputBundle.activeMemoryStoreId,
203
+ canonicalEntries,
204
+ insights,
205
+ narrativeUpdate: modelResult?.narrativeUpdate,
206
+ relationshipUpdate: modelResult?.relationshipUpdate,
207
+ validation: {
208
+ schemaValid: true,
209
+ sourceGrounded: true,
210
+ sensitivityClean: true,
211
+ unsupportedClaims: modelResult?.unsupportedClaims ?? [],
212
+ errors: [],
213
+ checkedAt: new Date().toISOString(),
214
+ },
215
+ });
216
+ // ─── 8. Validate ───────────────────────────────────────────────────────────
217
+ const toolExperienceIds = toolExperienceSummaries.map((t) => t.sourceRefs[0].sourceId);
218
+ const validation = validateDreamOutput({
219
+ output,
220
+ inputEvidenceIds: inputBundle.evidenceRefs,
221
+ inputChronicleIds: inputBundle.chronicleEntryIds,
222
+ inputToolExperienceIds: toolExperienceIds,
223
+ });
224
+ // Update output with validation result
225
+ output.validation = validation.validation;
226
+ let outputStatus = "candidate";
227
+ if (!validation.eligible) {
228
+ outputStatus = "archived";
229
+ // If model failed but rules produced something, mark partial
230
+ if (fallbackReason === "model_timeout") {
231
+ outputStatus = "partial";
232
+ }
233
+ }
234
+ output.status = outputStatus;
235
+ // ─── 9. Write output + lifecycle transition ──────────────────────────────────
236
+ await input.statePort.writeDreamOutput(output);
237
+ // DR-023: validation pass triggers accepted transition.
238
+ if (validation.eligible) {
239
+ try {
240
+ await input.statePort.markDreamOutputLifecycle({
241
+ outputId: output.outputId,
242
+ newStatus: "accepted",
243
+ validation: validation.validation,
244
+ updatedAt: new Date().toISOString(),
245
+ });
246
+ output.status = "accepted";
247
+ }
248
+ catch {
249
+ // Transition failed (e.g., concurrent modification); keep candidate in memory
250
+ // but DB remains candidate since transition was rolled back.
251
+ output.status = "candidate";
252
+ }
253
+ }
254
+ const finishedAt = new Date().toISOString();
255
+ const durationMs = new Date(finishedAt).getTime() - new Date(startedAt).getTime();
256
+ const trace = buildTrace({
257
+ traceId,
258
+ runId,
259
+ startedAt,
260
+ finishedAt,
261
+ durationMs,
262
+ inputCounts: inputBundle.inputCounts,
263
+ fallbackReason,
264
+ llmCostUsd,
265
+ validationErrors: validation.validation.errors,
266
+ timeoutMs: fallbackReason === "model_timeout" ? operatorTimeoutMs : undefined,
267
+ sensitivityFailure: false,
268
+ });
269
+ await input.tracePort?.recordDreamTrace(trace);
270
+ return {
271
+ runId,
272
+ status: "completed",
273
+ output,
274
+ trace,
275
+ fallbackReason,
276
+ };
277
+ }
278
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
279
+ function buildOutput(params) {
280
+ return {
281
+ outputId: `dream_output:${crypto.randomUUID()}`,
282
+ runId: params.runId,
283
+ status: "candidate",
284
+ inputMemoryStoreId: params.inputMemoryStoreId,
285
+ canonicalEntries: params.canonicalEntries,
286
+ insights: params.insights,
287
+ narrativeUpdate: params.narrativeUpdate,
288
+ relationshipUpdate: params.relationshipUpdate,
289
+ validation: params.validation,
290
+ };
291
+ }
292
+ function buildTrace(params) {
293
+ return {
294
+ traceId: params.traceId,
295
+ runId: params.runId,
296
+ startedAt: params.startedAt,
297
+ finishedAt: params.finishedAt ?? params.startedAt,
298
+ durationMs: params.durationMs ?? 0,
299
+ inputCounts: params.inputCounts,
300
+ fallbackReason: params.fallbackReason,
301
+ llmCostUsd: params.llmCostUsd,
302
+ validationErrors: params.validationErrors,
303
+ timeoutMs: params.timeoutMs,
304
+ sensitivityFailure: params.sensitivityFailure,
305
+ };
306
+ }