titan-agent 5.3.1 → 5.4.0
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/README.md +5 -5
- package/dist/agent/agent.js +11 -1
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/session.js +106 -5
- package/dist/agent/session.js.map +1 -1
- package/dist/agent/subAgent.js +77 -1
- package/dist/agent/subAgent.js.map +1 -1
- package/dist/agent/toolRunner.js +17 -0
- package/dist/agent/toolRunner.js.map +1 -1
- package/dist/config/schema.js +18 -2
- package/dist/config/schema.js.map +1 -1
- package/dist/gateway/server.js +17 -1
- package/dist/gateway/server.js.map +1 -1
- package/dist/memory/graph.js +49 -15
- package/dist/memory/graph.js.map +1 -1
- package/dist/memory/index.js +192 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/memory.js +1 -0
- package/dist/memory/memory.js.map +1 -1
- package/dist/organism/drives.js +47 -11
- package/dist/organism/drives.js.map +1 -1
- package/dist/organism/pressure.js +16 -0
- package/dist/organism/pressure.js.map +1 -1
- package/dist/safety/fabricationGuard.js +140 -0
- package/dist/safety/fabricationGuard.js.map +1 -0
- package/dist/skills/builtin/fb_autopilot.js +16 -1
- package/dist/skills/builtin/fb_autopilot.js.map +1 -1
- package/dist/skills/builtin/gepa.js +23 -1
- package/dist/skills/builtin/gepa.js.map +1 -1
- package/dist/skills/builtin/model_trainer.js +31 -4
- package/dist/skills/builtin/model_trainer.js.map +1 -1
- package/dist/skills/builtin/self_improve.js +50 -2
- package/dist/skills/builtin/self_improve.js.map +1 -1
- package/dist/telemetry/activityLog.js +158 -0
- package/dist/telemetry/activityLog.js.map +1 -0
- package/dist/utils/constants.js +3 -1
- package/dist/utils/constants.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const STOP_WORDS = /* @__PURE__ */ new Set([
|
|
3
|
+
"a",
|
|
4
|
+
"an",
|
|
5
|
+
"the",
|
|
6
|
+
"is",
|
|
7
|
+
"it",
|
|
8
|
+
"in",
|
|
9
|
+
"on",
|
|
10
|
+
"at",
|
|
11
|
+
"to",
|
|
12
|
+
"of",
|
|
13
|
+
"do",
|
|
14
|
+
"you",
|
|
15
|
+
"we",
|
|
16
|
+
"i",
|
|
17
|
+
"me",
|
|
18
|
+
"my",
|
|
19
|
+
"that",
|
|
20
|
+
"this",
|
|
21
|
+
"was",
|
|
22
|
+
"are",
|
|
23
|
+
"be",
|
|
24
|
+
"been",
|
|
25
|
+
"have",
|
|
26
|
+
"has",
|
|
27
|
+
"had",
|
|
28
|
+
"and",
|
|
29
|
+
"or",
|
|
30
|
+
"but",
|
|
31
|
+
"if",
|
|
32
|
+
"so",
|
|
33
|
+
"not",
|
|
34
|
+
"no",
|
|
35
|
+
"yes",
|
|
36
|
+
"can",
|
|
37
|
+
"how",
|
|
38
|
+
"what",
|
|
39
|
+
"about",
|
|
40
|
+
"from",
|
|
41
|
+
"with",
|
|
42
|
+
"for",
|
|
43
|
+
"up",
|
|
44
|
+
"out",
|
|
45
|
+
"its",
|
|
46
|
+
"our",
|
|
47
|
+
"your",
|
|
48
|
+
"they",
|
|
49
|
+
"them",
|
|
50
|
+
"he",
|
|
51
|
+
"she",
|
|
52
|
+
"his",
|
|
53
|
+
"her",
|
|
54
|
+
"will",
|
|
55
|
+
"would",
|
|
56
|
+
"could",
|
|
57
|
+
"should",
|
|
58
|
+
"did",
|
|
59
|
+
"does",
|
|
60
|
+
"just",
|
|
61
|
+
"now",
|
|
62
|
+
"some",
|
|
63
|
+
"any",
|
|
64
|
+
"all",
|
|
65
|
+
"very",
|
|
66
|
+
"too",
|
|
67
|
+
"also",
|
|
68
|
+
"than",
|
|
69
|
+
"then",
|
|
70
|
+
"when",
|
|
71
|
+
"where",
|
|
72
|
+
"who",
|
|
73
|
+
"which",
|
|
74
|
+
"there",
|
|
75
|
+
"here",
|
|
76
|
+
"again",
|
|
77
|
+
"today",
|
|
78
|
+
"earlier",
|
|
79
|
+
"remember"
|
|
80
|
+
]);
|
|
81
|
+
function tokenize(text) {
|
|
82
|
+
if (!text) return [];
|
|
83
|
+
return text.toLowerCase().replace(/[^a-z0-9\- ]+/g, " ").split(/\s+/).filter((t) => t.length > 1 && !STOP_WORDS.has(t));
|
|
84
|
+
}
|
|
85
|
+
class MemoryIndex {
|
|
86
|
+
/** token → array of postings */
|
|
87
|
+
postings = /* @__PURE__ */ new Map();
|
|
88
|
+
/** episode count, used to compute IDF */
|
|
89
|
+
docCount = 0;
|
|
90
|
+
/** episode IDs we've indexed, used for `removeEpisode` and `has` */
|
|
91
|
+
indexed = /* @__PURE__ */ new Set();
|
|
92
|
+
/** Add (or re-add) an episode to the index. Idempotent — calling twice
|
|
93
|
+
* with the same id replaces the previous entry. */
|
|
94
|
+
addEpisode(episodeId, content) {
|
|
95
|
+
if (this.indexed.has(episodeId)) {
|
|
96
|
+
this.removeEpisode(episodeId);
|
|
97
|
+
}
|
|
98
|
+
const tokens = tokenize(content);
|
|
99
|
+
if (tokens.length === 0) {
|
|
100
|
+
this.indexed.add(episodeId);
|
|
101
|
+
this.docCount += 1;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const tf = /* @__PURE__ */ new Map();
|
|
105
|
+
const headTokens = new Set(tokenize(content.slice(0, 100)));
|
|
106
|
+
for (const t of tokens) tf.set(t, (tf.get(t) ?? 0) + 1);
|
|
107
|
+
for (const [token, count] of tf) {
|
|
108
|
+
const list = this.postings.get(token) ?? [];
|
|
109
|
+
list.push({ episodeId, tf: count, inHead: headTokens.has(token) });
|
|
110
|
+
this.postings.set(token, list);
|
|
111
|
+
}
|
|
112
|
+
this.indexed.add(episodeId);
|
|
113
|
+
this.docCount += 1;
|
|
114
|
+
}
|
|
115
|
+
/** Remove an episode from the index. Used when pruning. */
|
|
116
|
+
removeEpisode(episodeId) {
|
|
117
|
+
if (!this.indexed.has(episodeId)) return;
|
|
118
|
+
for (const [token, list] of this.postings) {
|
|
119
|
+
const filtered = list.filter((p) => p.episodeId !== episodeId);
|
|
120
|
+
if (filtered.length === 0) this.postings.delete(token);
|
|
121
|
+
else if (filtered.length !== list.length) this.postings.set(token, filtered);
|
|
122
|
+
}
|
|
123
|
+
this.indexed.delete(episodeId);
|
|
124
|
+
this.docCount = Math.max(0, this.docCount - 1);
|
|
125
|
+
}
|
|
126
|
+
/** True if the episode is currently indexed. */
|
|
127
|
+
has(episodeId) {
|
|
128
|
+
return this.indexed.has(episodeId);
|
|
129
|
+
}
|
|
130
|
+
/** Number of episodes in the index. */
|
|
131
|
+
size() {
|
|
132
|
+
return this.docCount;
|
|
133
|
+
}
|
|
134
|
+
/** Number of unique tokens (vocabulary size). */
|
|
135
|
+
vocabularySize() {
|
|
136
|
+
return this.postings.size;
|
|
137
|
+
}
|
|
138
|
+
/** Search the index. Returns up to `limit` matches sorted by score.
|
|
139
|
+
* Score is BM25-lite: sum over query terms of (tf × idf) + headBoost.
|
|
140
|
+
* Empty query returns empty array. */
|
|
141
|
+
search(query, limit = 20) {
|
|
142
|
+
const queryTokens = tokenize(query);
|
|
143
|
+
if (queryTokens.length === 0) return [];
|
|
144
|
+
const scoreById = /* @__PURE__ */ new Map();
|
|
145
|
+
for (const term of queryTokens) {
|
|
146
|
+
const postings = this.postings.get(term);
|
|
147
|
+
if (!postings || postings.length === 0) continue;
|
|
148
|
+
const df = postings.length;
|
|
149
|
+
const idf = 1 + Math.log((this.docCount + 1) / (df + 1));
|
|
150
|
+
for (const p of postings) {
|
|
151
|
+
const termScore = p.tf * idf + (p.inHead ? 0.5 : 0);
|
|
152
|
+
const acc = scoreById.get(p.episodeId) ?? { score: 0, matched: /* @__PURE__ */ new Set() };
|
|
153
|
+
acc.score += termScore;
|
|
154
|
+
acc.matched.add(term);
|
|
155
|
+
scoreById.set(p.episodeId, acc);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const matches = [];
|
|
159
|
+
for (const [episodeId, { score, matched }] of scoreById) {
|
|
160
|
+
matches.push({ episodeId, score, matchedTerms: Array.from(matched) });
|
|
161
|
+
}
|
|
162
|
+
matches.sort((a, b) => b.score - a.score);
|
|
163
|
+
return matches.slice(0, limit);
|
|
164
|
+
}
|
|
165
|
+
/** Drop all entries — used for tests + full rebuilds. */
|
|
166
|
+
clear() {
|
|
167
|
+
this.postings.clear();
|
|
168
|
+
this.indexed.clear();
|
|
169
|
+
this.docCount = 0;
|
|
170
|
+
}
|
|
171
|
+
/** Build a fresh index from a list of episodes. */
|
|
172
|
+
static fromEpisodes(episodes) {
|
|
173
|
+
const idx = new MemoryIndex();
|
|
174
|
+
for (const ep of episodes) idx.addEpisode(ep.id, ep.content);
|
|
175
|
+
return idx;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
let _instance = null;
|
|
179
|
+
function getMemoryIndex() {
|
|
180
|
+
if (!_instance) _instance = new MemoryIndex();
|
|
181
|
+
return _instance;
|
|
182
|
+
}
|
|
183
|
+
function _resetMemoryIndexForTests() {
|
|
184
|
+
_instance = new MemoryIndex();
|
|
185
|
+
}
|
|
186
|
+
export {
|
|
187
|
+
MemoryIndex,
|
|
188
|
+
_resetMemoryIndexForTests,
|
|
189
|
+
getMemoryIndex,
|
|
190
|
+
tokenize
|
|
191
|
+
};
|
|
192
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/memory/index.ts"],"sourcesContent":["/**\n * Inverted-Index Keyword Search (Phase 9 / Track B2)\n *\n * `searchMemory()` in graph.ts used to scan every episode linearly with a\n * BM25-ish score per term per episode — at 5000 episodes and 5 terms,\n * that's 25 000 substring searches per query. This module trades a bit of\n * memory for a constant-time-per-query lookup: token → posting-list of\n * episode IDs + per-doc term frequency.\n *\n * Sized for TITAN's typical workload:\n * - ~5000 episodes max (MAX_EPISODES bound)\n * - ~50 tokens per episode after tokenisation\n * - Index footprint ≈ 250 000 (token, episodeId, tf) tuples — single-digit MB.\n *\n * Not a full-text engine. Tokens are lowercased, punctuation stripped,\n * stop-words filtered (same set as the legacy linear scan). No stemming\n * or fuzzy match — that's what vectors.ts is for. The contract is:\n * \"what the linear scan returned, faster\".\n *\n * Usage:\n * const index = new MemoryIndex();\n * for (const ep of episodes) index.addEpisode(ep.id, ep.content);\n * const matches = index.search('weather forecast', 20);\n * // → [{ episodeId, score }, ...] sorted by score desc\n *\n * Indexes can be rebuilt from the underlying graph at any time\n * (`MemoryIndex.fromEpisodes(eps)`), so we don't bother with persistence.\n * Memory cost is small enough that recomputing on startup is cheap.\n */\n\nconst STOP_WORDS = new Set([\n 'a', 'an', 'the', 'is', 'it', 'in', 'on', 'at', 'to', 'of', 'do', 'you', 'we', 'i',\n 'me', 'my', 'that', 'this', 'was', 'are', 'be', 'been', 'have', 'has', 'had', 'and',\n 'or', 'but', 'if', 'so', 'not', 'no', 'yes', 'can', 'how', 'what', 'about', 'from',\n 'with', 'for', 'up', 'out', 'its', 'our', 'your', 'they', 'them', 'he', 'she', 'his',\n 'her', 'will', 'would', 'could', 'should', 'did', 'does', 'just', 'now', 'some', 'any',\n 'all', 'very', 'too', 'also', 'than', 'then', 'when', 'where', 'who', 'which', 'there',\n 'here', 'again', 'today', 'earlier', 'remember',\n]);\n\n/** Tokenise a string for indexing/search. Lowercase, strip non-alphanum\n * except hyphens (kept for words like \"self-improve\"), drop stop words,\n * drop tokens shorter than 2 chars. */\nexport function tokenize(text: string): string[] {\n if (!text) return [];\n return text\n .toLowerCase()\n .replace(/[^a-z0-9\\- ]+/g, ' ')\n .split(/\\s+/)\n .filter(t => t.length > 1 && !STOP_WORDS.has(t));\n}\n\n/** A single posting-list entry for a (token, episode) pair. */\ninterface Posting {\n episodeId: string;\n /** Term frequency within this episode. */\n tf: number;\n /** True if the term appears in the first 100 chars of the episode\n * content — used for the \"title boost\" the legacy scan applied. */\n inHead: boolean;\n}\n\n/** Search hit, sorted by score in `search()`. */\nexport interface IndexMatch {\n episodeId: string;\n /** TF-IDF-ish score. Higher = more relevant. */\n score: number;\n /** Which query terms matched this episode (debug aid). */\n matchedTerms: string[];\n}\n\nexport class MemoryIndex {\n /** token → array of postings */\n private postings = new Map<string, Posting[]>();\n /** episode count, used to compute IDF */\n private docCount = 0;\n /** episode IDs we've indexed, used for `removeEpisode` and `has` */\n private indexed = new Set<string>();\n\n /** Add (or re-add) an episode to the index. Idempotent — calling twice\n * with the same id replaces the previous entry. */\n addEpisode(episodeId: string, content: string): void {\n if (this.indexed.has(episodeId)) {\n this.removeEpisode(episodeId);\n }\n const tokens = tokenize(content);\n if (tokens.length === 0) {\n // Still mark as indexed so subsequent re-adds don't double-count.\n this.indexed.add(episodeId);\n this.docCount += 1;\n return;\n }\n\n // Compute term frequencies + head-presence\n const tf = new Map<string, number>();\n const headTokens = new Set(tokenize(content.slice(0, 100)));\n for (const t of tokens) tf.set(t, (tf.get(t) ?? 0) + 1);\n\n for (const [token, count] of tf) {\n const list = this.postings.get(token) ?? [];\n list.push({ episodeId, tf: count, inHead: headTokens.has(token) });\n this.postings.set(token, list);\n }\n this.indexed.add(episodeId);\n this.docCount += 1;\n }\n\n /** Remove an episode from the index. Used when pruning. */\n removeEpisode(episodeId: string): void {\n if (!this.indexed.has(episodeId)) return;\n for (const [token, list] of this.postings) {\n const filtered = list.filter(p => p.episodeId !== episodeId);\n if (filtered.length === 0) this.postings.delete(token);\n else if (filtered.length !== list.length) this.postings.set(token, filtered);\n }\n this.indexed.delete(episodeId);\n this.docCount = Math.max(0, this.docCount - 1);\n }\n\n /** True if the episode is currently indexed. */\n has(episodeId: string): boolean {\n return this.indexed.has(episodeId);\n }\n\n /** Number of episodes in the index. */\n size(): number {\n return this.docCount;\n }\n\n /** Number of unique tokens (vocabulary size). */\n vocabularySize(): number {\n return this.postings.size;\n }\n\n /** Search the index. Returns up to `limit` matches sorted by score.\n * Score is BM25-lite: sum over query terms of (tf × idf) + headBoost.\n * Empty query returns empty array. */\n search(query: string, limit = 20): IndexMatch[] {\n const queryTokens = tokenize(query);\n if (queryTokens.length === 0) return [];\n\n // Per-episode score accumulator\n const scoreById = new Map<string, { score: number; matched: Set<string> }>();\n\n for (const term of queryTokens) {\n const postings = this.postings.get(term);\n if (!postings || postings.length === 0) continue;\n\n // IDF — log smoothing to dampen common terms.\n // 1 + log((docCount+1)/(df+1)) keeps it positive even when df==docCount.\n const df = postings.length;\n const idf = 1 + Math.log((this.docCount + 1) / (df + 1));\n\n for (const p of postings) {\n // tf × idf, with a flat bonus when the term is in the\n // first 100 chars (cheap \"title boost\" the legacy scan had)\n const termScore = p.tf * idf + (p.inHead ? 0.5 : 0);\n const acc = scoreById.get(p.episodeId) ?? { score: 0, matched: new Set<string>() };\n acc.score += termScore;\n acc.matched.add(term);\n scoreById.set(p.episodeId, acc);\n }\n }\n\n const matches: IndexMatch[] = [];\n for (const [episodeId, { score, matched }] of scoreById) {\n matches.push({ episodeId, score, matchedTerms: Array.from(matched) });\n }\n matches.sort((a, b) => b.score - a.score);\n return matches.slice(0, limit);\n }\n\n /** Drop all entries — used for tests + full rebuilds. */\n clear(): void {\n this.postings.clear();\n this.indexed.clear();\n this.docCount = 0;\n }\n\n /** Build a fresh index from a list of episodes. */\n static fromEpisodes(episodes: Array<{ id: string; content: string }>): MemoryIndex {\n const idx = new MemoryIndex();\n for (const ep of episodes) idx.addEpisode(ep.id, ep.content);\n return idx;\n }\n}\n\n/** Module-level singleton used by graph.ts. Cleared + rebuilt by tests. */\nlet _instance: MemoryIndex | null = null;\n\nexport function getMemoryIndex(): MemoryIndex {\n if (!_instance) _instance = new MemoryIndex();\n return _instance;\n}\n\n/** Test-only: reset the singleton between scenarios. */\nexport function _resetMemoryIndexForTests(): void {\n _instance = new MemoryIndex();\n}\n"],"mappings":"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","names":[]}
|
package/dist/memory/memory.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/memory/memory.ts"],"sourcesContent":["/**\n * TITAN — Memory / Persistence System\n * JSON-file-backed persistent memory for conversations, facts, preferences, and usage.\n * Uses no native dependencies — pure Node.js for maximum portability.\n */\nimport { existsSync, readFileSync, writeFileSync, renameSync } from 'fs';\nimport { join } from 'path';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport { ensureDir } from '../utils/helpers.js';\nimport logger from '../utils/logger.js';\nimport { encrypt, decrypt, type EncryptedPayload } from '../security/encryption.js';\nimport { isVectorSearchAvailable, searchVectors, addVector } from './vectors.js';\n\nconst COMPONENT = 'Memory';\n\n// ─── Data Store ──────────────────────────────────────────────────\n\ninterface DataStore {\n conversations: ConversationMessage[];\n memories: MemoryEntry[];\n sessions: SessionRecord[];\n usageStats: UsageRecord[];\n cronJobs: CronRecord[];\n skillsInstalled: SkillRecord[];\n}\n\ninterface MemoryEntry {\n id: string;\n category: string;\n key: string;\n value: string;\n metadata?: string;\n createdAt: string;\n updatedAt: string;\n}\n\ninterface SessionRecord {\n id: string;\n channel: string;\n user_id: string;\n agent_id: string;\n status: string;\n message_count: number;\n created_at: string;\n last_active: string;\n name?: string;\n last_message?: string;\n // D3: Persisted session overrides (survive session recovery after timeout/restart)\n model_override?: string;\n thinking_override?: string;\n // Hunt Finding #19 (2026-04-14): true when this session was created via an\n // explicit sessionId (getOrCreateSessionById). Named sessions MUST NOT be\n // returned by the default-slot lookup in getOrCreateSession — otherwise\n // subsequent no-sessionId requests from the same channel+user+agent will\n // inherit the most recent named session's history, causing privacy bleed\n // between API callers.\n is_named?: boolean;\n}\n\ninterface UsageRecord {\n id: number;\n session_id: string;\n provider: string;\n model: string;\n prompt_tokens: number;\n completion_tokens: number;\n total_tokens: number;\n created_at: string;\n}\n\ninterface CronRecord {\n id: string;\n name: string;\n schedule: string;\n command: string;\n mode?: 'shell' | 'tool'; // Execution mode (default: shell for backward compat)\n allowedTools?: string[]; // Tool allowlist for tool-mode jobs\n enabled: boolean;\n last_run?: string;\n next_run?: string;\n created_at: string;\n}\n\ninterface SkillRecord {\n name: string;\n version: string;\n source: string;\n enabled: boolean;\n installed_at: string;\n}\n\nconst DB_FILE = join(TITAN_HOME, 'titan-data.json');\n\nlet store: DataStore | null = null;\nlet dirty = false;\nlet isShuttingDown = false;\n\nfunction getDefaultStore(): DataStore {\n return {\n conversations: [],\n memories: [],\n sessions: [],\n usageStats: [],\n cronJobs: [],\n skillsInstalled: [],\n };\n}\n\n// NOTE: Sync I/O is intentional — runs only once at cold start, then cached in-memory.\nfunction loadStore(): DataStore {\n if (store) return store;\n ensureDir(TITAN_HOME);\n if (existsSync(DB_FILE)) {\n try {\n const raw = readFileSync(DB_FILE, 'utf-8');\n store = JSON.parse(raw) as DataStore;\n // Ensure all fields exist\n store.conversations = store.conversations || [];\n store.memories = store.memories || [];\n store.sessions = store.sessions || [];\n store.usageStats = store.usageStats || [];\n store.cronJobs = store.cronJobs || [];\n store.skillsInstalled = store.skillsInstalled || [];\n } catch {\n logger.warn(COMPONENT, 'Could not load data store, creating fresh one');\n store = getDefaultStore();\n }\n } else {\n store = getDefaultStore();\n }\n return store;\n}\n\nfunction saveStore(): void {\n if (!store || isShuttingDown) return;\n ensureDir(TITAN_HOME);\n try {\n const tmpFile = DB_FILE + '.tmp';\n writeFileSync(tmpFile, JSON.stringify(store, null, 2), 'utf-8');\n renameSync(tmpFile, DB_FILE);\n dirty = false;\n } catch (e) {\n dirty = true;\n logger.error(COMPONENT, `Failed to save data: ${(e as Error).message}`);\n }\n}\n\n// Auto-save periodically\nlet saveTimeout: ReturnType<typeof setTimeout> | null = null;\nfunction debouncedSave(): void {\n if (dirty) { saveStore(); return; }\n if (saveTimeout) clearTimeout(saveTimeout);\n saveTimeout = setTimeout(saveStore, 1000);\n saveTimeout.unref();\n}\n\n/** Initialize the memory system */\nexport function initMemory(): void {\n loadStore();\n logger.info(COMPONENT, 'Memory system initialized');\n}\n\n/** Close / flush the memory system */\nexport function closeMemory(): void {\n if (saveTimeout) { clearTimeout(saveTimeout); saveTimeout = null; }\n saveStore();\n if (dirty) {\n logger.error(COMPONENT, 'DATA MAY BE LOST — failed to flush memory store on shutdown');\n }\n isShuttingDown = true;\n}\n\n/** Get internal store (for skills like cron that need direct access) */\nexport function getDb(): DataStore {\n return loadStore();\n}\n\n// ─── Conversation History ────────────────────────────────────────\n\nexport interface ConversationMessage {\n id: string;\n sessionId: string;\n role: string;\n content: string;\n toolCalls?: string;\n toolCallId?: string;\n model?: string;\n tokenCount: number;\n createdAt: string;\n isEncrypted?: boolean;\n}\n\n/** Save a message to conversation history */\nexport function saveMessage(message: Omit<ConversationMessage, 'createdAt'>, e2eKey?: string): void {\n const s = loadStore();\n\n let content = message.content;\n let isEncrypted = false;\n\n if (e2eKey) {\n try {\n const payload = encrypt(message.content, Buffer.from(e2eKey, 'base64'));\n content = JSON.stringify(payload);\n isEncrypted = true;\n } catch {\n logger.error(COMPONENT, `Failed to encrypt message for storage`);\n content = \"[ENCRYPTION FAILED] \" + content; // Fallback, though we should probably throw in strict environments\n }\n }\n\n s.conversations.push({\n ...message,\n content,\n isEncrypted,\n createdAt: new Date().toISOString(),\n });\n // Keep only last 5000 messages total to prevent unbounded growth\n if (s.conversations.length > 5000) {\n s.conversations = s.conversations.slice(-5000);\n }\n debouncedSave();\n}\n\n/** Get conversation history for a session */\nexport function getHistory(sessionId: string, limit: number = 50, e2eKey?: string): ConversationMessage[] {\n const s = loadStore();\n const rawHistory = s.conversations\n .filter((m) => m.sessionId === sessionId)\n .slice(-limit);\n\n if (!e2eKey) {\n // If no key is provided, we just return the raw payload. \n // If it's encrypted, it'll just show the JSON string of the EncryptedPayload.\n return rawHistory;\n }\n\n // Decrypt the ones that were encrypted\n return rawHistory.map(m => {\n if (m.isEncrypted) {\n try {\n const payload = JSON.parse(m.content) as EncryptedPayload;\n return {\n ...m,\n content: decrypt(payload, Buffer.from(e2eKey, 'base64'))\n };\n } catch {\n logger.error(COMPONENT, `Failed to decrypt message ${m.id}`);\n return { ...m, content: \"[DECRYPTION FAILED]\" };\n }\n }\n return m;\n });\n}\n\n/** Update session name and/or last message snippet */\nexport function updateSessionMeta(sessionId: string, meta: { name?: string; last_message?: string; model_override?: string; thinking_override?: string }): void {\n const s = loadStore();\n const rec = s.sessions.find(ses => ses.id === sessionId);\n if (!rec) return;\n if (meta.name !== undefined) rec.name = meta.name;\n if (meta.last_message !== undefined) rec.last_message = meta.last_message;\n // D3: Persist session overrides to database so they survive timeout/restart\n if (meta.model_override !== undefined) rec.model_override = meta.model_override;\n if (meta.thinking_override !== undefined) rec.thinking_override = meta.thinking_override;\n debouncedSave();\n}\n\n/** Clear conversation history for a session */\nexport function clearHistory(sessionId: string): void {\n const s = loadStore();\n s.conversations = s.conversations.filter((m) => m.sessionId !== sessionId);\n debouncedSave();\n}\n\n// ─── Persistent Memory (Facts / Preferences) ─────────────────────\n\n/** Store a memory (key-value with category) */\nexport function rememberFact(category: string, key: string, value: string, metadata?: Record<string, unknown>): void {\n const s = loadStore();\n const id = `${category}:${key}`;\n const existingIdx = s.memories.findIndex((m) => m.id === id);\n const now = new Date().toISOString();\n\n if (existingIdx >= 0) {\n s.memories[existingIdx].value = value;\n s.memories[existingIdx].metadata = metadata ? JSON.stringify(metadata) : undefined;\n s.memories[existingIdx].updatedAt = now;\n } else {\n s.memories.push({\n id,\n category,\n key,\n value,\n metadata: metadata ? JSON.stringify(metadata) : undefined,\n createdAt: now,\n updatedAt: now,\n });\n }\n debouncedSave();\n\n // Index to vector store (fire-and-forget)\n if (isVectorSearchAvailable()) {\n addVector(id, `${category}: ${key} = ${value}`, 'memory', { category, key }).catch(e => logger.debug(COMPONENT, `Background vector indexing failed: ${(e as Error).message}`));\n }\n}\n\n/** Recall a specific memory */\nexport function recallFact(category: string, key: string): string | null {\n const s = loadStore();\n const entry = s.memories.find((m) => m.category === category && m.key === key);\n return entry?.value || null;\n}\n\n/** Search memories by category — hybrid keyword + vector search */\nexport async function searchMemories(category?: string, query?: string): Promise<Array<{ key: string; value: string; category: string; score?: number }>> {\n const s = loadStore();\n let results = s.memories;\n\n if (category) {\n results = results.filter((m) => m.category === category);\n }\n if (query) {\n const q = query.toLowerCase();\n // Word-boundary match to avoid false positives (\"use\" matching \"user\", \"reuse\")\n const qRegex = new RegExp('\\\\b' + q.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&') + '\\\\b', 'i');\n results = results.filter((m) =>\n qRegex.test(m.key) || qRegex.test(m.value)\n );\n }\n\n // Keyword scoring\n const scored = results.map(m => {\n let score = 0;\n if (query) {\n const q = query.toLowerCase();\n const keyLower = m.key.toLowerCase();\n const valLower = m.value.toLowerCase();\n // Exact key match scores highest\n if (keyLower === q) score += 5;\n else if (keyLower.includes(q)) score += 2;\n if (valLower.includes(q)) score += 1;\n // BM25-style: boost for multiple keyword matches\n const terms = q.split(/\\s+/).filter(Boolean);\n for (const term of terms) {\n if (keyLower.includes(term)) score += 1;\n if (valLower.includes(term)) score += 0.5;\n }\n }\n return { key: m.key, value: m.value, category: m.category, id: m.id, score };\n });\n\n // Vector search augmentation (hybrid mode)\n if (query && isVectorSearchAvailable()) {\n try {\n const vectorResults = await searchVectors(query, 20, 'memory', 0.4);\n for (const vr of vectorResults) {\n // Skip stale vector IDs that no longer exist in the store\n const memEntry = s.memories.find(m => m.id === vr.id);\n if (!memEntry) continue;\n const existing = scored.find(s => s.id === vr.id);\n if (existing) {\n // Boost keyword results that also match semantically\n existing.score += vr.score * 3;\n } else {\n // Add vector-only results (semantically similar but no keyword match)\n const entry = s.memories.find(m => m.id === vr.id);\n if (entry && (!category || entry.category === category)) {\n scored.push({\n key: entry.key,\n value: entry.value,\n category: entry.category,\n id: entry.id,\n score: vr.score * 2,\n });\n }\n }\n }\n } catch {\n // Vector search failure is non-fatal\n }\n }\n\n scored.sort((a, b) => b.score - a.score);\n // Deduplicate by ID (vector + keyword can match the same entry)\n const seen = new Set<string>();\n const unique = scored.filter(m => { if (seen.has(m.id)) return false; seen.add(m.id); return true; });\n return unique.slice(0, 50).map(m => ({ key: m.key, value: m.value, category: m.category, score: m.score }));\n}\n\n// ─── Usage Tracking ──────────────────────────────────────────────\n\n/** Record usage statistics */\nexport function recordUsage(sessionId: string, provider: string, model: string, promptTokens: number, completionTokens: number): void {\n const s = loadStore();\n s.usageStats.push({\n id: Date.now(),\n session_id: sessionId,\n provider,\n model,\n prompt_tokens: promptTokens,\n completion_tokens: completionTokens,\n total_tokens: promptTokens + completionTokens,\n created_at: new Date().toISOString(),\n });\n // Keep only last 10000 records\n if (s.usageStats.length > 10000) {\n s.usageStats = s.usageStats.slice(-10000);\n }\n debouncedSave();\n}\n\n/** Get total usage statistics */\nexport function getUsageStats(): { totalTokens: number; totalRequests: number; byProvider: Record<string, number> } {\n const s = loadStore();\n let totalTokens = 0;\n const byProvider: Record<string, number> = {};\n\n for (const rec of s.usageStats) {\n totalTokens += rec.total_tokens;\n byProvider[rec.provider] = (byProvider[rec.provider] || 0) + rec.total_tokens;\n }\n\n return {\n totalTokens,\n totalRequests: s.usageStats.length,\n byProvider,\n };\n}\n"],"mappings":"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","names":["s"]}
|
|
1
|
+
{"version":3,"sources":["../../src/memory/memory.ts"],"sourcesContent":["/**\n * TITAN — Memory / Persistence System\n * JSON-file-backed persistent memory for conversations, facts, preferences, and usage.\n * Uses no native dependencies — pure Node.js for maximum portability.\n */\nimport { existsSync, readFileSync, writeFileSync, renameSync } from 'fs';\nimport { join } from 'path';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport { ensureDir } from '../utils/helpers.js';\nimport logger from '../utils/logger.js';\nimport { encrypt, decrypt, type EncryptedPayload } from '../security/encryption.js';\nimport { isVectorSearchAvailable, searchVectors, addVector } from './vectors.js';\n\nconst COMPONENT = 'Memory';\n\n// ─── Data Store ──────────────────────────────────────────────────\n\ninterface DataStore {\n conversations: ConversationMessage[];\n memories: MemoryEntry[];\n sessions: SessionRecord[];\n usageStats: UsageRecord[];\n cronJobs: CronRecord[];\n skillsInstalled: SkillRecord[];\n}\n\ninterface MemoryEntry {\n id: string;\n category: string;\n key: string;\n value: string;\n metadata?: string;\n createdAt: string;\n updatedAt: string;\n}\n\ninterface SessionRecord {\n id: string;\n channel: string;\n user_id: string;\n agent_id: string;\n status: string;\n message_count: number;\n created_at: string;\n last_active: string;\n name?: string;\n last_message?: string;\n // D3: Persisted session overrides (survive session recovery after timeout/restart)\n model_override?: string;\n thinking_override?: string;\n // Hunt Finding #19 (2026-04-14): true when this session was created via an\n // explicit sessionId (getOrCreateSessionById). Named sessions MUST NOT be\n // returned by the default-slot lookup in getOrCreateSession — otherwise\n // subsequent no-sessionId requests from the same channel+user+agent will\n // inherit the most recent named session's history, causing privacy bleed\n // between API callers.\n is_named?: boolean;\n}\n\ninterface UsageRecord {\n id: number;\n session_id: string;\n provider: string;\n model: string;\n prompt_tokens: number;\n completion_tokens: number;\n total_tokens: number;\n created_at: string;\n}\n\ninterface CronRecord {\n id: string;\n name: string;\n schedule: string;\n command: string;\n mode?: 'shell' | 'tool'; // Execution mode (default: shell for backward compat)\n allowedTools?: string[]; // Tool allowlist for tool-mode jobs\n enabled: boolean;\n last_run?: string;\n next_run?: string;\n created_at: string;\n}\n\ninterface SkillRecord {\n name: string;\n version: string;\n source: string;\n enabled: boolean;\n installed_at: string;\n}\n\nconst DB_FILE = join(TITAN_HOME, 'titan-data.json');\n\nlet store: DataStore | null = null;\nlet dirty = false;\nlet isShuttingDown = false;\n\nfunction getDefaultStore(): DataStore {\n return {\n conversations: [],\n memories: [],\n sessions: [],\n usageStats: [],\n cronJobs: [],\n skillsInstalled: [],\n };\n}\n\n// NOTE: Sync I/O is intentional — runs only once at cold start, then cached in-memory.\nfunction loadStore(): DataStore {\n if (store) return store;\n ensureDir(TITAN_HOME);\n if (existsSync(DB_FILE)) {\n try {\n const raw = readFileSync(DB_FILE, 'utf-8');\n store = JSON.parse(raw) as DataStore;\n // Ensure all fields exist\n store.conversations = store.conversations || [];\n store.memories = store.memories || [];\n store.sessions = store.sessions || [];\n store.usageStats = store.usageStats || [];\n store.cronJobs = store.cronJobs || [];\n store.skillsInstalled = store.skillsInstalled || [];\n } catch {\n logger.warn(COMPONENT, 'Could not load data store, creating fresh one');\n store = getDefaultStore();\n }\n } else {\n store = getDefaultStore();\n }\n return store;\n}\n\nfunction saveStore(): void {\n if (!store || isShuttingDown) return;\n ensureDir(TITAN_HOME);\n try {\n const tmpFile = DB_FILE + '.tmp';\n writeFileSync(tmpFile, JSON.stringify(store, null, 2), 'utf-8');\n renameSync(tmpFile, DB_FILE);\n dirty = false;\n } catch (e) {\n dirty = true;\n logger.error(COMPONENT, `Failed to save data: ${(e as Error).message}`);\n }\n}\n\n// Auto-save periodically\nlet saveTimeout: ReturnType<typeof setTimeout> | null = null;\nexport function debouncedSave(): void {\n if (dirty) { saveStore(); return; }\n if (saveTimeout) clearTimeout(saveTimeout);\n saveTimeout = setTimeout(saveStore, 1000);\n saveTimeout.unref();\n}\n\n/** Initialize the memory system */\nexport function initMemory(): void {\n loadStore();\n logger.info(COMPONENT, 'Memory system initialized');\n}\n\n/** Close / flush the memory system */\nexport function closeMemory(): void {\n if (saveTimeout) { clearTimeout(saveTimeout); saveTimeout = null; }\n saveStore();\n if (dirty) {\n logger.error(COMPONENT, 'DATA MAY BE LOST — failed to flush memory store on shutdown');\n }\n isShuttingDown = true;\n}\n\n/** Get internal store (for skills like cron that need direct access) */\nexport function getDb(): DataStore {\n return loadStore();\n}\n\n// ─── Conversation History ────────────────────────────────────────\n\nexport interface ConversationMessage {\n id: string;\n sessionId: string;\n role: string;\n content: string;\n toolCalls?: string;\n toolCallId?: string;\n model?: string;\n tokenCount: number;\n createdAt: string;\n isEncrypted?: boolean;\n}\n\n/** Save a message to conversation history */\nexport function saveMessage(message: Omit<ConversationMessage, 'createdAt'>, e2eKey?: string): void {\n const s = loadStore();\n\n let content = message.content;\n let isEncrypted = false;\n\n if (e2eKey) {\n try {\n const payload = encrypt(message.content, Buffer.from(e2eKey, 'base64'));\n content = JSON.stringify(payload);\n isEncrypted = true;\n } catch {\n logger.error(COMPONENT, `Failed to encrypt message for storage`);\n content = \"[ENCRYPTION FAILED] \" + content; // Fallback, though we should probably throw in strict environments\n }\n }\n\n s.conversations.push({\n ...message,\n content,\n isEncrypted,\n createdAt: new Date().toISOString(),\n });\n // Keep only last 5000 messages total to prevent unbounded growth\n if (s.conversations.length > 5000) {\n s.conversations = s.conversations.slice(-5000);\n }\n debouncedSave();\n}\n\n/** Get conversation history for a session */\nexport function getHistory(sessionId: string, limit: number = 50, e2eKey?: string): ConversationMessage[] {\n const s = loadStore();\n const rawHistory = s.conversations\n .filter((m) => m.sessionId === sessionId)\n .slice(-limit);\n\n if (!e2eKey) {\n // If no key is provided, we just return the raw payload. \n // If it's encrypted, it'll just show the JSON string of the EncryptedPayload.\n return rawHistory;\n }\n\n // Decrypt the ones that were encrypted\n return rawHistory.map(m => {\n if (m.isEncrypted) {\n try {\n const payload = JSON.parse(m.content) as EncryptedPayload;\n return {\n ...m,\n content: decrypt(payload, Buffer.from(e2eKey, 'base64'))\n };\n } catch {\n logger.error(COMPONENT, `Failed to decrypt message ${m.id}`);\n return { ...m, content: \"[DECRYPTION FAILED]\" };\n }\n }\n return m;\n });\n}\n\n/** Update session name and/or last message snippet */\nexport function updateSessionMeta(sessionId: string, meta: { name?: string; last_message?: string; model_override?: string; thinking_override?: string }): void {\n const s = loadStore();\n const rec = s.sessions.find(ses => ses.id === sessionId);\n if (!rec) return;\n if (meta.name !== undefined) rec.name = meta.name;\n if (meta.last_message !== undefined) rec.last_message = meta.last_message;\n // D3: Persist session overrides to database so they survive timeout/restart\n if (meta.model_override !== undefined) rec.model_override = meta.model_override;\n if (meta.thinking_override !== undefined) rec.thinking_override = meta.thinking_override;\n debouncedSave();\n}\n\n/** Clear conversation history for a session */\nexport function clearHistory(sessionId: string): void {\n const s = loadStore();\n s.conversations = s.conversations.filter((m) => m.sessionId !== sessionId);\n debouncedSave();\n}\n\n// ─── Persistent Memory (Facts / Preferences) ─────────────────────\n\n/** Store a memory (key-value with category) */\nexport function rememberFact(category: string, key: string, value: string, metadata?: Record<string, unknown>): void {\n const s = loadStore();\n const id = `${category}:${key}`;\n const existingIdx = s.memories.findIndex((m) => m.id === id);\n const now = new Date().toISOString();\n\n if (existingIdx >= 0) {\n s.memories[existingIdx].value = value;\n s.memories[existingIdx].metadata = metadata ? JSON.stringify(metadata) : undefined;\n s.memories[existingIdx].updatedAt = now;\n } else {\n s.memories.push({\n id,\n category,\n key,\n value,\n metadata: metadata ? JSON.stringify(metadata) : undefined,\n createdAt: now,\n updatedAt: now,\n });\n }\n debouncedSave();\n\n // Index to vector store (fire-and-forget)\n if (isVectorSearchAvailable()) {\n addVector(id, `${category}: ${key} = ${value}`, 'memory', { category, key }).catch(e => logger.debug(COMPONENT, `Background vector indexing failed: ${(e as Error).message}`));\n }\n}\n\n/** Recall a specific memory */\nexport function recallFact(category: string, key: string): string | null {\n const s = loadStore();\n const entry = s.memories.find((m) => m.category === category && m.key === key);\n return entry?.value || null;\n}\n\n/** Search memories by category — hybrid keyword + vector search */\nexport async function searchMemories(category?: string, query?: string): Promise<Array<{ key: string; value: string; category: string; score?: number }>> {\n const s = loadStore();\n let results = s.memories;\n\n if (category) {\n results = results.filter((m) => m.category === category);\n }\n if (query) {\n const q = query.toLowerCase();\n // Word-boundary match to avoid false positives (\"use\" matching \"user\", \"reuse\")\n const qRegex = new RegExp('\\\\b' + q.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&') + '\\\\b', 'i');\n results = results.filter((m) =>\n qRegex.test(m.key) || qRegex.test(m.value)\n );\n }\n\n // Keyword scoring\n const scored = results.map(m => {\n let score = 0;\n if (query) {\n const q = query.toLowerCase();\n const keyLower = m.key.toLowerCase();\n const valLower = m.value.toLowerCase();\n // Exact key match scores highest\n if (keyLower === q) score += 5;\n else if (keyLower.includes(q)) score += 2;\n if (valLower.includes(q)) score += 1;\n // BM25-style: boost for multiple keyword matches\n const terms = q.split(/\\s+/).filter(Boolean);\n for (const term of terms) {\n if (keyLower.includes(term)) score += 1;\n if (valLower.includes(term)) score += 0.5;\n }\n }\n return { key: m.key, value: m.value, category: m.category, id: m.id, score };\n });\n\n // Vector search augmentation (hybrid mode)\n if (query && isVectorSearchAvailable()) {\n try {\n const vectorResults = await searchVectors(query, 20, 'memory', 0.4);\n for (const vr of vectorResults) {\n // Skip stale vector IDs that no longer exist in the store\n const memEntry = s.memories.find(m => m.id === vr.id);\n if (!memEntry) continue;\n const existing = scored.find(s => s.id === vr.id);\n if (existing) {\n // Boost keyword results that also match semantically\n existing.score += vr.score * 3;\n } else {\n // Add vector-only results (semantically similar but no keyword match)\n const entry = s.memories.find(m => m.id === vr.id);\n if (entry && (!category || entry.category === category)) {\n scored.push({\n key: entry.key,\n value: entry.value,\n category: entry.category,\n id: entry.id,\n score: vr.score * 2,\n });\n }\n }\n }\n } catch {\n // Vector search failure is non-fatal\n }\n }\n\n scored.sort((a, b) => b.score - a.score);\n // Deduplicate by ID (vector + keyword can match the same entry)\n const seen = new Set<string>();\n const unique = scored.filter(m => { if (seen.has(m.id)) return false; seen.add(m.id); return true; });\n return unique.slice(0, 50).map(m => ({ key: m.key, value: m.value, category: m.category, score: m.score }));\n}\n\n// ─── Usage Tracking ──────────────────────────────────────────────\n\n/** Record usage statistics */\nexport function recordUsage(sessionId: string, provider: string, model: string, promptTokens: number, completionTokens: number): void {\n const s = loadStore();\n s.usageStats.push({\n id: Date.now(),\n session_id: sessionId,\n provider,\n model,\n prompt_tokens: promptTokens,\n completion_tokens: completionTokens,\n total_tokens: promptTokens + completionTokens,\n created_at: new Date().toISOString(),\n });\n // Keep only last 10000 records\n if (s.usageStats.length > 10000) {\n s.usageStats = s.usageStats.slice(-10000);\n }\n debouncedSave();\n}\n\n/** Get total usage statistics */\nexport function getUsageStats(): { totalTokens: number; totalRequests: number; byProvider: Record<string, number> } {\n const s = loadStore();\n let totalTokens = 0;\n const byProvider: Record<string, number> = {};\n\n for (const rec of s.usageStats) {\n totalTokens += rec.total_tokens;\n byProvider[rec.provider] = (byProvider[rec.provider] || 0) + rec.total_tokens;\n }\n\n return {\n totalTokens,\n totalRequests: s.usageStats.length,\n byProvider,\n };\n}\n"],"mappings":"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","names":["s"]}
|
package/dist/organism/drives.js
CHANGED
|
@@ -198,24 +198,46 @@ const SOCIAL = {
|
|
|
198
198
|
weight: 0.7,
|
|
199
199
|
compute: (snap) => {
|
|
200
200
|
const eligible = snap.agents.filter((a) => (a.totalTasksCompleted ?? 0) > 0 || a.status === "active");
|
|
201
|
-
if (eligible.length === 0) {
|
|
202
|
-
return { satisfaction: 0.9, inputs: { totalAgents: snap.agents.length, staleAgents: 0 } };
|
|
203
|
-
}
|
|
204
201
|
const hourMs = 36e5;
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
202
|
+
let agentSat = 0.9;
|
|
203
|
+
let stale = 0;
|
|
204
|
+
if (eligible.length > 0) {
|
|
205
|
+
stale = eligible.filter(
|
|
206
|
+
(a) => snap.now - new Date(a.lastHeartbeat).getTime() > hourMs
|
|
207
|
+
).length;
|
|
208
|
+
agentSat = clamp01(1 - stale / eligible.length);
|
|
209
|
+
}
|
|
210
|
+
const POST_DROUGHT_HOURS = 24;
|
|
211
|
+
let postSat;
|
|
212
|
+
let hoursSinceLastPost;
|
|
213
|
+
if (snap.lastFacebookPostAt && snap.lastFacebookPostAt > 0) {
|
|
214
|
+
hoursSinceLastPost = Math.max(0, (snap.now - snap.lastFacebookPostAt) / hourMs);
|
|
215
|
+
postSat = clamp01(1 - hoursSinceLastPost / POST_DROUGHT_HOURS);
|
|
216
|
+
} else {
|
|
217
|
+
hoursSinceLastPost = POST_DROUGHT_HOURS / 2;
|
|
218
|
+
postSat = 0.5;
|
|
219
|
+
}
|
|
220
|
+
const satisfaction = clamp01((agentSat + postSat) / 2);
|
|
209
221
|
return {
|
|
210
222
|
satisfaction,
|
|
211
|
-
inputs: {
|
|
223
|
+
inputs: {
|
|
224
|
+
totalAgents: eligible.length,
|
|
225
|
+
staleAgents: stale,
|
|
226
|
+
hoursSinceLastPost: Number(hoursSinceLastPost.toFixed(2)),
|
|
227
|
+
agentSatisfaction: Number(agentSat.toFixed(3)),
|
|
228
|
+
postSatisfaction: Number(postSat.toFixed(3))
|
|
229
|
+
}
|
|
212
230
|
};
|
|
213
231
|
},
|
|
214
232
|
describe: (_s, inputs) => {
|
|
215
233
|
const total = inputs?.totalAgents ?? 0;
|
|
216
234
|
const stale = inputs?.staleAgents ?? 0;
|
|
217
|
-
|
|
218
|
-
|
|
235
|
+
const hoursSince = inputs?.hoursSinceLastPost ?? 0;
|
|
236
|
+
const reasons = [];
|
|
237
|
+
if (stale > 0) reasons.push(`${stale}/${total} agent(s) unresponsive`);
|
|
238
|
+
if (hoursSince >= 12) reasons.push(`${Math.round(hoursSince)}h since last FB post`);
|
|
239
|
+
if (reasons.length === 0) return `${total} agent(s) all alive \xB7 posted recently`;
|
|
240
|
+
return reasons.join(" \xB7 ");
|
|
219
241
|
}
|
|
220
242
|
};
|
|
221
243
|
const DRIVES = [PURPOSE, HUNGER, CURIOSITY, SAFETY, SOCIAL];
|
|
@@ -260,6 +282,19 @@ function buildSnapshot() {
|
|
|
260
282
|
if (patterns !== null) unresolvedErrorPatterns = patterns;
|
|
261
283
|
} catch {
|
|
262
284
|
}
|
|
285
|
+
let lastFacebookPostAt = null;
|
|
286
|
+
try {
|
|
287
|
+
const fbStatePath = join(TITAN_HOME, "fb-autopilot-state.json");
|
|
288
|
+
if (existsSync(fbStatePath)) {
|
|
289
|
+
const raw = readFileSync(fbStatePath, "utf-8");
|
|
290
|
+
const state = JSON.parse(raw);
|
|
291
|
+
if (state.lastPostAt) {
|
|
292
|
+
const parsed = new Date(state.lastPostAt).getTime();
|
|
293
|
+
if (Number.isFinite(parsed)) lastFacebookPostAt = parsed;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
263
298
|
return {
|
|
264
299
|
now: Date.now(),
|
|
265
300
|
goals,
|
|
@@ -271,7 +306,8 @@ function buildSnapshot() {
|
|
|
271
306
|
vramSaturation,
|
|
272
307
|
telemetryErrorRate,
|
|
273
308
|
telemetryTotalRequests,
|
|
274
|
-
unresolvedErrorPatterns
|
|
309
|
+
unresolvedErrorPatterns,
|
|
310
|
+
lastFacebookPostAt
|
|
275
311
|
};
|
|
276
312
|
}
|
|
277
313
|
function readCachedVRAMSignal() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/organism/drives.ts"],"sourcesContent":["/**\n * TITAN — Drive Layer (Soma organism / endocrine system)\n *\n * Five homeostatic drives. Each computes a 0-1 \"satisfaction\" from existing\n * TITAN telemetry — no new instrumentation. When satisfaction dips below the\n * drive's setpoint, pressure accumulates. Cross-drive pressure fusion (see\n * pressure.ts) eventually produces a soma_proposal for human approval.\n *\n * Gated by config.organism.enabled — this module is inert when disabled.\n *\n * DRIVES SHIPPED IN v4.0:\n * Purpose — alignment with priority-1 goals\n * Hunger — backlog size vs. throughput\n * Curiosity — task-type diversity in recent trajectories\n * Safety — budget runway + recent error rate\n * Social — stale agent fraction\n *\n * DEFERRED TO v4.1+:\n * Hygiene — needs npm test + git status shell hooks\n */\nimport { existsSync, readFileSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport { ensureDir } from '../utils/helpers.js';\nimport { listGoals, getReadyTasks, type Goal } from '../agent/goals.js';\nimport { getRegisteredAgents, getBudgetPolicies, listRuns, type RegisteredAgent, type BudgetPolicy, type CPRun } from '../agent/commandPost.js';\nimport { getRecentTrajectories, type TaskTrajectory } from '../agent/trajectoryLogger.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'Drives';\nconst DRIVE_STATE_PATH = join(TITAN_HOME, 'drive-state.json');\n\n// ── Types ────────────────────────────────────────────────────────\n\nexport type DriveId = 'purpose' | 'hunger' | 'curiosity' | 'safety' | 'social';\n\nexport interface DriveSnapshot {\n /** Timestamp of the snapshot in epoch ms. */\n now: number;\n /** All goals from goals.ts. */\n goals: Goal[];\n /** Output of getReadyTasks() — ready-to-execute subtasks. */\n readyTasks: Array<{ goal: Goal; subtask: Goal['subtasks'][number] }>;\n /** Recent CPRun history (up to 100 most recent). */\n recentRuns: CPRun[];\n /** Active budget policies. */\n budgets: BudgetPolicy[];\n /** All registered agents. */\n agents: RegisteredAgent[];\n /** Last 100 trajectory entries. */\n trajectories: TaskTrajectory[];\n /**\n * v4.9.0: fraction of GPU VRAM in use (0–1). Undefined when no GPU\n * is attached or the orchestrator hasn't refreshed yet.\n */\n vramSaturation?: number;\n /**\n * v4.9.0: error rate across recent LLM / tool calls from the\n * gateway metrics layer (0–1). Undefined when metrics are unavailable.\n */\n telemetryErrorRate?: number;\n /** v4.9.0: total LLM + tool-call requests since gateway start. */\n telemetryTotalRequests?: number;\n /**\n * v4.9.0: count of error patterns the learning layer has accumulated\n * but not yet resolved. High count pulls Curiosity toward an\n * investigate/improve proposal.\n */\n unresolvedErrorPatterns?: number;\n}\n\nexport interface DriveDefinition {\n id: DriveId;\n label: string;\n /** Satisfaction level below which this drive starts contributing pressure. */\n defaultSetpoint: number;\n /** Relative weight in cross-drive pressure fusion (1.0 is baseline). */\n weight: number;\n /** Pure function — computes satisfaction 0-1 from the snapshot. */\n compute: (snapshot: DriveSnapshot) => { satisfaction: number; inputs?: Record<string, unknown> };\n /** Short human-readable explanation used in prompts, UI tooltips, and activity feed. */\n describe: (satisfaction: number, inputs?: Record<string, unknown>) => string;\n}\n\nexport interface DriveState {\n id: DriveId;\n label: string;\n satisfaction: number;\n setpoint: number;\n /** 0 when satisfaction >= setpoint, else (setpoint − satisfaction) × weight. */\n pressure: number;\n weight: number;\n inputs?: Record<string, unknown>;\n description: string;\n}\n\nexport interface DriveTickResult {\n timestamp: string;\n drives: DriveState[];\n totalPressure: number;\n dominantDrives: DriveId[];\n}\n\n// ── Numeric helpers ──────────────────────────────────────────────\n\n/** Clamp to [0,1]. */\nfunction clamp01(v: number): number {\n if (!Number.isFinite(v)) return 0;\n return Math.max(0, Math.min(1, v));\n}\n\n/** Sigmoid centred on `mid` with slope `k`. Returns high → 1 when x is low. */\nfunction invertedSigmoid(x: number, mid: number, k = 1): number {\n return clamp01(1 / (1 + Math.exp(k * (x - mid))));\n}\n\n/** Gini coefficient of a count distribution. 0 = uniform, 1 = all same task. */\nfunction gini(counts: number[]): number {\n if (counts.length === 0) return 0;\n const n = counts.length;\n const sum = counts.reduce((a, b) => a + b, 0);\n if (sum === 0) return 0;\n const sorted = [...counts].sort((a, b) => a - b);\n let cum = 0;\n for (let i = 0; i < n; i++) cum += (i + 1) * sorted[i];\n return clamp01((2 * cum) / (n * sum) - (n + 1) / n);\n}\n\n// ── Drive definitions ────────────────────────────────────────────\n\nconst PURPOSE: DriveDefinition = {\n id: 'purpose',\n label: 'Purpose',\n defaultSetpoint: 0.7,\n weight: 1.4,\n compute: (snap) => {\n // Priority-1 goals tagged as high-priority. Satisfaction reflects how\n // recently any of them progressed. No priority-1 goals → satiated\n // (nothing to worry about).\n const priorityOne = snap.goals.filter(g =>\n g.status === 'active' && g.priority === 1,\n );\n if (priorityOne.length === 0) {\n return { satisfaction: 0.9, inputs: { priorityOneCount: 0 } };\n }\n const latest = Math.max(...priorityOne.map(g =>\n new Date(g.updatedAt || g.createdAt).getTime(),\n ));\n const hoursSince = Math.max(0, (snap.now - latest) / 3_600_000);\n const satisfaction = clamp01(1 - hoursSince / 24);\n return {\n satisfaction,\n inputs: { priorityOneCount: priorityOne.length, hoursSinceProgress: Math.round(hoursSince * 10) / 10 },\n };\n },\n describe: (s, inputs) => {\n const count = (inputs?.priorityOneCount as number) ?? 0;\n if (count === 0) return 'no priority-1 goals in flight';\n const hours = (inputs?.hoursSinceProgress as number) ?? 0;\n if (s < 0.3) return `${count} priority-1 goal(s) stalled — no progress in ${hours.toFixed(1)}h`;\n if (s < 0.6) return `${count} priority-1 goal(s) need attention`;\n return `${count} priority-1 goal(s) on track`;\n },\n};\n\nconst HUNGER: DriveDefinition = {\n id: 'hunger',\n label: 'Hunger',\n defaultSetpoint: 0.6,\n weight: 1.0,\n compute: (snap) => {\n const readyCount = snap.readyTasks.length;\n // Oldest ready subtask age in hours, using parent goal createdAt as proxy.\n const oldestAgeHours = snap.readyTasks.length === 0\n ? 0\n : Math.max(...snap.readyTasks.map(r =>\n (snap.now - new Date(r.goal.createdAt).getTime()) / 3_600_000,\n ));\n // Both signals independently drag satisfaction down.\n // v5.0.0: floor backlog satisfaction at 0.15 so extreme backlogs\n // (e.g. 1000+ zombie goals) don't drive hunger to absolute zero,\n // which causes SOMA to panic-propose even more goals.\n const backlogSatisfaction = Math.max(0.15, invertedSigmoid(readyCount, 5, 0.35));\n const ageSatisfaction = invertedSigmoid(oldestAgeHours, 4, 0.5);\n const satisfaction = Math.min(backlogSatisfaction, ageSatisfaction);\n return {\n satisfaction,\n inputs: { readyCount, oldestAgeHours: Math.round(oldestAgeHours * 10) / 10 },\n };\n },\n describe: (s, inputs) => {\n const count = (inputs?.readyCount as number) ?? 0;\n const age = (inputs?.oldestAgeHours as number) ?? 0;\n if (count === 0) return 'backlog empty';\n if (s < 0.3) return `backlog ${count}, oldest ${age.toFixed(1)}h — elevated`;\n if (s < 0.6) return `backlog ${count}, oldest ${age.toFixed(1)}h`;\n return `backlog ${count} — fed`;\n },\n};\n\nconst CURIOSITY: DriveDefinition = {\n id: 'curiosity',\n label: 'Curiosity',\n defaultSetpoint: 0.5,\n weight: 0.8,\n compute: (snap) => {\n // Novelty = task-type diversity across recent trajectories.\n // Few distinct task types → elevated curiosity (stale). Rich variety\n // → satiated. We compose two signals:\n // 1) coverage: how many distinct types relative to a target of 5\n // 2) balance: how evenly distributed those types are (1 − gini)\n // Satisfaction = min(coverage, balance) so either deficit pulls it\n // down. Low sample counts default to middling satisfaction.\n if (snap.trajectories.length < 5) {\n return { satisfaction: 0.6, inputs: { trajectoryCount: snap.trajectories.length } };\n }\n const typeCounts: Record<string, number> = {};\n for (const t of snap.trajectories) {\n typeCounts[t.taskType || 'unknown'] = (typeCounts[t.taskType || 'unknown'] || 0) + 1;\n }\n const typeCount = Object.keys(typeCounts).length;\n const coverage = clamp01(typeCount / 5);\n const counts = Object.values(typeCounts);\n const balance = typeCount <= 1 ? 0 : clamp01(1 - gini(counts));\n const diversitySat = typeCount <= 1 ? coverage : Math.min(coverage, balance);\n\n // v4.9.0: unresolved error patterns are a form of \"task-type\n // novelty the organism hasn't figured out yet.\" More than a\n // handful of unresolved patterns pulls Curiosity toward an\n // investigate-and-improve proposal (feeds Self-Improve pipeline).\n // Scales 0→10+ patterns linearly.\n let errorPatternSat = 1;\n if (typeof snap.unresolvedErrorPatterns === 'number' && snap.unresolvedErrorPatterns > 2) {\n errorPatternSat = clamp01(1 - (snap.unresolvedErrorPatterns - 2) / 10);\n }\n\n const satisfaction = Math.min(diversitySat, errorPatternSat);\n return {\n satisfaction,\n inputs: {\n trajectoryCount: snap.trajectories.length,\n taskTypes: typeCount,\n coverage: Math.round(coverage * 100) / 100,\n balance: Math.round(balance * 100) / 100,\n unresolvedErrorPatterns: snap.unresolvedErrorPatterns ?? 0,\n errorPatternSat: Math.round(errorPatternSat * 100) / 100,\n },\n };\n },\n describe: (s, inputs) => {\n const types = (inputs?.taskTypes as number) ?? 0;\n const patterns = (inputs?.unresolvedErrorPatterns as number) ?? 0;\n if (patterns >= 5) return `${patterns} unresolved error patterns — needs investigation`;\n if (s < 0.3) return `stuck in ${types} task type(s) — stale`;\n if (s < 0.6) return `${types} task type(s) — could use novelty`;\n return `${types} distinct task type(s) — engaged`;\n },\n};\n\nconst SAFETY: DriveDefinition = {\n id: 'safety',\n label: 'Safety',\n defaultSetpoint: 0.8,\n weight: 1.6,\n compute: (snap) => {\n // Budget runway: min runway across all enabled budgets.\n let budgetSatisfaction = 1;\n const relevantBudgets = snap.budgets.filter(b => b.enabled && b.limitUsd > 0);\n if (relevantBudgets.length > 0) {\n const runways = relevantBudgets.map(b => clamp01(1 - b.currentSpend / b.limitUsd));\n budgetSatisfaction = Math.min(...runways);\n }\n // Recent error rate from last 100 CPRuns in the last 24h.\n const dayMs = 86_400_000;\n const recent = snap.recentRuns.filter(r =>\n snap.now - new Date(r.startedAt).getTime() < dayMs,\n );\n let errorSatisfaction = 1;\n if (recent.length >= 5) {\n const errors = recent.filter(r => r.status === 'error' || r.status === 'failed').length;\n errorSatisfaction = clamp01(1 - errors / recent.length);\n }\n\n // v4.9.0: VRAM saturation above 85% presses Safety. Below 85%,\n // saturation has no effect. Scales linearly 85%–100% → sat 1→0.\n let vramSatisfaction = 1;\n if (snap.vramSaturation !== undefined) {\n if (snap.vramSaturation > 0.85) {\n vramSatisfaction = clamp01(1 - (snap.vramSaturation - 0.85) / 0.15);\n }\n }\n\n // v4.9.0: gateway-level telemetry error rate (LLM/tool calls).\n // Independent of CPRun error rate — catches tool failures that\n // never bubbled up to Command Post.\n let telemetrySatisfaction = 1;\n if (snap.telemetryErrorRate !== undefined) {\n telemetrySatisfaction = clamp01(1 - snap.telemetryErrorRate * 2);\n }\n\n // Safety is a min-aggregate — the weakest link dominates.\n const satisfaction = Math.min(\n budgetSatisfaction,\n errorSatisfaction,\n vramSatisfaction,\n telemetrySatisfaction,\n );\n return {\n satisfaction,\n inputs: {\n budgetSatisfaction: Math.round(budgetSatisfaction * 100) / 100,\n errorSatisfaction: Math.round(errorSatisfaction * 100) / 100,\n vramSatisfaction: Math.round(vramSatisfaction * 100) / 100,\n telemetrySatisfaction: Math.round(telemetrySatisfaction * 100) / 100,\n recentRunCount: recent.length,\n vramSaturationPct: snap.vramSaturation !== undefined ? Math.round(snap.vramSaturation * 100) : null,\n telemetryErrorRatePct: snap.telemetryErrorRate !== undefined ? Math.round(snap.telemetryErrorRate * 100) : null,\n },\n };\n },\n describe: (s, inputs) => {\n const budget = (inputs?.budgetSatisfaction as number) ?? 1;\n const errors = (inputs?.errorSatisfaction as number) ?? 1;\n const vram = (inputs?.vramSatisfaction as number) ?? 1;\n const tel = (inputs?.telemetrySatisfaction as number) ?? 1;\n if (budget < 0.2) return 'budget runway critical';\n if (vram < 0.4) return `VRAM saturated (${inputs?.vramSaturationPct}%) — spawns at risk`;\n if (tel < 0.5) return `gateway error rate elevated (${inputs?.telemetryErrorRatePct}%)`;\n if (errors < 0.5) return 'elevated error rate in recent runs';\n if (s < 0.6) return 'safety posture weakening';\n return 'safety posture healthy';\n },\n};\n\nconst SOCIAL: DriveDefinition = {\n id: 'social',\n label: 'Social',\n defaultSetpoint: 0.7,\n weight: 0.7,\n compute: (snap) => {\n // v4.8.1: ignore specialists that were registered but never given\n // work (`totalTasksCompleted === 0`). They have nothing to heartbeat\n // about; counting them as \"unresponsive\" was a false negative.\n const eligible = snap.agents.filter(a => (a.totalTasksCompleted ?? 0) > 0 || a.status === 'active');\n if (eligible.length === 0) {\n return { satisfaction: 0.9, inputs: { totalAgents: snap.agents.length, staleAgents: 0 } };\n }\n const hourMs = 3_600_000;\n const stale = eligible.filter(a =>\n snap.now - new Date(a.lastHeartbeat).getTime() > hourMs,\n ).length;\n const satisfaction = clamp01(1 - stale / eligible.length);\n return {\n satisfaction,\n inputs: { totalAgents: eligible.length, staleAgents: stale },\n };\n },\n describe: (_s, inputs) => {\n const total = (inputs?.totalAgents as number) ?? 0;\n const stale = (inputs?.staleAgents as number) ?? 0;\n if (stale === 0) return `${total} agent(s) all alive`;\n return `${stale}/${total} agent(s) unresponsive`;\n },\n};\n\nexport const DRIVES: DriveDefinition[] = [PURPOSE, HUNGER, CURIOSITY, SAFETY, SOCIAL];\n\n// ── Snapshot builder ─────────────────────────────────────────────\n\n/** Build a DriveSnapshot by reading current TITAN state. Synchronous —\n * all inputs are in-memory or cheap disk reads. */\nexport function buildSnapshot(): DriveSnapshot {\n const goals = listGoals();\n let readyTasks: DriveSnapshot['readyTasks'] = [];\n try { readyTasks = getReadyTasks(); } catch { /* empty */ }\n const agents = getRegisteredAgents();\n const budgets = getBudgetPolicies();\n let recentRuns: CPRun[] = [];\n try { recentRuns = listRuns(undefined, 100); } catch { /* empty */ }\n let trajectories: TaskTrajectory[] = [];\n try { trajectories = getRecentTrajectories(100); } catch { /* empty */ }\n\n // v4.9.0 — pull optional closed-loop signals. Each wrapped in try so\n // drive tick never fails if a downstream module is missing or throws.\n\n let vramSaturation: number | undefined;\n try {\n const vr = readCachedVRAMSignal();\n if (vr !== null) vramSaturation = vr;\n } catch { /* no signal */ }\n\n let telemetryErrorRate: number | undefined;\n let telemetryTotalRequests: number | undefined;\n try {\n const metrics = readCachedTelemetrySignal();\n if (metrics) {\n telemetryErrorRate = metrics.errorRate;\n telemetryTotalRequests = metrics.totalRequests;\n }\n } catch { /* no signal */ }\n\n let unresolvedErrorPatterns: number | undefined;\n try {\n const patterns = readUnresolvedErrorPatternCount();\n if (patterns !== null) unresolvedErrorPatterns = patterns;\n } catch { /* no signal */ }\n\n return {\n now: Date.now(),\n goals,\n readyTasks,\n recentRuns,\n budgets,\n agents,\n trajectories,\n vramSaturation,\n telemetryErrorRate,\n telemetryTotalRequests,\n unresolvedErrorPatterns,\n };\n}\n\n// ── v4.9.0 signal readers ──────────────────────────────────────────\n\n/**\n * Reads the VRAM orchestrator's last cached snapshot (no refresh) and\n * returns used/total saturation as 0–1. Returns null when no GPU is\n * attached or the orchestrator hasn't polled yet.\n *\n * Synchronous: buildSnapshot() is called in the drive-tick hot path\n * every 60s, and we don't want to add an async nvidia-smi probe on\n * top of the existing 10s VRAM refresh.\n */\nfunction readCachedVRAMSignal(): number | null {\n try {\n // Dynamic require-like import from the already-loaded module\n // singleton. If VRAM module hasn't been initialized (e.g., in\n // tests), just return null.\n const mod = (globalThis as unknown as { __titan_vram_last?: { freeMB?: number; totalMB?: number; usedMB?: number } }).__titan_vram_last;\n if (!mod) return null;\n const total = mod.totalMB ?? 0;\n if (!Number.isFinite(total) || total <= 0) return null;\n const used = Number.isFinite(mod.usedMB) ? mod.usedMB! : (total - (mod.freeMB ?? total));\n const pct = used / total;\n if (!Number.isFinite(pct)) return null;\n return Math.max(0, Math.min(1, pct));\n } catch {\n return null;\n }\n}\n\n/** Reads the gateway metrics layer's summary (sync, in-memory). */\nfunction readCachedTelemetrySignal(): { errorRate: number; totalRequests: number } | null {\n try {\n // Using require-style resolve so tests that mock the drives\n // module don't pull in the metrics graph.\n const mod = (globalThis as unknown as { __titan_metrics_summary?: () => { totalRequests?: number; errorRate?: number } | null }).__titan_metrics_summary;\n if (typeof mod !== 'function') return null;\n const s = mod();\n if (!s || typeof s.totalRequests !== 'number' || typeof s.errorRate !== 'number') return null;\n // Only treat the signal as meaningful once we have enough samples.\n if (s.totalRequests < 10) return null;\n return { errorRate: s.errorRate, totalRequests: s.totalRequests };\n } catch {\n return null;\n }\n}\n\n/** Reads count of unresolved error patterns from the learning layer. */\nfunction readUnresolvedErrorPatternCount(): number | null {\n try {\n const mod = (globalThis as unknown as { __titan_unresolved_error_patterns?: () => number }).__titan_unresolved_error_patterns;\n if (typeof mod !== 'function') return null;\n const n = mod();\n if (typeof n !== 'number' || !Number.isFinite(n)) return null;\n return n;\n } catch {\n return null;\n }\n}\n\n// ── Drive state computation ──────────────────────────────────────\n\n/** Compute all drive states for a given snapshot, applying per-drive\n * setpoint + weight overrides + disabled-drive filter (all from\n * config.organism.{driveSetpoints,driveWeights,disabledDrives}). */\nexport function computeAllDrives(\n snapshot: DriveSnapshot,\n setpointOverrides: Partial<Record<DriveId, number>> = {},\n weightOverrides: Partial<Record<DriveId, number>> = {},\n disabledDrives: DriveId[] = [],\n): DriveState[] {\n const out: DriveState[] = [];\n const disabled = new Set(disabledDrives);\n for (const def of DRIVES) {\n if (disabled.has(def.id)) continue;\n const { satisfaction, inputs } = def.compute(snapshot);\n const setpoint = setpointOverrides[def.id] ?? def.defaultSetpoint;\n const weight = weightOverrides[def.id] ?? def.weight;\n const pressure = satisfaction < setpoint\n ? (setpoint - satisfaction) * weight\n : 0;\n out.push({\n id: def.id,\n label: def.label,\n satisfaction: clamp01(satisfaction),\n setpoint,\n pressure,\n weight,\n inputs,\n description: def.describe(satisfaction, inputs),\n });\n }\n return out;\n}\n\n// ── Persistence ──────────────────────────────────────────────────\n\nexport interface PersistedDriveHistory {\n latest: DriveTickResult;\n /** Ring buffer of last ≤1440 ticks (~24h at 60s cadence). */\n history: Array<{ timestamp: string; satisfactions: Record<DriveId, number> }>;\n}\n\n/** Load the last-written drive state (if any). Returns null on first run. */\nexport function loadDriveHistory(): PersistedDriveHistory | null {\n if (!existsSync(DRIVE_STATE_PATH)) return null;\n try {\n return JSON.parse(readFileSync(DRIVE_STATE_PATH, 'utf-8')) as PersistedDriveHistory;\n } catch (err) {\n logger.warn(COMPONENT, `drive-state.json corrupt: ${(err as Error).message}`);\n return null;\n }\n}\n\n/** Persist the tick. Ring-buffers history to a max of 1440 entries. */\nexport function saveDriveTick(tick: DriveTickResult): void {\n try {\n ensureDir(TITAN_HOME);\n const existing = loadDriveHistory();\n const satisfactions: Record<string, number> = {};\n for (const d of tick.drives) satisfactions[d.id] = d.satisfaction;\n const history = (existing?.history || []).concat([{\n timestamp: tick.timestamp,\n satisfactions: satisfactions as Record<DriveId, number>,\n }]);\n const trimmed = history.length > 1440 ? history.slice(-1440) : history;\n const payload: PersistedDriveHistory = { latest: tick, history: trimmed };\n writeFileSync(DRIVE_STATE_PATH, JSON.stringify(payload, null, 2), 'utf-8');\n } catch (err) {\n logger.warn(COMPONENT, `Failed to save drive state: ${(err as Error).message}`);\n }\n}\n\n// ── One-call convenience ─────────────────────────────────────────\n\n/** Build snapshot → compute drives → package as a DriveTickResult. Does NOT\n * persist; callers decide whether to save (daemon tick does; read-only API\n * endpoints don't). */\nexport function runDriveTick(\n setpointOverrides: Partial<Record<DriveId, number>> = {},\n weightOverrides: Partial<Record<DriveId, number>> = {},\n disabledDrives: DriveId[] = [],\n): DriveTickResult {\n const snapshot = buildSnapshot();\n const drives = computeAllDrives(snapshot, setpointOverrides, weightOverrides, disabledDrives);\n const totalPressure = drives.reduce((sum, d) => sum + d.pressure, 0);\n const dominantDrives = drives\n .filter(d => d.pressure > 0)\n .sort((a, b) => b.pressure - a.pressure)\n .slice(0, 2)\n .map(d => d.id);\n return {\n timestamp: new Date().toISOString(),\n drives,\n totalPressure,\n dominantDrives,\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/organism/drives.ts"],"sourcesContent":["/**\n * TITAN — Drive Layer (Soma organism / endocrine system)\n *\n * Five homeostatic drives. Each computes a 0-1 \"satisfaction\" from existing\n * TITAN telemetry — no new instrumentation. When satisfaction dips below the\n * drive's setpoint, pressure accumulates. Cross-drive pressure fusion (see\n * pressure.ts) eventually produces a soma_proposal for human approval.\n *\n * Gated by config.organism.enabled — this module is inert when disabled.\n *\n * DRIVES SHIPPED IN v4.0:\n * Purpose — alignment with priority-1 goals\n * Hunger — backlog size vs. throughput\n * Curiosity — task-type diversity in recent trajectories\n * Safety — budget runway + recent error rate\n * Social — stale agent fraction\n *\n * DEFERRED TO v4.1+:\n * Hygiene — needs npm test + git status shell hooks\n */\nimport { existsSync, readFileSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport { ensureDir } from '../utils/helpers.js';\nimport { listGoals, getReadyTasks, type Goal } from '../agent/goals.js';\nimport { getRegisteredAgents, getBudgetPolicies, listRuns, type RegisteredAgent, type BudgetPolicy, type CPRun } from '../agent/commandPost.js';\nimport { getRecentTrajectories, type TaskTrajectory } from '../agent/trajectoryLogger.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'Drives';\nconst DRIVE_STATE_PATH = join(TITAN_HOME, 'drive-state.json');\n\n// ── Types ────────────────────────────────────────────────────────\n\nexport type DriveId = 'purpose' | 'hunger' | 'curiosity' | 'safety' | 'social';\n\nexport interface DriveSnapshot {\n /** Timestamp of the snapshot in epoch ms. */\n now: number;\n /** All goals from goals.ts. */\n goals: Goal[];\n /** Output of getReadyTasks() — ready-to-execute subtasks. */\n readyTasks: Array<{ goal: Goal; subtask: Goal['subtasks'][number] }>;\n /** Recent CPRun history (up to 100 most recent). */\n recentRuns: CPRun[];\n /** Active budget policies. */\n budgets: BudgetPolicy[];\n /** All registered agents. */\n agents: RegisteredAgent[];\n /** Last 100 trajectory entries. */\n trajectories: TaskTrajectory[];\n /**\n * v4.9.0: fraction of GPU VRAM in use (0–1). Undefined when no GPU\n * is attached or the orchestrator hasn't refreshed yet.\n */\n vramSaturation?: number;\n /**\n * v4.9.0: error rate across recent LLM / tool calls from the\n * gateway metrics layer (0–1). Undefined when metrics are unavailable.\n */\n telemetryErrorRate?: number;\n /** v4.9.0: total LLM + tool-call requests since gateway start. */\n telemetryTotalRequests?: number;\n /**\n * v4.9.0: count of error patterns the learning layer has accumulated\n * but not yet resolved. High count pulls Curiosity toward an\n * investigate/improve proposal.\n */\n unresolvedErrorPatterns?: number;\n /**\n * v5.3.2 (Phase 8 / Track B): timestamp of the most recent successful\n * Facebook page post in epoch ms, or null if TITAN hasn't posted yet\n * since fb-autopilot was enabled. Sourced from\n * `~/.titan/fb-autopilot-state.json`. Drives the Social-drive's\n * \"social media presence\" factor — without this, social pressure was\n * 100% about agent heartbeat staleness, which the README implied was\n * about social posting. Now both factors blend.\n */\n lastFacebookPostAt?: number | null;\n}\n\nexport interface DriveDefinition {\n id: DriveId;\n label: string;\n /** Satisfaction level below which this drive starts contributing pressure. */\n defaultSetpoint: number;\n /** Relative weight in cross-drive pressure fusion (1.0 is baseline). */\n weight: number;\n /** Pure function — computes satisfaction 0-1 from the snapshot. */\n compute: (snapshot: DriveSnapshot) => { satisfaction: number; inputs?: Record<string, unknown> };\n /** Short human-readable explanation used in prompts, UI tooltips, and activity feed. */\n describe: (satisfaction: number, inputs?: Record<string, unknown>) => string;\n}\n\nexport interface DriveState {\n id: DriveId;\n label: string;\n satisfaction: number;\n setpoint: number;\n /** 0 when satisfaction >= setpoint, else (setpoint − satisfaction) × weight. */\n pressure: number;\n weight: number;\n inputs?: Record<string, unknown>;\n description: string;\n}\n\nexport interface DriveTickResult {\n timestamp: string;\n drives: DriveState[];\n totalPressure: number;\n dominantDrives: DriveId[];\n}\n\n// ── Numeric helpers ──────────────────────────────────────────────\n\n/** Clamp to [0,1]. */\nfunction clamp01(v: number): number {\n if (!Number.isFinite(v)) return 0;\n return Math.max(0, Math.min(1, v));\n}\n\n/** Sigmoid centred on `mid` with slope `k`. Returns high → 1 when x is low. */\nfunction invertedSigmoid(x: number, mid: number, k = 1): number {\n return clamp01(1 / (1 + Math.exp(k * (x - mid))));\n}\n\n/** Gini coefficient of a count distribution. 0 = uniform, 1 = all same task. */\nfunction gini(counts: number[]): number {\n if (counts.length === 0) return 0;\n const n = counts.length;\n const sum = counts.reduce((a, b) => a + b, 0);\n if (sum === 0) return 0;\n const sorted = [...counts].sort((a, b) => a - b);\n let cum = 0;\n for (let i = 0; i < n; i++) cum += (i + 1) * sorted[i];\n return clamp01((2 * cum) / (n * sum) - (n + 1) / n);\n}\n\n// ── Drive definitions ────────────────────────────────────────────\n\nconst PURPOSE: DriveDefinition = {\n id: 'purpose',\n label: 'Purpose',\n defaultSetpoint: 0.7,\n weight: 1.4,\n compute: (snap) => {\n // Priority-1 goals tagged as high-priority. Satisfaction reflects how\n // recently any of them progressed. No priority-1 goals → satiated\n // (nothing to worry about).\n const priorityOne = snap.goals.filter(g =>\n g.status === 'active' && g.priority === 1,\n );\n if (priorityOne.length === 0) {\n return { satisfaction: 0.9, inputs: { priorityOneCount: 0 } };\n }\n const latest = Math.max(...priorityOne.map(g =>\n new Date(g.updatedAt || g.createdAt).getTime(),\n ));\n const hoursSince = Math.max(0, (snap.now - latest) / 3_600_000);\n const satisfaction = clamp01(1 - hoursSince / 24);\n return {\n satisfaction,\n inputs: { priorityOneCount: priorityOne.length, hoursSinceProgress: Math.round(hoursSince * 10) / 10 },\n };\n },\n describe: (s, inputs) => {\n const count = (inputs?.priorityOneCount as number) ?? 0;\n if (count === 0) return 'no priority-1 goals in flight';\n const hours = (inputs?.hoursSinceProgress as number) ?? 0;\n if (s < 0.3) return `${count} priority-1 goal(s) stalled — no progress in ${hours.toFixed(1)}h`;\n if (s < 0.6) return `${count} priority-1 goal(s) need attention`;\n return `${count} priority-1 goal(s) on track`;\n },\n};\n\nconst HUNGER: DriveDefinition = {\n id: 'hunger',\n label: 'Hunger',\n defaultSetpoint: 0.6,\n weight: 1.0,\n compute: (snap) => {\n const readyCount = snap.readyTasks.length;\n // Oldest ready subtask age in hours, using parent goal createdAt as proxy.\n const oldestAgeHours = snap.readyTasks.length === 0\n ? 0\n : Math.max(...snap.readyTasks.map(r =>\n (snap.now - new Date(r.goal.createdAt).getTime()) / 3_600_000,\n ));\n // Both signals independently drag satisfaction down.\n // v5.0.0: floor backlog satisfaction at 0.15 so extreme backlogs\n // (e.g. 1000+ zombie goals) don't drive hunger to absolute zero,\n // which causes SOMA to panic-propose even more goals.\n const backlogSatisfaction = Math.max(0.15, invertedSigmoid(readyCount, 5, 0.35));\n const ageSatisfaction = invertedSigmoid(oldestAgeHours, 4, 0.5);\n const satisfaction = Math.min(backlogSatisfaction, ageSatisfaction);\n return {\n satisfaction,\n inputs: { readyCount, oldestAgeHours: Math.round(oldestAgeHours * 10) / 10 },\n };\n },\n describe: (s, inputs) => {\n const count = (inputs?.readyCount as number) ?? 0;\n const age = (inputs?.oldestAgeHours as number) ?? 0;\n if (count === 0) return 'backlog empty';\n if (s < 0.3) return `backlog ${count}, oldest ${age.toFixed(1)}h — elevated`;\n if (s < 0.6) return `backlog ${count}, oldest ${age.toFixed(1)}h`;\n return `backlog ${count} — fed`;\n },\n};\n\nconst CURIOSITY: DriveDefinition = {\n id: 'curiosity',\n label: 'Curiosity',\n defaultSetpoint: 0.5,\n weight: 0.8,\n compute: (snap) => {\n // Novelty = task-type diversity across recent trajectories.\n // Few distinct task types → elevated curiosity (stale). Rich variety\n // → satiated. We compose two signals:\n // 1) coverage: how many distinct types relative to a target of 5\n // 2) balance: how evenly distributed those types are (1 − gini)\n // Satisfaction = min(coverage, balance) so either deficit pulls it\n // down. Low sample counts default to middling satisfaction.\n if (snap.trajectories.length < 5) {\n return { satisfaction: 0.6, inputs: { trajectoryCount: snap.trajectories.length } };\n }\n const typeCounts: Record<string, number> = {};\n for (const t of snap.trajectories) {\n typeCounts[t.taskType || 'unknown'] = (typeCounts[t.taskType || 'unknown'] || 0) + 1;\n }\n const typeCount = Object.keys(typeCounts).length;\n const coverage = clamp01(typeCount / 5);\n const counts = Object.values(typeCounts);\n const balance = typeCount <= 1 ? 0 : clamp01(1 - gini(counts));\n const diversitySat = typeCount <= 1 ? coverage : Math.min(coverage, balance);\n\n // v4.9.0: unresolved error patterns are a form of \"task-type\n // novelty the organism hasn't figured out yet.\" More than a\n // handful of unresolved patterns pulls Curiosity toward an\n // investigate-and-improve proposal (feeds Self-Improve pipeline).\n // Scales 0→10+ patterns linearly.\n let errorPatternSat = 1;\n if (typeof snap.unresolvedErrorPatterns === 'number' && snap.unresolvedErrorPatterns > 2) {\n errorPatternSat = clamp01(1 - (snap.unresolvedErrorPatterns - 2) / 10);\n }\n\n const satisfaction = Math.min(diversitySat, errorPatternSat);\n return {\n satisfaction,\n inputs: {\n trajectoryCount: snap.trajectories.length,\n taskTypes: typeCount,\n coverage: Math.round(coverage * 100) / 100,\n balance: Math.round(balance * 100) / 100,\n unresolvedErrorPatterns: snap.unresolvedErrorPatterns ?? 0,\n errorPatternSat: Math.round(errorPatternSat * 100) / 100,\n },\n };\n },\n describe: (s, inputs) => {\n const types = (inputs?.taskTypes as number) ?? 0;\n const patterns = (inputs?.unresolvedErrorPatterns as number) ?? 0;\n if (patterns >= 5) return `${patterns} unresolved error patterns — needs investigation`;\n if (s < 0.3) return `stuck in ${types} task type(s) — stale`;\n if (s < 0.6) return `${types} task type(s) — could use novelty`;\n return `${types} distinct task type(s) — engaged`;\n },\n};\n\nconst SAFETY: DriveDefinition = {\n id: 'safety',\n label: 'Safety',\n defaultSetpoint: 0.8,\n weight: 1.6,\n compute: (snap) => {\n // Budget runway: min runway across all enabled budgets.\n let budgetSatisfaction = 1;\n const relevantBudgets = snap.budgets.filter(b => b.enabled && b.limitUsd > 0);\n if (relevantBudgets.length > 0) {\n const runways = relevantBudgets.map(b => clamp01(1 - b.currentSpend / b.limitUsd));\n budgetSatisfaction = Math.min(...runways);\n }\n // Recent error rate from last 100 CPRuns in the last 24h.\n const dayMs = 86_400_000;\n const recent = snap.recentRuns.filter(r =>\n snap.now - new Date(r.startedAt).getTime() < dayMs,\n );\n let errorSatisfaction = 1;\n if (recent.length >= 5) {\n const errors = recent.filter(r => r.status === 'error' || r.status === 'failed').length;\n errorSatisfaction = clamp01(1 - errors / recent.length);\n }\n\n // v4.9.0: VRAM saturation above 85% presses Safety. Below 85%,\n // saturation has no effect. Scales linearly 85%–100% → sat 1→0.\n let vramSatisfaction = 1;\n if (snap.vramSaturation !== undefined) {\n if (snap.vramSaturation > 0.85) {\n vramSatisfaction = clamp01(1 - (snap.vramSaturation - 0.85) / 0.15);\n }\n }\n\n // v4.9.0: gateway-level telemetry error rate (LLM/tool calls).\n // Independent of CPRun error rate — catches tool failures that\n // never bubbled up to Command Post.\n let telemetrySatisfaction = 1;\n if (snap.telemetryErrorRate !== undefined) {\n telemetrySatisfaction = clamp01(1 - snap.telemetryErrorRate * 2);\n }\n\n // Safety is a min-aggregate — the weakest link dominates.\n const satisfaction = Math.min(\n budgetSatisfaction,\n errorSatisfaction,\n vramSatisfaction,\n telemetrySatisfaction,\n );\n return {\n satisfaction,\n inputs: {\n budgetSatisfaction: Math.round(budgetSatisfaction * 100) / 100,\n errorSatisfaction: Math.round(errorSatisfaction * 100) / 100,\n vramSatisfaction: Math.round(vramSatisfaction * 100) / 100,\n telemetrySatisfaction: Math.round(telemetrySatisfaction * 100) / 100,\n recentRunCount: recent.length,\n vramSaturationPct: snap.vramSaturation !== undefined ? Math.round(snap.vramSaturation * 100) : null,\n telemetryErrorRatePct: snap.telemetryErrorRate !== undefined ? Math.round(snap.telemetryErrorRate * 100) : null,\n },\n };\n },\n describe: (s, inputs) => {\n const budget = (inputs?.budgetSatisfaction as number) ?? 1;\n const errors = (inputs?.errorSatisfaction as number) ?? 1;\n const vram = (inputs?.vramSatisfaction as number) ?? 1;\n const tel = (inputs?.telemetrySatisfaction as number) ?? 1;\n if (budget < 0.2) return 'budget runway critical';\n if (vram < 0.4) return `VRAM saturated (${inputs?.vramSaturationPct}%) — spawns at risk`;\n if (tel < 0.5) return `gateway error rate elevated (${inputs?.telemetryErrorRatePct}%)`;\n if (errors < 0.5) return 'elevated error rate in recent runs';\n if (s < 0.6) return 'safety posture weakening';\n return 'safety posture healthy';\n },\n};\n\nconst SOCIAL: DriveDefinition = {\n id: 'social',\n label: 'Social',\n defaultSetpoint: 0.7,\n weight: 0.7,\n compute: (snap) => {\n // v4.8.1: ignore specialists that were registered but never given\n // work (`totalTasksCompleted === 0`). They have nothing to heartbeat\n // about; counting them as \"unresponsive\" was a false negative.\n const eligible = snap.agents.filter(a => (a.totalTasksCompleted ?? 0) > 0 || a.status === 'active');\n const hourMs = 3_600_000;\n\n // ── Factor 1: agent liveness (legacy) ───────────────────────\n let agentSat = 0.9; // healthy default when no eligible agents\n let stale = 0;\n if (eligible.length > 0) {\n stale = eligible.filter(a =>\n snap.now - new Date(a.lastHeartbeat).getTime() > hourMs,\n ).length;\n agentSat = clamp01(1 - stale / eligible.length);\n }\n\n // ── Factor 2: social-media presence (v5.3.2) ────────────────\n // The README promises a Social drive that asks \"should I post or\n // reply?\" — that requires the drive to actually track posting\n // cadence, not just agent heartbeat. lastFacebookPostAt is wired\n // through buildSnapshot from fb-autopilot-state.json.\n //\n // Saturates at 24h: a drought of 24h+ with no post pulls\n // satisfaction to 0; a fresh post within the last hour keeps it\n // near 1. Linear in between. If lastFacebookPostAt is null/missing\n // (autopilot never ran or never posted), we treat the gap as\n // \"long\" — encourages a first post when a user enables FB.\n const POST_DROUGHT_HOURS = 24;\n let postSat: number;\n let hoursSinceLastPost: number;\n if (snap.lastFacebookPostAt && snap.lastFacebookPostAt > 0) {\n hoursSinceLastPost = Math.max(0, (snap.now - snap.lastFacebookPostAt) / hourMs);\n postSat = clamp01(1 - hoursSinceLastPost / POST_DROUGHT_HOURS);\n } else {\n // Treat \"never posted\" as ~12h drought. Don't peg to 0 —\n // organism shouldn't fire a Soma proposal the moment FB is\n // enabled before the user has even configured anything.\n hoursSinceLastPost = POST_DROUGHT_HOURS / 2;\n postSat = 0.5;\n }\n\n // Equal-weight blend. Either factor low → drive deficits.\n const satisfaction = clamp01((agentSat + postSat) / 2);\n\n return {\n satisfaction,\n inputs: {\n totalAgents: eligible.length,\n staleAgents: stale,\n hoursSinceLastPost: Number(hoursSinceLastPost.toFixed(2)),\n agentSatisfaction: Number(agentSat.toFixed(3)),\n postSatisfaction: Number(postSat.toFixed(3)),\n },\n };\n },\n describe: (_s, inputs) => {\n const total = (inputs?.totalAgents as number) ?? 0;\n const stale = (inputs?.staleAgents as number) ?? 0;\n const hoursSince = (inputs?.hoursSinceLastPost as number) ?? 0;\n const reasons: string[] = [];\n if (stale > 0) reasons.push(`${stale}/${total} agent(s) unresponsive`);\n if (hoursSince >= 12) reasons.push(`${Math.round(hoursSince)}h since last FB post`);\n if (reasons.length === 0) return `${total} agent(s) all alive · posted recently`;\n return reasons.join(' · ');\n },\n};\n\nexport const DRIVES: DriveDefinition[] = [PURPOSE, HUNGER, CURIOSITY, SAFETY, SOCIAL];\n\n// ── Snapshot builder ─────────────────────────────────────────────\n\n/** Build a DriveSnapshot by reading current TITAN state. Synchronous —\n * all inputs are in-memory or cheap disk reads. */\nexport function buildSnapshot(): DriveSnapshot {\n const goals = listGoals();\n let readyTasks: DriveSnapshot['readyTasks'] = [];\n try { readyTasks = getReadyTasks(); } catch { /* empty */ }\n const agents = getRegisteredAgents();\n const budgets = getBudgetPolicies();\n let recentRuns: CPRun[] = [];\n try { recentRuns = listRuns(undefined, 100); } catch { /* empty */ }\n let trajectories: TaskTrajectory[] = [];\n try { trajectories = getRecentTrajectories(100); } catch { /* empty */ }\n\n // v4.9.0 — pull optional closed-loop signals. Each wrapped in try so\n // drive tick never fails if a downstream module is missing or throws.\n\n let vramSaturation: number | undefined;\n try {\n const vr = readCachedVRAMSignal();\n if (vr !== null) vramSaturation = vr;\n } catch { /* no signal */ }\n\n let telemetryErrorRate: number | undefined;\n let telemetryTotalRequests: number | undefined;\n try {\n const metrics = readCachedTelemetrySignal();\n if (metrics) {\n telemetryErrorRate = metrics.errorRate;\n telemetryTotalRequests = metrics.totalRequests;\n }\n } catch { /* no signal */ }\n\n let unresolvedErrorPatterns: number | undefined;\n try {\n const patterns = readUnresolvedErrorPatternCount();\n if (patterns !== null) unresolvedErrorPatterns = patterns;\n } catch { /* no signal */ }\n\n // v5.3.2 Track B: read fb-autopilot's last successful post timestamp so\n // the Social drive's \"social media presence\" factor has real input.\n // Best-effort: never throws — Social drive falls back to a neutral\n // \"12h drought\" when this is absent (see SOCIAL.compute).\n let lastFacebookPostAt: number | null = null;\n try {\n const fbStatePath = join(TITAN_HOME, 'fb-autopilot-state.json');\n if (existsSync(fbStatePath)) {\n const raw = readFileSync(fbStatePath, 'utf-8');\n const state = JSON.parse(raw) as { lastPostAt?: string | null };\n if (state.lastPostAt) {\n const parsed = new Date(state.lastPostAt).getTime();\n if (Number.isFinite(parsed)) lastFacebookPostAt = parsed;\n }\n }\n } catch { /* ok — autopilot state missing or malformed; fall back */ }\n\n return {\n now: Date.now(),\n goals,\n readyTasks,\n recentRuns,\n budgets,\n agents,\n trajectories,\n vramSaturation,\n telemetryErrorRate,\n telemetryTotalRequests,\n unresolvedErrorPatterns,\n lastFacebookPostAt,\n };\n}\n\n// ── v4.9.0 signal readers ──────────────────────────────────────────\n\n/**\n * Reads the VRAM orchestrator's last cached snapshot (no refresh) and\n * returns used/total saturation as 0–1. Returns null when no GPU is\n * attached or the orchestrator hasn't polled yet.\n *\n * Synchronous: buildSnapshot() is called in the drive-tick hot path\n * every 60s, and we don't want to add an async nvidia-smi probe on\n * top of the existing 10s VRAM refresh.\n */\nfunction readCachedVRAMSignal(): number | null {\n try {\n // Dynamic require-like import from the already-loaded module\n // singleton. If VRAM module hasn't been initialized (e.g., in\n // tests), just return null.\n const mod = (globalThis as unknown as { __titan_vram_last?: { freeMB?: number; totalMB?: number; usedMB?: number } }).__titan_vram_last;\n if (!mod) return null;\n const total = mod.totalMB ?? 0;\n if (!Number.isFinite(total) || total <= 0) return null;\n const used = Number.isFinite(mod.usedMB) ? mod.usedMB! : (total - (mod.freeMB ?? total));\n const pct = used / total;\n if (!Number.isFinite(pct)) return null;\n return Math.max(0, Math.min(1, pct));\n } catch {\n return null;\n }\n}\n\n/** Reads the gateway metrics layer's summary (sync, in-memory). */\nfunction readCachedTelemetrySignal(): { errorRate: number; totalRequests: number } | null {\n try {\n // Using require-style resolve so tests that mock the drives\n // module don't pull in the metrics graph.\n const mod = (globalThis as unknown as { __titan_metrics_summary?: () => { totalRequests?: number; errorRate?: number } | null }).__titan_metrics_summary;\n if (typeof mod !== 'function') return null;\n const s = mod();\n if (!s || typeof s.totalRequests !== 'number' || typeof s.errorRate !== 'number') return null;\n // Only treat the signal as meaningful once we have enough samples.\n if (s.totalRequests < 10) return null;\n return { errorRate: s.errorRate, totalRequests: s.totalRequests };\n } catch {\n return null;\n }\n}\n\n/** Reads count of unresolved error patterns from the learning layer. */\nfunction readUnresolvedErrorPatternCount(): number | null {\n try {\n const mod = (globalThis as unknown as { __titan_unresolved_error_patterns?: () => number }).__titan_unresolved_error_patterns;\n if (typeof mod !== 'function') return null;\n const n = mod();\n if (typeof n !== 'number' || !Number.isFinite(n)) return null;\n return n;\n } catch {\n return null;\n }\n}\n\n// ── Drive state computation ──────────────────────────────────────\n\n/** Compute all drive states for a given snapshot, applying per-drive\n * setpoint + weight overrides + disabled-drive filter (all from\n * config.organism.{driveSetpoints,driveWeights,disabledDrives}). */\nexport function computeAllDrives(\n snapshot: DriveSnapshot,\n setpointOverrides: Partial<Record<DriveId, number>> = {},\n weightOverrides: Partial<Record<DriveId, number>> = {},\n disabledDrives: DriveId[] = [],\n): DriveState[] {\n const out: DriveState[] = [];\n const disabled = new Set(disabledDrives);\n for (const def of DRIVES) {\n if (disabled.has(def.id)) continue;\n const { satisfaction, inputs } = def.compute(snapshot);\n const setpoint = setpointOverrides[def.id] ?? def.defaultSetpoint;\n const weight = weightOverrides[def.id] ?? def.weight;\n const pressure = satisfaction < setpoint\n ? (setpoint - satisfaction) * weight\n : 0;\n out.push({\n id: def.id,\n label: def.label,\n satisfaction: clamp01(satisfaction),\n setpoint,\n pressure,\n weight,\n inputs,\n description: def.describe(satisfaction, inputs),\n });\n }\n return out;\n}\n\n// ── Persistence ──────────────────────────────────────────────────\n\nexport interface PersistedDriveHistory {\n latest: DriveTickResult;\n /** Ring buffer of last ≤1440 ticks (~24h at 60s cadence). */\n history: Array<{ timestamp: string; satisfactions: Record<DriveId, number> }>;\n}\n\n/** Load the last-written drive state (if any). Returns null on first run. */\nexport function loadDriveHistory(): PersistedDriveHistory | null {\n if (!existsSync(DRIVE_STATE_PATH)) return null;\n try {\n return JSON.parse(readFileSync(DRIVE_STATE_PATH, 'utf-8')) as PersistedDriveHistory;\n } catch (err) {\n logger.warn(COMPONENT, `drive-state.json corrupt: ${(err as Error).message}`);\n return null;\n }\n}\n\n/** Persist the tick. Ring-buffers history to a max of 1440 entries. */\nexport function saveDriveTick(tick: DriveTickResult): void {\n try {\n ensureDir(TITAN_HOME);\n const existing = loadDriveHistory();\n const satisfactions: Record<string, number> = {};\n for (const d of tick.drives) satisfactions[d.id] = d.satisfaction;\n const history = (existing?.history || []).concat([{\n timestamp: tick.timestamp,\n satisfactions: satisfactions as Record<DriveId, number>,\n }]);\n const trimmed = history.length > 1440 ? history.slice(-1440) : history;\n const payload: PersistedDriveHistory = { latest: tick, history: trimmed };\n writeFileSync(DRIVE_STATE_PATH, JSON.stringify(payload, null, 2), 'utf-8');\n } catch (err) {\n logger.warn(COMPONENT, `Failed to save drive state: ${(err as Error).message}`);\n }\n}\n\n// ── One-call convenience ─────────────────────────────────────────\n\n/** Build snapshot → compute drives → package as a DriveTickResult. Does NOT\n * persist; callers decide whether to save (daemon tick does; read-only API\n * endpoints don't). */\nexport function runDriveTick(\n setpointOverrides: Partial<Record<DriveId, number>> = {},\n weightOverrides: Partial<Record<DriveId, number>> = {},\n disabledDrives: DriveId[] = [],\n): DriveTickResult {\n const snapshot = buildSnapshot();\n const drives = computeAllDrives(snapshot, setpointOverrides, weightOverrides, disabledDrives);\n const totalPressure = drives.reduce((sum, d) => sum + d.pressure, 0);\n const dominantDrives = drives\n .filter(d => d.pressure > 0)\n .sort((a, b) => b.pressure - a.pressure)\n .slice(0, 2)\n .map(d => d.id);\n return {\n timestamp: new Date().toISOString(),\n drives,\n totalPressure,\n dominantDrives,\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -168,6 +168,22 @@ async function runPressureCycle(drives) {
|
|
|
168
168
|
for (const d of reading.perDrive.slice(0, 3)) {
|
|
169
169
|
noteLines.push(`- ${d.id}: pressure ${d.pressure.toFixed(2)} \u2014 ${d.description}`);
|
|
170
170
|
}
|
|
171
|
+
if (decision.dominantDrives[0] === "social") {
|
|
172
|
+
const socialDrive = drives.find((d) => d.id === "social");
|
|
173
|
+
const hoursSince = socialDrive?.inputs?.hoursSinceLastPost ?? 0;
|
|
174
|
+
if (hoursSince >= 6) {
|
|
175
|
+
noteLines.push(
|
|
176
|
+
"",
|
|
177
|
+
"PROPOSAL HINT: Social drive deficit is driven by Facebook posting drought.",
|
|
178
|
+
`It has been ~${Math.round(hoursSince)}h since the last FB post.`,
|
|
179
|
+
"Propose a goal of type `facebook_post` with one of these contentTypes:",
|
|
180
|
+
' - "activity" \u2014 post real TITAN runtime activity from the last 24h',
|
|
181
|
+
' - "stats" \u2014 post download/install milestones if any crossed today',
|
|
182
|
+
' - "promo" \u2014 promo a recent feature shipment (only if a release tagged today)',
|
|
183
|
+
"Only propose if there is genuine activity to share \u2014 empty/generic posts are worse than none."
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
171
187
|
const consolidationNotes = noteLines.join("\n");
|
|
172
188
|
const somaAgentId = `soma:${decision.dominantDrives[0] ?? "fused"}`;
|
|
173
189
|
let approvalId;
|