@haaaiawd/second-nature 0.1.38 → 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 (50) hide show
  1. package/agent-inner-guide.md +18 -0
  2. package/index.js +10 -2
  3. package/openclaw.plugin.json +2 -2
  4. package/package.json +1 -1
  5. package/runtime/cli/commands/connector-init.js +11 -4
  6. package/runtime/cli/index.js +6 -1
  7. package/runtime/cli/ops/heartbeat-surface.d.ts +15 -0
  8. package/runtime/cli/ops/heartbeat-surface.js +16 -2
  9. package/runtime/cli/ops/ops-router.js +229 -83
  10. package/runtime/cli/ops/workspace-heartbeat-runner.js +49 -4
  11. package/runtime/connectors/services/connector-executor-adapter.js +192 -41
  12. package/runtime/core/second-nature/guidance/apply-guidance.d.ts +2 -0
  13. package/runtime/core/second-nature/guidance/apply-guidance.js +6 -1
  14. package/runtime/core/second-nature/guidance/user-reply-continuity.d.ts +1 -1
  15. package/runtime/core/second-nature/guidance/user-reply-continuity.js +14 -5
  16. package/runtime/core/second-nature/orchestrator/intent-planner.js +15 -0
  17. package/runtime/core/second-nature/runtime/service-entry.d.ts +3 -0
  18. package/runtime/core/second-nature/runtime/service-entry.js +1 -2
  19. package/runtime/dream/dream-engine.d.ts +14 -0
  20. package/runtime/dream/dream-engine.js +306 -0
  21. package/runtime/dream/dream-input-loader.d.ts +37 -0
  22. package/runtime/dream/dream-input-loader.js +155 -0
  23. package/runtime/dream/dream-scheduler.d.ts +75 -0
  24. package/runtime/dream/dream-scheduler.js +131 -0
  25. package/runtime/dream/index.d.ts +16 -0
  26. package/runtime/dream/index.js +14 -0
  27. package/runtime/dream/insight-extractor.d.ts +32 -0
  28. package/runtime/dream/insight-extractor.js +135 -0
  29. package/runtime/dream/memory-consolidator.d.ts +45 -0
  30. package/runtime/dream/memory-consolidator.js +140 -0
  31. package/runtime/dream/narrative-update-proposal.d.ts +34 -0
  32. package/runtime/dream/narrative-update-proposal.js +83 -0
  33. package/runtime/dream/output-validator.d.ts +20 -0
  34. package/runtime/dream/output-validator.js +110 -0
  35. package/runtime/dream/redaction-gate.d.ts +31 -0
  36. package/runtime/dream/redaction-gate.js +109 -0
  37. package/runtime/dream/relationship-update-proposal.d.ts +27 -0
  38. package/runtime/dream/relationship-update-proposal.js +119 -0
  39. package/runtime/dream/sampler.d.ts +30 -0
  40. package/runtime/dream/sampler.js +65 -0
  41. package/runtime/dream/types.d.ts +187 -0
  42. package/runtime/dream/types.js +11 -0
  43. package/runtime/guidance/fallback.js +6 -3
  44. package/runtime/guidance/guidance-assembler.js +5 -3
  45. package/runtime/guidance/output-guard.d.ts +4 -1
  46. package/runtime/guidance/output-guard.js +24 -0
  47. package/runtime/guidance/template-registry.d.ts +5 -1
  48. package/runtime/guidance/template-registry.js +71 -30
  49. package/runtime/guidance/types.d.ts +14 -0
  50. package/runtime/observability/projections/guidance-audit.js +4 -1
@@ -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
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * DreamInputLoader — T-DQS.C.2 (DR-026: Idempotent Claim Loading)
3
+ *
4
+ * Core logic: Load unreferenced QuietClaims as Dream inputs.
5
+ *
6
+ * Idempotent mechanism (DR-026):
7
+ * - Queries daily_diary_index + life_evidence_index for candidate source refs.
8
+ * - Excludes refs already consumed by accepted dream_output_index projections.
9
+ * - When Dream lock is held, claims are queued; on next Dream run after lock release,
10
+ * they are automatically included (no separate "skipped" tracking needed).
11
+ * - Subsequent Dream runs exclude already-accepted refs, preventing re-processing.
12
+ *
13
+ * Lock semantics:
14
+ * - Lock TTL is 35min (enforced by DreamScheduler, not this module).
15
+ * - This loader only reads; lock enforcement is upstream.
16
+ *
17
+ * ToolExperience summaries:
18
+ * - Loads recent tool_experience records aggregated by (connector_id, capability_id, outcome).
19
+ * - Provides frequency count and last recorded time for Dream insight extraction.
20
+ *
21
+ * Contract:
22
+ * - Returns empty evidenceRefs when no unreferenced claims exist.
23
+ * - Never fabricates inputs; only reads from existing DB state.
24
+ *
25
+ * Performance: O(n) where n = life_evidence_index rows (capped by LIMIT).
26
+ * Memory: O(m) where m = deduplicated ref count (typically < evidenceLimit).
27
+ *
28
+ * Test coverage: tests/unit/dream/dream-input-loader.test.ts
29
+ */
30
+ import type { Database } from "sql.js";
31
+ import type { DreamStatePort } from "./types.js";
32
+ export interface DreamInputLoaderOptions {
33
+ database: {
34
+ sqlite: Database;
35
+ };
36
+ }
37
+ export declare function createDreamInputLoader(options: DreamInputLoaderOptions): Pick<DreamStatePort, "loadDreamInputs">;
@@ -0,0 +1,155 @@
1
+ /**
2
+ * DreamInputLoader — T-DQS.C.2 (DR-026: Idempotent Claim Loading)
3
+ *
4
+ * Core logic: Load unreferenced QuietClaims as Dream inputs.
5
+ *
6
+ * Idempotent mechanism (DR-026):
7
+ * - Queries daily_diary_index + life_evidence_index for candidate source refs.
8
+ * - Excludes refs already consumed by accepted dream_output_index projections.
9
+ * - When Dream lock is held, claims are queued; on next Dream run after lock release,
10
+ * they are automatically included (no separate "skipped" tracking needed).
11
+ * - Subsequent Dream runs exclude already-accepted refs, preventing re-processing.
12
+ *
13
+ * Lock semantics:
14
+ * - Lock TTL is 35min (enforced by DreamScheduler, not this module).
15
+ * - This loader only reads; lock enforcement is upstream.
16
+ *
17
+ * ToolExperience summaries:
18
+ * - Loads recent tool_experience records aggregated by (connector_id, capability_id, outcome).
19
+ * - Provides frequency count and last recorded time for Dream insight extraction.
20
+ *
21
+ * Contract:
22
+ * - Returns empty evidenceRefs when no unreferenced claims exist.
23
+ * - Never fabricates inputs; only reads from existing DB state.
24
+ *
25
+ * Performance: O(n) where n = life_evidence_index rows (capped by LIMIT).
26
+ * Memory: O(m) where m = deduplicated ref count (typically < evidenceLimit).
27
+ *
28
+ * Test coverage: tests/unit/dream/dream-input-loader.test.ts
29
+ */
30
+ function safeParseJson(json, fallback) {
31
+ try {
32
+ const parsed = JSON.parse(json);
33
+ return parsed ?? fallback;
34
+ }
35
+ catch {
36
+ return fallback;
37
+ }
38
+ }
39
+ /** Extract ref ids from JSON that may be string[] or {id?, sourceId?}[] */
40
+ function extractRefIdsFromJson(json) {
41
+ const parsed = safeParseJson(json, []);
42
+ if (!Array.isArray(parsed))
43
+ return [];
44
+ const ids = [];
45
+ for (const item of parsed) {
46
+ if (typeof item === "string") {
47
+ ids.push(item);
48
+ }
49
+ else if (item && typeof item === "object" && !Array.isArray(item)) {
50
+ const obj = item;
51
+ if (typeof obj.id === "string")
52
+ ids.push(obj.id);
53
+ if (typeof obj.sourceId === "string")
54
+ ids.push(obj.sourceId);
55
+ }
56
+ }
57
+ return ids;
58
+ }
59
+ /** Extract consumed ref ids from canonical_entries_json (CanonicalMemoryEntry[]) */
60
+ function extractConsumedRefIdsFromEntriesJson(json) {
61
+ const entries = safeParseJson(json, []);
62
+ if (!Array.isArray(entries))
63
+ return [];
64
+ const ids = [];
65
+ for (const entry of entries) {
66
+ if (entry && typeof entry === "object" && !Array.isArray(entry)) {
67
+ const sourceRefs = entry.sourceRefs;
68
+ if (Array.isArray(sourceRefs)) {
69
+ for (const sr of sourceRefs) {
70
+ if (typeof sr === "string") {
71
+ ids.push(sr);
72
+ }
73
+ else if (sr && typeof sr === "object" && !Array.isArray(sr)) {
74
+ const obj = sr;
75
+ if (typeof obj.sourceId === "string")
76
+ ids.push(obj.sourceId);
77
+ if (typeof obj.id === "string")
78
+ ids.push(obj.id);
79
+ }
80
+ }
81
+ }
82
+ }
83
+ }
84
+ return ids;
85
+ }
86
+ export function createDreamInputLoader(options) {
87
+ const { sqlite } = options.database;
88
+ return {
89
+ // async aligns with DreamStatePort.loadDreamInputs signature (Promise<DreamInputBundle>).
90
+ // All current operations are synchronous (sql.js in-memory), but the contract
91
+ // reserves the right to use async DB drivers in the future.
92
+ async loadDreamInputs(query = {}) {
93
+ // Defaults from 05A_TASKS.md T-DQS.C.2 and dream-quiet-system.md §10.2
94
+ const timeWindowDays = query.timeWindowDays ?? 30;
95
+ const evidenceLimit = query.evidenceLimit ?? 1000;
96
+ const since = new Date(Date.now() - timeWindowDays * 24 * 60 * 60 * 1000).toISOString();
97
+ // ─── 1. Collect candidate ref ids from daily_diary_index ─────────────────
98
+ const candidateRefs = new Set();
99
+ const diaryResult = sqlite.exec(`SELECT source_refs_json FROM daily_diary_index WHERE created_at >= ? ORDER BY created_at DESC`, [since]);
100
+ for (const row of diaryResult[0]?.values ?? []) {
101
+ for (const id of extractRefIdsFromJson(String(row[0]))) {
102
+ candidateRefs.add(id);
103
+ }
104
+ }
105
+ // ─── 2. Collect candidate ref ids from life_evidence_index ───────────────
106
+ const evidenceResult = sqlite.exec(`SELECT source_refs_json FROM life_evidence_index WHERE timestamp >= ? ORDER BY timestamp DESC LIMIT ?`, [since, evidenceLimit]);
107
+ for (const row of evidenceResult[0]?.values ?? []) {
108
+ for (const id of extractRefIdsFromJson(String(row[0]))) {
109
+ candidateRefs.add(id);
110
+ }
111
+ }
112
+ // ─── 3. Collect consumed refs from accepted dream outputs ────────────────
113
+ const consumedRefs = new Set();
114
+ const acceptedResult = sqlite.exec(`SELECT canonical_entries_json FROM dream_output_index WHERE status = 'accepted'`);
115
+ for (const row of acceptedResult[0]?.values ?? []) {
116
+ for (const id of extractConsumedRefIdsFromEntriesJson(String(row[0]))) {
117
+ consumedRefs.add(id);
118
+ }
119
+ }
120
+ // ─── 4. Filter: keep only refs not consumed by accepted projections ──────
121
+ const evidenceRefs = [...candidateRefs].filter((ref) => !consumedRefs.has(ref));
122
+ // ─── 5. Load ToolExperience summaries (aggregated by connector/capability/outcome) ─
123
+ const toolExpResult = sqlite.exec(`SELECT connector_id, capability_id, outcome, COUNT(*) as count, MAX(created_at) as last_recorded_at
124
+ FROM tool_experience
125
+ WHERE created_at >= ?
126
+ GROUP BY connector_id, capability_id, outcome
127
+ ORDER BY last_recorded_at DESC
128
+ LIMIT ?`, [since, evidenceLimit]);
129
+ const toolExperienceSummaries = [];
130
+ for (const row of toolExpResult[0]?.values ?? []) {
131
+ toolExperienceSummaries.push({
132
+ connectorId: String(row[0]),
133
+ capabilityId: String(row[1]),
134
+ outcome: String(row[2]),
135
+ count: Number(row[3]),
136
+ lastRecordedAt: String(row[4]),
137
+ });
138
+ }
139
+ return {
140
+ evidenceRefs,
141
+ chronicleEntryIds: [],
142
+ activeMemoryStoreId: undefined,
143
+ narrativeSnapshotId: undefined,
144
+ relationshipSnapshotId: undefined,
145
+ goalSnapshotIds: [],
146
+ toolExperienceSummaries,
147
+ inputCounts: {
148
+ evidence: evidenceRefs.length,
149
+ chronicle: 0, // T-DQS.C.2 scope: evidence only; chronicle loaded separately
150
+ memoryEntries: 0, // T-DQS.C.2 scope: evidence only; memory loaded separately
151
+ },
152
+ };
153
+ },
154
+ };
155
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Dream Scheduler
3
+ *
4
+ * Core logic: trigger Dream runs via cron schedule, evidence threshold, or manual
5
+ * request. Uses DreamRunLock to prevent concurrent runs on the same workspace/input
6
+ * window. Operator timeout is enforced via options in the engine call.
7
+ *
8
+ * - Cron: simplified — checks if a scheduled window is due (last run + interval).
9
+ * - Evidence threshold: triggers when evidence count exceeds threshold since last run.
10
+ * - Manual: always allowed if no active lock.
11
+ * - Lock: in-memory or port-backed; released after run completes or times out.
12
+ * Test coverage: tests/integration/dream/t7-1-2-dream-scheduler.test.ts
13
+ */
14
+ import type { DreamEngineInput } from "./types.js";
15
+ import type { DreamTriggerKind, DreamTracePort, DreamStatePort, DreamModelPort, DreamBudgetPort, ModelAssistPort } from "./types.js";
16
+ export interface SchedulerInput {
17
+ triggerKind: DreamTriggerKind;
18
+ runId: string;
19
+ traceId: string;
20
+ statePort: DreamStatePort;
21
+ /** @deprecated Use modelAssistPort (DR-027). */
22
+ modelPort?: DreamModelPort;
23
+ modelAssistPort?: ModelAssistPort;
24
+ tracePort?: DreamTracePort;
25
+ budgetPort?: DreamBudgetPort;
26
+ options?: DreamEngineInput["options"];
27
+ lockPort?: DreamRunLockPort;
28
+ windowKey?: string;
29
+ }
30
+ export interface DreamRunLockPort {
31
+ acquireLock(input: {
32
+ runId: string;
33
+ windowKey: string;
34
+ ttlMs: number;
35
+ }): Promise<{
36
+ acquired: boolean;
37
+ existingRunId?: string;
38
+ }>;
39
+ releaseLock(input: {
40
+ runId: string;
41
+ windowKey: string;
42
+ }): Promise<void>;
43
+ }
44
+ export interface ScheduleResult {
45
+ runId: string;
46
+ status: "started" | "skipped" | "queued";
47
+ reason?: string;
48
+ }
49
+ export declare function memoryLockPort(): DreamRunLockPort;
50
+ export declare function scheduleDream(input: SchedulerInput): Promise<ScheduleResult>;
51
+ export interface CronPolicy {
52
+ type: "cron";
53
+ intervalHours: number;
54
+ lastRunAt?: string;
55
+ }
56
+ export interface EvidenceThresholdPolicy {
57
+ type: "evidence_threshold";
58
+ threshold: number;
59
+ currentEvidenceCount: number;
60
+ lastRunEvidenceCount: number;
61
+ }
62
+ export interface ManualPolicy {
63
+ type: "manual";
64
+ }
65
+ export interface QuietCompletionPolicy {
66
+ type: "quiet_completion";
67
+ quietCompletedAt: string;
68
+ windowStartHour: number;
69
+ windowEndHour: number;
70
+ }
71
+ export type TriggerPolicy = CronPolicy | EvidenceThresholdPolicy | ManualPolicy | QuietCompletionPolicy;
72
+ export declare function shouldTrigger(policy: TriggerPolicy): {
73
+ shouldRun: boolean;
74
+ reason?: string;
75
+ };