@haaaiawd/second-nature 0.1.38 → 0.1.39

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 (46) hide show
  1. package/agent-inner-guide.md +18 -0
  2. package/index.js +1270 -1262
  3. package/openclaw.plugin.json +29 -29
  4. package/package.json +55 -55
  5. package/runtime/cli/ops/heartbeat-surface.d.ts +75 -60
  6. package/runtime/cli/ops/heartbeat-surface.js +97 -83
  7. package/runtime/cli/ops/ops-router.js +1428 -1282
  8. package/runtime/cli/ops/workspace-heartbeat-runner.js +236 -191
  9. package/runtime/core/second-nature/guidance/apply-guidance.d.ts +12 -10
  10. package/runtime/core/second-nature/guidance/apply-guidance.js +15 -10
  11. package/runtime/core/second-nature/guidance/user-reply-continuity.d.ts +50 -50
  12. package/runtime/core/second-nature/guidance/user-reply-continuity.js +89 -80
  13. package/runtime/core/second-nature/runtime/service-entry.d.ts +39 -36
  14. package/runtime/core/second-nature/runtime/service-entry.js +44 -45
  15. package/runtime/dream/dream-engine.d.ts +14 -0
  16. package/runtime/dream/dream-engine.js +306 -0
  17. package/runtime/dream/dream-input-loader.d.ts +37 -0
  18. package/runtime/dream/dream-input-loader.js +155 -0
  19. package/runtime/dream/dream-scheduler.d.ts +75 -0
  20. package/runtime/dream/dream-scheduler.js +131 -0
  21. package/runtime/dream/index.d.ts +16 -0
  22. package/runtime/dream/index.js +14 -0
  23. package/runtime/dream/insight-extractor.d.ts +32 -0
  24. package/runtime/dream/insight-extractor.js +135 -0
  25. package/runtime/dream/memory-consolidator.d.ts +45 -0
  26. package/runtime/dream/memory-consolidator.js +140 -0
  27. package/runtime/dream/narrative-update-proposal.d.ts +34 -0
  28. package/runtime/dream/narrative-update-proposal.js +83 -0
  29. package/runtime/dream/output-validator.d.ts +20 -0
  30. package/runtime/dream/output-validator.js +110 -0
  31. package/runtime/dream/redaction-gate.d.ts +31 -0
  32. package/runtime/dream/redaction-gate.js +109 -0
  33. package/runtime/dream/relationship-update-proposal.d.ts +27 -0
  34. package/runtime/dream/relationship-update-proposal.js +119 -0
  35. package/runtime/dream/sampler.d.ts +30 -0
  36. package/runtime/dream/sampler.js +65 -0
  37. package/runtime/dream/types.d.ts +187 -0
  38. package/runtime/dream/types.js +11 -0
  39. package/runtime/guidance/fallback.js +20 -17
  40. package/runtime/guidance/guidance-assembler.js +76 -74
  41. package/runtime/guidance/output-guard.d.ts +13 -10
  42. package/runtime/guidance/output-guard.js +53 -29
  43. package/runtime/guidance/template-registry.d.ts +20 -16
  44. package/runtime/guidance/template-registry.js +123 -82
  45. package/runtime/guidance/types.d.ts +98 -84
  46. package/runtime/observability/projections/guidance-audit.js +38 -35
@@ -1,80 +1,89 @@
1
- import { buildMinimalGuidanceFallback } from "../../../guidance/fallback.js";
2
- import { selectPersonaSnippets } from "../../../guidance/persona-selection.js";
3
- import { getBaselineAtmosphereTemplate } from "../../../guidance/template-registry.js";
4
- /**
5
- * Scene context for user reply - uses a distinct scene type
6
- * to avoid confusion with platform reply scene.
7
- */
8
- export const USER_REPLY_SCENE_TYPE = "user_reply";
9
- /**
10
- * Build very light continuity guidance for direct user replies.
11
- *
12
- * Returns a minimal guidance payload with:
13
- * - Light atmosphere (continuity-focused)
14
- * - NO impulses (unlike platform reply scene)
15
- * - Optional persona reinforcement (1-2 snippets max)
16
- * - Minimal output guard (tone consistency only)
17
- */
18
- export async function buildLightReplyContinuity(input) {
19
- const sceneContext = {
20
- sceneType: "user_reply",
21
- mode: "active",
22
- riskLevel: "low",
23
- sceneSummary: "direct user reply continuity",
24
- };
25
- try {
26
- // Light atmosphere - continuity focused
27
- const atmosphereTemplate = getBaselineAtmosphereTemplate();
28
- const atmosphere = {
29
- kind: "atmosphere",
30
- text: `保持同一个人的语气。${input.replyContext.recentTone ? `最近语气参考:${input.replyContext.recentTone}` : "延续既有连续感。"}`,
31
- openness: "open",
32
- pressureLabels: ["user_reply", "continuity"],
33
- reviewStatus: atmosphereTemplate.reviewStatus,
34
- };
35
- // NO impulses for user reply - this is the key difference from platform reply
36
- const impulses = [];
37
- // Minimal persona reinforcement - only if candidates available
38
- let personaReinforcement = [];
39
- if (input.personaCandidates && input.personaCandidates.length > 0) {
40
- const personaDecision = selectPersonaSnippets({
41
- sceneContext,
42
- candidates: input.personaCandidates.slice(0, 2), // Max 2 snippets for light continuity
43
- });
44
- personaReinforcement = personaDecision.snippets;
45
- }
46
- // Minimal output guard - tone consistency only
47
- const outputGuard = {
48
- kind: "output_guard",
49
- constraints: [
50
- "保持对话语气,不要用帖子回复腔",
51
- "延续同一个人格连续性",
52
- ],
53
- hardGuardPriority: true,
54
- };
55
- return {
56
- scene: sceneContext,
57
- atmosphere,
58
- impulses,
59
- personaReinforcement,
60
- outputGuard,
61
- };
62
- }
63
- catch {
64
- // Fallback to minimal guidance
65
- return buildMinimalGuidanceFallback(sceneContext);
66
- }
67
- }
68
- /**
69
- * Check if an input should be classified as direct user reply.
70
- *
71
- * Classification criteria:
72
- * - Trigger source is user_reply
73
- * - Not a platform comment/reply
74
- * - Not an explicit task delegation
75
- */
76
- export function isDirectUserReply(input) {
77
- return (input.triggerSource === "user_reply" &&
78
- !input.isPlatformReply &&
79
- !input.isExplicitTask);
80
- }
1
+ import { buildMinimalGuidanceFallback } from "../../../guidance/fallback.js";
2
+ import { buildExpressionBoundary } from "../../../guidance/output-guard.js";
3
+ import { selectPersonaSnippets } from "../../../guidance/persona-selection.js";
4
+ import { getShortAtmosphereTemplate } from "../../../guidance/template-registry.js";
5
+ /**
6
+ * Scene context for user reply - uses a distinct scene type
7
+ * to avoid confusion with platform reply scene.
8
+ */
9
+ export const USER_REPLY_SCENE_TYPE = "user_reply";
10
+ /**
11
+ * Build very light continuity guidance for direct user replies.
12
+ *
13
+ * Returns a minimal guidance payload with:
14
+ * - Light atmosphere (continuity-focused)
15
+ * - NO impulses (unlike platform reply scene)
16
+ * - Optional persona reinforcement (1-2 snippets max)
17
+ * - Minimal expression boundary (tone consistency only)
18
+ */
19
+ export async function buildLightReplyContinuity(input) {
20
+ const sceneContext = {
21
+ sceneType: "user_reply",
22
+ mode: "active",
23
+ riskLevel: "low",
24
+ sceneSummary: "direct user reply continuity",
25
+ };
26
+ try {
27
+ // Light atmosphere - continuity focused (T-V7C.C.7: short constraint style)
28
+ const atmosphereTemplate = getShortAtmosphereTemplate(sceneContext.mode, sceneContext.riskLevel);
29
+ const atmosphere = {
30
+ kind: "atmosphere",
31
+ text: `保持同一个人的语气。${input.replyContext.recentTone ? `最近语气参考:${input.replyContext.recentTone}` : "延续既有连续感。"}`,
32
+ openness: "open",
33
+ pressureLabels: ["user_reply", "continuity"],
34
+ reviewStatus: atmosphereTemplate.reviewStatus,
35
+ };
36
+ // NO impulses for user reply - this is the key difference from platform reply
37
+ const impulses = [];
38
+ // Minimal persona reinforcement - only if candidates available
39
+ let personaReinforcement = [];
40
+ if (input.personaCandidates && input.personaCandidates.length > 0) {
41
+ const personaDecision = selectPersonaSnippets({
42
+ sceneContext,
43
+ candidates: input.personaCandidates.slice(0, 2), // Max 2 snippets for light continuity
44
+ });
45
+ personaReinforcement = personaDecision.snippets;
46
+ }
47
+ // Minimal expression boundary - tone consistency only (T-V7C.C.7)
48
+ const outputGuard = {
49
+ kind: "output_guard",
50
+ constraints: [
51
+ "保持对话语气,不要用帖子回复腔",
52
+ "延续同一个人格连续性",
53
+ ],
54
+ hardGuardPriority: true,
55
+ _semanticNote: "output_guard_only_shapes_expression",
56
+ };
57
+ const expressionBoundary = buildExpressionBoundary(sceneContext.sceneType);
58
+ // Override with user-reply-specific constraints
59
+ expressionBoundary.constraints = [
60
+ "保持对话语气,不要用帖子回复腔",
61
+ "延续同一个人格连续性",
62
+ ];
63
+ return {
64
+ scene: sceneContext,
65
+ atmosphere,
66
+ impulses,
67
+ personaReinforcement,
68
+ outputGuard,
69
+ expressionBoundary,
70
+ };
71
+ }
72
+ catch {
73
+ // Fallback to minimal guidance
74
+ return buildMinimalGuidanceFallback(sceneContext);
75
+ }
76
+ }
77
+ /**
78
+ * Check if an input should be classified as direct user reply.
79
+ *
80
+ * Classification criteria:
81
+ * - Trigger source is user_reply
82
+ * - Not a platform comment/reply
83
+ * - Not an explicit task delegation
84
+ */
85
+ export function isDirectUserReply(input) {
86
+ return (input.triggerSource === "user_reply" &&
87
+ !input.isPlatformReply &&
88
+ !input.isExplicitTask);
89
+ }
@@ -1,36 +1,39 @@
1
- /**
2
- * Second Nature Runtime Service Entry
3
- *
4
- * This module provides the actual implementation for the `second-nature-runtime` service.
5
- * It serves as the heartbeat host bridge candidate carrier and initializes the
6
- * minimal runtime state needed for the plugin to function.
7
- *
8
- * Per ADR-005: heartbeat is the free-rhythm main entry; this service provides
9
- * the stable runtime state that heartbeat rounds will interact with.
10
- */
11
- export interface RuntimeServiceContext {
12
- /** Workspace root path for state/observability databases */
13
- workspaceRoot?: string;
14
- /** Plugin configuration overrides */
15
- config?: Record<string, unknown>;
16
- }
17
- export interface RuntimeServiceHandle {
18
- /** Service is ready and accepting requests */
19
- ready: boolean;
20
- /** Runtime version string */
21
- version: string;
22
- /** Close the runtime handle and release resources */
23
- close: () => void;
24
- }
25
- /**
26
- * Start the Second Nature runtime service.
27
- *
28
- * This is the non-empty implementation that replaces the previous `start() { return; }` shell.
29
- * It initializes the minimal runtime state and returns a handle that can be used
30
- * by the heartbeat host bridge.
31
- */
32
- export declare function startRuntimeService(ctx?: RuntimeServiceContext): RuntimeServiceHandle;
33
- /**
34
- * Get the current runtime service handle, or null if not started.
35
- */
36
- export declare function getRuntimeHandle(): RuntimeServiceHandle | null;
1
+ /**
2
+ * Second Nature Runtime Service Entry
3
+ *
4
+ * This module provides the actual implementation for the `second-nature-runtime` service.
5
+ * It serves as the heartbeat host bridge candidate carrier and initializes the
6
+ * minimal runtime state needed for the plugin to function.
7
+ *
8
+ * Per ADR-005: heartbeat is the free-rhythm main entry; this service provides
9
+ * the stable runtime state that heartbeat rounds will interact with.
10
+ */
11
+ export interface RuntimeServiceContext {
12
+ /** Workspace root path for state/observability databases */
13
+ workspaceRoot?: string;
14
+ /** Plugin configuration overrides */
15
+ config?: Record<string, unknown>;
16
+ /** Runtime version — supplied by the plugin entry from its package manifest.
17
+ * Eliminates hard-coded version drift (previously `const version = "0.1.38"`). */
18
+ version?: string;
19
+ }
20
+ export interface RuntimeServiceHandle {
21
+ /** Service is ready and accepting requests */
22
+ ready: boolean;
23
+ /** Runtime version string */
24
+ version: string;
25
+ /** Close the runtime handle and release resources */
26
+ close: () => void;
27
+ }
28
+ /**
29
+ * Start the Second Nature runtime service.
30
+ *
31
+ * This is the non-empty implementation that replaces the previous `start() { return; }` shell.
32
+ * It initializes the minimal runtime state and returns a handle that can be used
33
+ * by the heartbeat host bridge.
34
+ */
35
+ export declare function startRuntimeService(ctx?: RuntimeServiceContext): RuntimeServiceHandle;
36
+ /**
37
+ * Get the current runtime service handle, or null if not started.
38
+ */
39
+ export declare function getRuntimeHandle(): RuntimeServiceHandle | null;
@@ -1,45 +1,44 @@
1
- /**
2
- * Second Nature Runtime Service Entry
3
- *
4
- * This module provides the actual implementation for the `second-nature-runtime` service.
5
- * It serves as the heartbeat host bridge candidate carrier and initializes the
6
- * minimal runtime state needed for the plugin to function.
7
- *
8
- * Per ADR-005: heartbeat is the free-rhythm main entry; this service provides
9
- * the stable runtime state that heartbeat rounds will interact with.
10
- */
11
- let activeHandle = null;
12
- /**
13
- * Start the Second Nature runtime service.
14
- *
15
- * This is the non-empty implementation that replaces the previous `start() { return; }` shell.
16
- * It initializes the minimal runtime state and returns a handle that can be used
17
- * by the heartbeat host bridge.
18
- */
19
- export function startRuntimeService(ctx) {
20
- if (activeHandle?.ready) {
21
- return activeHandle;
22
- }
23
- // Initialize minimal runtime state
24
- // In future iterations, this will connect to:
25
- // - state-system (SQLite database initialization)
26
- // - observability-system (event store setup)
27
- // - control-plane-system (heartbeat bridge preparation)
28
- const workspaceRoot = ctx?.workspaceRoot ?? process.cwd();
29
- /** Keep in sync with `plugin/package.json` when cutting releases. */
30
- const version = "0.1.21";
31
- activeHandle = {
32
- ready: true,
33
- version,
34
- close() {
35
- activeHandle = null;
36
- },
37
- };
38
- return activeHandle;
39
- }
40
- /**
41
- * Get the current runtime service handle, or null if not started.
42
- */
43
- export function getRuntimeHandle() {
44
- return activeHandle;
45
- }
1
+ /**
2
+ * Second Nature Runtime Service Entry
3
+ *
4
+ * This module provides the actual implementation for the `second-nature-runtime` service.
5
+ * It serves as the heartbeat host bridge candidate carrier and initializes the
6
+ * minimal runtime state needed for the plugin to function.
7
+ *
8
+ * Per ADR-005: heartbeat is the free-rhythm main entry; this service provides
9
+ * the stable runtime state that heartbeat rounds will interact with.
10
+ */
11
+ let activeHandle = null;
12
+ /**
13
+ * Start the Second Nature runtime service.
14
+ *
15
+ * This is the non-empty implementation that replaces the previous `start() { return; }` shell.
16
+ * It initializes the minimal runtime state and returns a handle that can be used
17
+ * by the heartbeat host bridge.
18
+ */
19
+ export function startRuntimeService(ctx) {
20
+ if (activeHandle?.ready) {
21
+ return activeHandle;
22
+ }
23
+ // Initialize minimal runtime state
24
+ // In future iterations, this will connect to:
25
+ // - state-system (SQLite database initialization)
26
+ // - observability-system (event store setup)
27
+ // - control-plane-system (heartbeat bridge preparation)
28
+ const workspaceRoot = ctx?.workspaceRoot ?? process.cwd();
29
+ const version = ctx?.version ?? "unknown";
30
+ activeHandle = {
31
+ ready: true,
32
+ version,
33
+ close() {
34
+ activeHandle = null;
35
+ },
36
+ };
37
+ return activeHandle;
38
+ }
39
+ /**
40
+ * Get the current runtime service handle, or null if not started.
41
+ */
42
+ export function getRuntimeHandle() {
43
+ return activeHandle;
44
+ }
@@ -0,0 +1,14 @@
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 type { DreamEngineInput, DreamRunResult } from "./types.js";
14
+ export declare function runDream(input: DreamEngineInput): Promise<DreamRunResult>;
@@ -0,0 +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
+ }