@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.
- package/agent-inner-guide.md +18 -0
- package/index.js +1270 -1262
- package/openclaw.plugin.json +29 -29
- package/package.json +55 -55
- package/runtime/cli/ops/heartbeat-surface.d.ts +75 -60
- package/runtime/cli/ops/heartbeat-surface.js +97 -83
- package/runtime/cli/ops/ops-router.js +1428 -1282
- package/runtime/cli/ops/workspace-heartbeat-runner.js +236 -191
- package/runtime/core/second-nature/guidance/apply-guidance.d.ts +12 -10
- package/runtime/core/second-nature/guidance/apply-guidance.js +15 -10
- package/runtime/core/second-nature/guidance/user-reply-continuity.d.ts +50 -50
- package/runtime/core/second-nature/guidance/user-reply-continuity.js +89 -80
- package/runtime/core/second-nature/runtime/service-entry.d.ts +39 -36
- package/runtime/core/second-nature/runtime/service-entry.js +44 -45
- package/runtime/dream/dream-engine.d.ts +14 -0
- package/runtime/dream/dream-engine.js +306 -0
- package/runtime/dream/dream-input-loader.d.ts +37 -0
- package/runtime/dream/dream-input-loader.js +155 -0
- package/runtime/dream/dream-scheduler.d.ts +75 -0
- package/runtime/dream/dream-scheduler.js +131 -0
- package/runtime/dream/index.d.ts +16 -0
- package/runtime/dream/index.js +14 -0
- package/runtime/dream/insight-extractor.d.ts +32 -0
- package/runtime/dream/insight-extractor.js +135 -0
- package/runtime/dream/memory-consolidator.d.ts +45 -0
- package/runtime/dream/memory-consolidator.js +140 -0
- package/runtime/dream/narrative-update-proposal.d.ts +34 -0
- package/runtime/dream/narrative-update-proposal.js +83 -0
- package/runtime/dream/output-validator.d.ts +20 -0
- package/runtime/dream/output-validator.js +110 -0
- package/runtime/dream/redaction-gate.d.ts +31 -0
- package/runtime/dream/redaction-gate.js +109 -0
- package/runtime/dream/relationship-update-proposal.d.ts +27 -0
- package/runtime/dream/relationship-update-proposal.js +119 -0
- package/runtime/dream/sampler.d.ts +30 -0
- package/runtime/dream/sampler.js +65 -0
- package/runtime/dream/types.d.ts +187 -0
- package/runtime/dream/types.js +11 -0
- package/runtime/guidance/fallback.js +20 -17
- package/runtime/guidance/guidance-assembler.js +76 -74
- package/runtime/guidance/output-guard.d.ts +13 -10
- package/runtime/guidance/output-guard.js +53 -29
- package/runtime/guidance/template-registry.d.ts +20 -16
- package/runtime/guidance/template-registry.js +123 -82
- package/runtime/guidance/types.d.ts +98 -84
- package/runtime/observability/projections/guidance-audit.js +38 -35
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
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 { runDream } from "./dream-engine.js";
|
|
15
|
+
const DEFAULT_LOCK_TTL_MS = 35 * 60 * 1000; // 35 min (covers 30 min operator timeout + buffer)
|
|
16
|
+
// In-memory lock fallback when no lockPort is provided
|
|
17
|
+
export function memoryLockPort() {
|
|
18
|
+
const locks = new Map();
|
|
19
|
+
return {
|
|
20
|
+
async acquireLock(input) {
|
|
21
|
+
const key = input.windowKey;
|
|
22
|
+
const existing = locks.get(key);
|
|
23
|
+
if (existing && existing.expiresAt > Date.now()) {
|
|
24
|
+
return { acquired: false, existingRunId: existing.runId };
|
|
25
|
+
}
|
|
26
|
+
locks.set(key, {
|
|
27
|
+
runId: input.runId,
|
|
28
|
+
expiresAt: Date.now() + input.ttlMs,
|
|
29
|
+
});
|
|
30
|
+
return { acquired: true };
|
|
31
|
+
},
|
|
32
|
+
async releaseLock(input) {
|
|
33
|
+
const key = input.windowKey;
|
|
34
|
+
const existing = locks.get(key);
|
|
35
|
+
if (existing && existing.runId === input.runId) {
|
|
36
|
+
locks.delete(key);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export async function scheduleDream(input) {
|
|
42
|
+
const lock = input.lockPort ?? memoryLockPort();
|
|
43
|
+
const windowKey = input.windowKey ?? "dream_lock:default";
|
|
44
|
+
// Acquire lock
|
|
45
|
+
const lockResult = await lock.acquireLock({
|
|
46
|
+
runId: input.runId,
|
|
47
|
+
windowKey,
|
|
48
|
+
ttlMs: DEFAULT_LOCK_TTL_MS,
|
|
49
|
+
});
|
|
50
|
+
if (!lockResult.acquired) {
|
|
51
|
+
const reason = input.triggerKind === "quiet_completion"
|
|
52
|
+
? "skip:lock_held"
|
|
53
|
+
: `lock_held_by:${lockResult.existingRunId ?? "unknown"}`;
|
|
54
|
+
return {
|
|
55
|
+
runId: input.runId,
|
|
56
|
+
status: "skipped",
|
|
57
|
+
reason,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// Run Dream asynchronously; do not await so heartbeat is not blocked
|
|
61
|
+
runDream({
|
|
62
|
+
runId: input.runId,
|
|
63
|
+
traceId: input.traceId,
|
|
64
|
+
triggerKind: input.triggerKind,
|
|
65
|
+
statePort: input.statePort,
|
|
66
|
+
modelPort: input.modelPort,
|
|
67
|
+
modelAssistPort: input.modelAssistPort,
|
|
68
|
+
tracePort: input.tracePort,
|
|
69
|
+
budgetPort: input.budgetPort,
|
|
70
|
+
options: input.options,
|
|
71
|
+
})
|
|
72
|
+
.then(async (result) => {
|
|
73
|
+
await lock.releaseLock({ runId: input.runId, windowKey });
|
|
74
|
+
return result;
|
|
75
|
+
})
|
|
76
|
+
.catch(async (err) => {
|
|
77
|
+
console.error("[dream-scheduler] runDream failed:", err);
|
|
78
|
+
await lock.releaseLock({ runId: input.runId, windowKey });
|
|
79
|
+
});
|
|
80
|
+
return {
|
|
81
|
+
runId: input.runId,
|
|
82
|
+
status: "started",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function isHourInWindow(hour, start, end) {
|
|
86
|
+
if (start < end) {
|
|
87
|
+
return hour >= start && hour < end;
|
|
88
|
+
}
|
|
89
|
+
return hour >= start || hour < end;
|
|
90
|
+
}
|
|
91
|
+
export function shouldTrigger(policy) {
|
|
92
|
+
switch (policy.type) {
|
|
93
|
+
case "cron": {
|
|
94
|
+
if (!policy.lastRunAt) {
|
|
95
|
+
return { shouldRun: true, reason: "first_run" };
|
|
96
|
+
}
|
|
97
|
+
const last = new Date(policy.lastRunAt).getTime();
|
|
98
|
+
const next = last + policy.intervalHours * 60 * 60 * 1000;
|
|
99
|
+
if (Date.now() >= next) {
|
|
100
|
+
return { shouldRun: true, reason: "cron_due" };
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
shouldRun: false,
|
|
104
|
+
reason: `next_run_at:${new Date(next).toISOString()}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
case "evidence_threshold": {
|
|
108
|
+
const delta = policy.currentEvidenceCount - policy.lastRunEvidenceCount;
|
|
109
|
+
if (delta >= policy.threshold) {
|
|
110
|
+
return {
|
|
111
|
+
shouldRun: true,
|
|
112
|
+
reason: `threshold_reached:${delta}/${policy.threshold}`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
shouldRun: false,
|
|
117
|
+
reason: `below_threshold:${delta}/${policy.threshold}`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
case "manual": {
|
|
121
|
+
return { shouldRun: true, reason: "manual_trigger" };
|
|
122
|
+
}
|
|
123
|
+
case "quiet_completion": {
|
|
124
|
+
const hour = new Date(policy.quietCompletedAt).getUTCHours();
|
|
125
|
+
if (isHourInWindow(hour, policy.windowStartHour, policy.windowEndHour)) {
|
|
126
|
+
return { shouldRun: true, reason: "quiet_completion_in_window" };
|
|
127
|
+
}
|
|
128
|
+
return { shouldRun: false, reason: "skip:out_of_window" };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dream System public API.
|
|
3
|
+
*/
|
|
4
|
+
export * from "./types.js";
|
|
5
|
+
export { consolidateMemory } from "./memory-consolidator.js";
|
|
6
|
+
export { sampleDreamInput } from "./sampler.js";
|
|
7
|
+
export { redactDreamInput, redactBundle } from "./redaction-gate.js";
|
|
8
|
+
export { validateDreamOutput } from "./output-validator.js";
|
|
9
|
+
export { runDream } from "./dream-engine.js";
|
|
10
|
+
export { scheduleDream, shouldTrigger, memoryLockPort } from "./dream-scheduler.js";
|
|
11
|
+
export type { SchedulerInput, DreamRunLockPort, ScheduleResult, CronPolicy, EvidenceThresholdPolicy, ManualPolicy, QuietCompletionPolicy, TriggerPolicy } from "./dream-scheduler.js";
|
|
12
|
+
export { extractInsights } from "./insight-extractor.js";
|
|
13
|
+
export { draftNarrativeFromDream } from "./narrative-update-proposal.js";
|
|
14
|
+
export { draftRelationshipFromDream } from "./relationship-update-proposal.js";
|
|
15
|
+
export { createDreamInputLoader } from "./dream-input-loader.js";
|
|
16
|
+
export type { DreamInputLoaderOptions } from "./dream-input-loader.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dream System public API.
|
|
3
|
+
*/
|
|
4
|
+
export * from "./types.js";
|
|
5
|
+
export { consolidateMemory } from "./memory-consolidator.js";
|
|
6
|
+
export { sampleDreamInput } from "./sampler.js";
|
|
7
|
+
export { redactDreamInput, redactBundle } from "./redaction-gate.js";
|
|
8
|
+
export { validateDreamOutput } from "./output-validator.js";
|
|
9
|
+
export { runDream } from "./dream-engine.js";
|
|
10
|
+
export { scheduleDream, shouldTrigger, memoryLockPort } from "./dream-scheduler.js";
|
|
11
|
+
export { extractInsights } from "./insight-extractor.js";
|
|
12
|
+
export { draftNarrativeFromDream } from "./narrative-update-proposal.js";
|
|
13
|
+
export { draftRelationshipFromDream } from "./relationship-update-proposal.js";
|
|
14
|
+
export { createDreamInputLoader } from "./dream-input-loader.js";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Insight Extractor
|
|
3
|
+
*
|
|
4
|
+
* Core logic: extract source-grounded insight candidates from sampled evidence.
|
|
5
|
+
* Rules-based (no LLM required) using keyword patterns, frequency counts, and
|
|
6
|
+
* temporal clustering. Each insight carries type, summary, sourceRefs, and confidence.
|
|
7
|
+
*
|
|
8
|
+
* - Pattern: recurring themes across multiple evidence entries.
|
|
9
|
+
* - Learning: new skills or behaviors observed over time.
|
|
10
|
+
* - Observation: one-off notable events.
|
|
11
|
+
* - Conflict: contradictory claims or repeated failures.
|
|
12
|
+
* Test coverage: tests/unit/dream/t7-1-3-insight-extraction.test.ts
|
|
13
|
+
*/
|
|
14
|
+
import type { DreamInsight } from "./types.js";
|
|
15
|
+
export interface ExtractInsightsInput {
|
|
16
|
+
evidenceSummaries: Array<{
|
|
17
|
+
id: string;
|
|
18
|
+
summary: string;
|
|
19
|
+
createdAt: string;
|
|
20
|
+
kind?: string;
|
|
21
|
+
}>;
|
|
22
|
+
chronicleSummaries: Array<{
|
|
23
|
+
id: string;
|
|
24
|
+
summary: string;
|
|
25
|
+
createdAt: string;
|
|
26
|
+
}>;
|
|
27
|
+
redacted?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare function extractInsights(input: ExtractInsightsInput): {
|
|
30
|
+
insights: DreamInsight[];
|
|
31
|
+
unsupportedClaims: string[];
|
|
32
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Insight Extractor
|
|
3
|
+
*
|
|
4
|
+
* Core logic: extract source-grounded insight candidates from sampled evidence.
|
|
5
|
+
* Rules-based (no LLM required) using keyword patterns, frequency counts, and
|
|
6
|
+
* temporal clustering. Each insight carries type, summary, sourceRefs, and confidence.
|
|
7
|
+
*
|
|
8
|
+
* - Pattern: recurring themes across multiple evidence entries.
|
|
9
|
+
* - Learning: new skills or behaviors observed over time.
|
|
10
|
+
* - Observation: one-off notable events.
|
|
11
|
+
* - Conflict: contradictory claims or repeated failures.
|
|
12
|
+
* Test coverage: tests/unit/dream/t7-1-3-insight-extraction.test.ts
|
|
13
|
+
*/
|
|
14
|
+
// Keywords that indicate learning/development
|
|
15
|
+
const LEARNING_KEYWORDS = [
|
|
16
|
+
"learned", "learn", "figured out", "discovered", "understood",
|
|
17
|
+
"new approach", "different way", "better method", "improved",
|
|
18
|
+
];
|
|
19
|
+
// Keywords that indicate recurring patterns
|
|
20
|
+
const PATTERN_KEYWORDS = [
|
|
21
|
+
"again", "regularly", "routine", "habit", "consistently",
|
|
22
|
+
"every", "frequently", "pattern", "trend",
|
|
23
|
+
];
|
|
24
|
+
// Keywords that indicate conflict or failure
|
|
25
|
+
const CONFLICT_KEYWORDS = [
|
|
26
|
+
"failed", "failure", "error", "bug", "broke", "broken",
|
|
27
|
+
"conflict", "contradict", "disagree", "mismatch", "unexpected",
|
|
28
|
+
"not working", "does not work", "issue", "problem",
|
|
29
|
+
];
|
|
30
|
+
function countKeywordMatches(text, keywords) {
|
|
31
|
+
const lower = text.toLowerCase();
|
|
32
|
+
return keywords.reduce((count, kw) => {
|
|
33
|
+
const regex = new RegExp(kw.replace(/\s+/g, "\\s+"), "g");
|
|
34
|
+
const matches = lower.match(regex);
|
|
35
|
+
return count + (matches ? matches.length : 0);
|
|
36
|
+
}, 0);
|
|
37
|
+
}
|
|
38
|
+
function groupByDay(items) {
|
|
39
|
+
const groups = new Map();
|
|
40
|
+
for (const item of items) {
|
|
41
|
+
const day = item.createdAt.slice(0, 10);
|
|
42
|
+
const existing = groups.get(day) ?? [];
|
|
43
|
+
existing.push(item.summary);
|
|
44
|
+
groups.set(day, existing);
|
|
45
|
+
}
|
|
46
|
+
return groups;
|
|
47
|
+
}
|
|
48
|
+
function findRecurringThemes(items) {
|
|
49
|
+
const themeCounts = new Map();
|
|
50
|
+
for (const item of items) {
|
|
51
|
+
const words = item.summary.toLowerCase().split(/\s+/).filter((w) => w.length > 4);
|
|
52
|
+
for (const word of words) {
|
|
53
|
+
const entry = themeCounts.get(word) ?? { count: 0, sourceIds: [] };
|
|
54
|
+
entry.count++;
|
|
55
|
+
if (!entry.sourceIds.includes(item.id)) {
|
|
56
|
+
entry.sourceIds.push(item.id);
|
|
57
|
+
}
|
|
58
|
+
themeCounts.set(word, entry);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return Array.from(themeCounts.entries())
|
|
62
|
+
.filter(([, v]) => v.count >= 3 && v.sourceIds.length >= 2)
|
|
63
|
+
.map(([theme, v]) => ({ theme, count: v.count, sourceIds: v.sourceIds }))
|
|
64
|
+
.sort((a, b) => b.count - a.count)
|
|
65
|
+
.slice(0, 5);
|
|
66
|
+
}
|
|
67
|
+
export function extractInsights(input) {
|
|
68
|
+
const allItems = [
|
|
69
|
+
...input.evidenceSummaries.map((e) => ({ ...e, origin: "evidence" })),
|
|
70
|
+
...input.chronicleSummaries.map((c) => ({ ...c, origin: "chronicle" })),
|
|
71
|
+
];
|
|
72
|
+
const insights = [];
|
|
73
|
+
const unsupportedClaims = [];
|
|
74
|
+
if (allItems.length === 0) {
|
|
75
|
+
return { insights: [], unsupportedClaims: ["no_evidence_for_insight"] };
|
|
76
|
+
}
|
|
77
|
+
// 1. Pattern insights — recurring themes
|
|
78
|
+
const themes = findRecurringThemes(allItems);
|
|
79
|
+
for (const theme of themes) {
|
|
80
|
+
insights.push({
|
|
81
|
+
id: `insight:pattern:${theme.theme}:${crypto.randomUUID()}`,
|
|
82
|
+
type: "pattern",
|
|
83
|
+
summary: `Recurring theme "${theme.theme}" observed across ${theme.sourceIds.length} entries`,
|
|
84
|
+
sourceRefs: theme.sourceIds.slice(0, 10),
|
|
85
|
+
confidence: Math.min(0.95, 0.5 + theme.count * 0.05),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
// 2. Learning insights — entries with learning keywords
|
|
89
|
+
const learningItems = allItems.filter((item) => countKeywordMatches(item.summary, LEARNING_KEYWORDS) > 0);
|
|
90
|
+
if (learningItems.length > 0) {
|
|
91
|
+
insights.push({
|
|
92
|
+
id: `insight:learning:${crypto.randomUUID()}`,
|
|
93
|
+
type: "learning",
|
|
94
|
+
summary: `Learning observed in ${learningItems.length} entries: ${learningItems[0].summary.slice(0, 60)}...`,
|
|
95
|
+
sourceRefs: learningItems.map((i) => i.id).slice(0, 10),
|
|
96
|
+
confidence: Math.min(0.9, 0.4 + learningItems.length * 0.1),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
// 3. Conflict insights — entries with conflict/failure keywords
|
|
100
|
+
const conflictItems = allItems.filter((item) => countKeywordMatches(item.summary, CONFLICT_KEYWORDS) > 0);
|
|
101
|
+
if (conflictItems.length >= 2) {
|
|
102
|
+
insights.push({
|
|
103
|
+
id: `insight:conflict:${crypto.randomUUID()}`,
|
|
104
|
+
type: "conflict",
|
|
105
|
+
summary: `Repeated issues observed in ${conflictItems.length} entries`,
|
|
106
|
+
sourceRefs: conflictItems.map((i) => i.id).slice(0, 10),
|
|
107
|
+
confidence: Math.min(0.85, 0.5 + conflictItems.length * 0.05),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
// 4. Observation insights — notable one-off events (high-activity days)
|
|
111
|
+
const byDay = groupByDay(allItems);
|
|
112
|
+
let maxDay = "";
|
|
113
|
+
let maxCount = 0;
|
|
114
|
+
for (const [day, summaries] of byDay) {
|
|
115
|
+
if (summaries.length > maxCount) {
|
|
116
|
+
maxCount = summaries.length;
|
|
117
|
+
maxDay = day;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (maxCount >= 3 && maxDay) {
|
|
121
|
+
const dayItems = allItems.filter((i) => i.createdAt.startsWith(maxDay));
|
|
122
|
+
insights.push({
|
|
123
|
+
id: `insight:observation:${crypto.randomUUID()}`,
|
|
124
|
+
type: "observation",
|
|
125
|
+
summary: `High activity day (${maxDay}): ${maxCount} notable events`,
|
|
126
|
+
sourceRefs: dayItems.map((i) => i.id).slice(0, 10),
|
|
127
|
+
confidence: Math.min(0.8, 0.4 + maxCount * 0.05),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
// If no insights extracted, record unsupported claim
|
|
131
|
+
if (insights.length === 0) {
|
|
132
|
+
unsupportedClaims.push("no_insight_patterns_detected");
|
|
133
|
+
}
|
|
134
|
+
return { insights, unsupportedClaims };
|
|
135
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rules-based memory consolidation.
|
|
3
|
+
*
|
|
4
|
+
* Core logic: dedupe, merge, stale cleanup, and conflict marking on evidence,
|
|
5
|
+
* chronicle, and existing memory entries. No LLM required.
|
|
6
|
+
*
|
|
7
|
+
* - Deduplicate by sourceRef id + kind; keep the most recent.
|
|
8
|
+
* - Merge entries with same kind + similar summary (naive prefix match).
|
|
9
|
+
* - Mark entries older than 90 days as stale (retain but flag).
|
|
10
|
+
* - Mark entries with conflicting sourceRefs as conflict.
|
|
11
|
+
* Test coverage: tests/integration/dream/t7-1-1-dream-pipeline.test.ts
|
|
12
|
+
*/
|
|
13
|
+
import type { CanonicalMemoryEntry, SourceRef } from "../storage/memory-store/memory-store-lifecycle.js";
|
|
14
|
+
export interface ConsolidationInput {
|
|
15
|
+
evidenceSummaries: Array<{
|
|
16
|
+
id: string;
|
|
17
|
+
summary: string;
|
|
18
|
+
sourceRefs: SourceRef[];
|
|
19
|
+
createdAt: string;
|
|
20
|
+
sensitivity?: string;
|
|
21
|
+
}>;
|
|
22
|
+
chronicleSummaries: Array<{
|
|
23
|
+
id: string;
|
|
24
|
+
summary: string;
|
|
25
|
+
sourceRefs: SourceRef[];
|
|
26
|
+
createdAt: string;
|
|
27
|
+
}>;
|
|
28
|
+
toolExperienceSummaries?: Array<{
|
|
29
|
+
id: string;
|
|
30
|
+
summary: string;
|
|
31
|
+
sourceRefs: SourceRef[];
|
|
32
|
+
createdAt: string;
|
|
33
|
+
}>;
|
|
34
|
+
existingEntries: CanonicalMemoryEntry[];
|
|
35
|
+
}
|
|
36
|
+
export interface ConsolidationResult {
|
|
37
|
+
entries: CanonicalMemoryEntry[];
|
|
38
|
+
conflicts: Array<{
|
|
39
|
+
entryId: string;
|
|
40
|
+
reason: string;
|
|
41
|
+
}>;
|
|
42
|
+
staleCount: number;
|
|
43
|
+
dedupeCount: number;
|
|
44
|
+
}
|
|
45
|
+
export declare function consolidateMemory(input: ConsolidationInput): ConsolidationResult;
|