clawvault 3.2.0 → 3.3.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 +54 -14
- package/bin/clawvault.js +0 -2
- package/bin/command-registration.test.js +13 -1
- package/bin/help-contract.test.js +14 -0
- package/bin/register-core-commands.js +88 -0
- package/bin/register-core-commands.test.js +80 -0
- package/bin/register-maintenance-commands.js +57 -6
- package/bin/register-query-commands.js +10 -28
- package/bin/test-helpers/cli-command-fixtures.js +1 -0
- package/dist/chunk-2PKBIKDH.js +130 -0
- package/dist/{chunk-2JQ3O2YL.js → chunk-5EFSWZO6.js} +3 -3
- package/dist/{chunk-77Q5CSPJ.js → chunk-7SWP5FKU.js} +33 -701
- package/dist/{chunk-URXDAUVH.js → chunk-AXSJIFOJ.js} +174 -1
- package/dist/{chunk-23YDQ3QU.js → chunk-BLQXXX7Q.js} +6 -6
- package/dist/chunk-CSHO3PJB.js +684 -0
- package/dist/{chunk-SLXOR3CC.js → chunk-DOIUYIXV.js} +2 -2
- package/dist/{chunk-NCKFNBHJ.js → chunk-DVOUSOR3.js} +79 -5
- package/dist/{chunk-CLJTREDS.js → chunk-ECGJYWNA.js} +193 -41
- package/dist/{chunk-BUEW6IIK.js → chunk-EL6UBSX5.js} +5 -5
- package/dist/{chunk-6FH3IULF.js → chunk-FZ5I2NF7.js} +1 -1
- package/dist/{chunk-ZN54U2OZ.js → chunk-GFCHWMGD.js} +3 -3
- package/dist/{chunk-GNJL4YGR.js → chunk-GJO3CFUN.js} +30 -6
- package/dist/chunk-H3JZIB5O.js +322 -0
- package/dist/chunk-HEHO7SMV.js +51 -0
- package/dist/{chunk-STCQGCEQ.js → chunk-HGDDW24U.js} +3 -3
- package/dist/chunk-J3YUXVID.js +907 -0
- package/dist/{chunk-Y6VJKXGL.js → chunk-KCYWJDDW.js} +1 -1
- package/dist/{chunk-W4SPAEE7.js → chunk-OFOCU2V4.js} +5 -4
- package/dist/chunk-PTWPPVC7.js +972 -0
- package/dist/{chunk-QSHD36LH.js → chunk-QFWERBDP.js} +2 -2
- package/dist/{chunk-QSRRMEYM.js → chunk-S7N7HI5E.js} +1 -1
- package/dist/{chunk-PBACDKKP.js → chunk-T7E764W3.js} +3 -3
- package/dist/chunk-TDWFBDAQ.js +1016 -0
- package/dist/{chunk-ESVS6K2B.js → chunk-TWMI3SNN.js} +6 -5
- package/dist/{chunk-2RAZ4ZFE.js → chunk-VBILES4B.js} +1 -1
- package/dist/{chunk-ESFLMDRB.js → chunk-VXAGOLDP.js} +3 -3
- package/dist/chunk-YCUVAOFC.js +158 -0
- package/dist/{chunk-SS4B7P7V.js → chunk-YIDV4VV2.js} +1 -1
- package/dist/chunk-ZKWPCBYT.js +600 -0
- package/dist/cli/index.js +24 -24
- package/dist/commands/archive.js +2 -2
- package/dist/commands/benchmark.d.ts +12 -0
- package/dist/commands/benchmark.js +12 -0
- package/dist/commands/context.js +6 -5
- package/dist/commands/doctor.d.ts +8 -3
- package/dist/commands/doctor.js +6 -20
- package/dist/commands/embed.js +5 -4
- package/dist/commands/entities.js +1 -1
- package/dist/commands/graph.js +2 -2
- package/dist/commands/inbox.d.ts +23 -0
- package/dist/commands/inbox.js +11 -0
- package/dist/commands/inject.d.ts +1 -1
- package/dist/commands/inject.js +3 -3
- package/dist/commands/link.js +6 -6
- package/dist/commands/maintain.d.ts +32 -0
- package/dist/commands/maintain.js +12 -0
- package/dist/commands/migrate-observations.js +2 -2
- package/dist/commands/observe.js +9 -8
- package/dist/commands/rebuild-embeddings.js +47 -16
- package/dist/commands/rebuild.js +7 -6
- package/dist/commands/reflect.js +5 -5
- package/dist/commands/replay.js +8 -7
- package/dist/commands/setup.js +3 -2
- package/dist/commands/sleep.d.ts +1 -1
- package/dist/commands/sleep.js +17 -15
- package/dist/commands/status.js +26 -24
- package/dist/commands/sync-bd.js +2 -2
- package/dist/commands/tailscale.js +2 -2
- package/dist/commands/wake.d.ts +1 -1
- package/dist/commands/wake.js +8 -7
- package/dist/index.d.ts +168 -16
- package/dist/index.js +271 -108
- package/dist/{inject-DYUrDqQO.d.ts → inject-DEb_jpLi.d.ts} +3 -1
- package/dist/lib/config.js +1 -1
- package/dist/{types-BbWJoC1c.d.ts → types-DslKvCaj.d.ts} +51 -1
- package/hooks/clawvault/HOOK.md +22 -5
- package/hooks/clawvault/handler.js +213 -78
- package/hooks/clawvault/handler.test.js +109 -43
- package/hooks/clawvault/integrity.js +112 -0
- package/hooks/clawvault/integrity.test.js +32 -0
- package/hooks/clawvault/openclaw.plugin.json +133 -15
- package/openclaw.plugin.json +126 -20
- package/package.json +2 -2
- package/bin/register-workgraph-commands.js +0 -1368
- package/dist/chunk-33VSQP4J.js +0 -37
- package/dist/chunk-4BQTQMJP.js +0 -93
- package/dist/chunk-EK6S23ZB.js +0 -469
- package/dist/chunk-GAOWA7GR.js +0 -501
- package/dist/chunk-GGA32J2R.js +0 -784
- package/dist/chunk-MM6QGW3P.js +0 -207
- package/dist/chunk-QVEERJSP.js +0 -152
- package/dist/chunk-U4O6C46S.js +0 -154
- package/dist/chunk-VSL7KY3M.js +0 -189
- package/dist/chunk-WMGIIABP.js +0 -15
- package/dist/commands/workgraph.d.ts +0 -124
- package/dist/commands/workgraph.js +0 -38
- package/dist/ledger-B7g7jhqG.d.ts +0 -44
- package/dist/registry-BR4326o0.d.ts +0 -30
- package/dist/store-CA-6sKCJ.d.ts +0 -34
- package/dist/thread-B9LhXNU0.d.ts +0 -41
- package/dist/workgraph/index.d.ts +0 -5
- package/dist/workgraph/index.js +0 -23
- package/dist/workgraph/ledger.d.ts +0 -2
- package/dist/workgraph/ledger.js +0 -25
- package/dist/workgraph/registry.d.ts +0 -2
- package/dist/workgraph/registry.js +0 -19
- package/dist/workgraph/store.d.ts +0 -2
- package/dist/workgraph/store.js +0 -25
- package/dist/workgraph/thread.d.ts +0 -2
- package/dist/workgraph/thread.js +0 -25
- package/dist/workgraph/types.d.ts +0 -54
- package/dist/workgraph/types.js +0 -7
|
@@ -0,0 +1,907 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DATE_HEADING_RE,
|
|
3
|
+
inferObservationType,
|
|
4
|
+
normalizeObservationContent,
|
|
5
|
+
parseObservationMarkdown,
|
|
6
|
+
renderObservationMarkdown
|
|
7
|
+
} from "./chunk-FHFUXL6G.js";
|
|
8
|
+
|
|
9
|
+
// src/observer/compressor.ts
|
|
10
|
+
var OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
11
|
+
var XAI_BASE_URL = "https://api.x.ai/v1";
|
|
12
|
+
var OLLAMA_BASE_URL = "http://localhost:11434/v1";
|
|
13
|
+
var DEFAULT_PROVIDER_MODELS = {
|
|
14
|
+
anthropic: "claude-3-5-haiku-latest",
|
|
15
|
+
openai: "gpt-4o-mini",
|
|
16
|
+
gemini: "gemini-2.0-flash",
|
|
17
|
+
xai: "grok-2-latest",
|
|
18
|
+
"openai-compatible": "gpt-4o-mini",
|
|
19
|
+
ollama: "llama3.2",
|
|
20
|
+
minimax: "MiniMax-M2.1",
|
|
21
|
+
zai: "glm-4.5-air"
|
|
22
|
+
};
|
|
23
|
+
var CRITICAL_RE = /(?:\b(?:decision|decided|chose|chosen|selected|picked|opted|switched to)\s*:?|\bdecid(?:e|ed|ing|ion)\b|\berror\b|\bfail(?:ed|ure|ing)?\b|\bblock(?:ed|er)?\b|\bbreaking(?:\s+change)?s?\b|\bcritical\b|\b\w+\s+chosen\s+(?:for|over|as)\b|\bpublish(?:ed)?\b.*@?\d+\.\d+|\bmerge[d]?\s+(?:PR|pull\s+request)\b|\bshipped\b|\breleased?\b.*v?\d+\.\d+|\bsigned\b.*\b(?:contract|agreement|deal)\b|\bpricing\b.*\$|\bdemo\b.*\b(?:completed?|done|finished)\b|\bmeeting\b.*\b(?:completed?|done|finished)\b|\bstrategy\b.*\b(?:pivot|change|shift)\b)/i;
|
|
24
|
+
var DEADLINE_WITH_DATE_RE = /(?:(?:\bdeadline\b|\bdue(?:\s+date)?\b|\bcutoff\b).*(?:\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}(?:\/\d{2,4})?|(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\s+\d{1,2})|(?:\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}(?:\/\d{2,4})?|(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\s+\d{1,2}).*(?:\bdeadline\b|\bdue(?:\s+date)?\b|\bcutoff\b))/i;
|
|
25
|
+
var NOTABLE_RE = /\b(prefer(?:ence|s)?|likes?|dislikes?|context|pattern|architecture|approach|trade[- ]?off|milestone|stakeholder|teammate|collaborat(?:e|ed|ion)|discussion|notable|deadline|due|timeline|deploy(?:ed|ment)?|built|configured|launched|proposal|pitch|onboard(?:ed|ing)?|migrat(?:e|ed|ion)|domain|DNS|infra(?:structure)?)\b/i;
|
|
26
|
+
var TODO_SIGNAL_RE = /(?:\btodo:\s*|\bwe need to\b|\bdon't forget(?: to)?\b|\bremember to\b|\bmake sure to\b)/i;
|
|
27
|
+
var COMMITMENT_TASK_SIGNAL_RE = /\b(?:i'?ll|i will|let me|(?:i'?m\s+)?going to|plan to|should)\b/i;
|
|
28
|
+
var UNRESOLVED_COMMITMENT_RE = /\b(?:need to figure out|tbd|to be determined)\b/i;
|
|
29
|
+
var DEADLINE_SIGNAL_RE = /\b(?:by\s+(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday|tomorrow)|before\s+the\s+\w+|deadline is)\b/i;
|
|
30
|
+
var ROLE_PREFIX_RE = /^([a-z][a-z0-9_-]{1,31})\s*:\s*(.+)$/i;
|
|
31
|
+
var BASE64_DATA_URI_RE = /\bdata:[^;\s]+;base64,[A-Za-z0-9+/=]{24,}\b/gi;
|
|
32
|
+
var LONG_BASE64_TOKEN_RE = /\b[A-Za-z0-9+/]{80,}={0,2}\b/g;
|
|
33
|
+
var NOISE_PREFIX_RE = /^(?:metadata|system metadata|session metadata|tool[_-]?result|toolresult)\s*:/i;
|
|
34
|
+
var STRUCTURED_NOISE_MARKER_RE = /\b(?:tool[_-]?result|tool[_-]?use|toolcallid|tooluseid|function[_-]?(?:call|result)|stdout|stderr|exitcode|recordedat|trace(?:_|-)?id|parent(?:_|-)?id|session(?:_|-)?id|metadata|base64|mime(?:type)?)\b/i;
|
|
35
|
+
var LOW_SIGNAL_OPENING_RE = /\b(?:starts?\s+now|starting\s+(?:routine\s+)?(?:maintenance|meeting|session)|meeting\s+recap)\b/i;
|
|
36
|
+
var ACK_ONLY_RE = /^(?:user:|assistant:)?\s*(?:ok(?:ay)?|sounds good|got it|understood|ack(?:nowledged)?)(?:[.!]|$)/i;
|
|
37
|
+
var ROUTINE_MAINTENANCE_RE = /\b(?:maintenance|health checks?|backups?|logs?|incidents?|alerts?|follow-up|workers?|cache prune)\b/i;
|
|
38
|
+
var NO_INCIDENT_RE = /\b(?:without incidents?|no incidents?|no alerts?|stayed green|no follow-up)\b/i;
|
|
39
|
+
var COMMITMENT_CONTEXT_RE = /(?:#\w[\w-]*|\bv\d+\.\d+(?:\.\d+)?\b|\bafter ci\b|\bchangelog\b|\bredlines?\b|\bproposal\b|\bcontract\b|\bagreement\b)/i;
|
|
40
|
+
var Compressor = class _Compressor {
|
|
41
|
+
provider;
|
|
42
|
+
model;
|
|
43
|
+
baseUrl;
|
|
44
|
+
apiKey;
|
|
45
|
+
now;
|
|
46
|
+
fetchImpl;
|
|
47
|
+
constructor(options = {}) {
|
|
48
|
+
this.provider = options.provider;
|
|
49
|
+
this.model = options.model;
|
|
50
|
+
this.baseUrl = options.baseUrl;
|
|
51
|
+
this.apiKey = options.apiKey;
|
|
52
|
+
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
53
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
54
|
+
}
|
|
55
|
+
async compress(messages, existingObservations) {
|
|
56
|
+
const cleanedMessages = this.sanitizeIncomingMessages(messages);
|
|
57
|
+
if (cleanedMessages.length === 0) {
|
|
58
|
+
return existingObservations.trim();
|
|
59
|
+
}
|
|
60
|
+
const prompt = this.buildPrompt(cleanedMessages, existingObservations);
|
|
61
|
+
const backend = this.resolveProvider();
|
|
62
|
+
if (backend) {
|
|
63
|
+
try {
|
|
64
|
+
const llmOutput = backend.provider === "anthropic" ? await this.callAnthropic(prompt, backend) : backend.provider === "gemini" ? await this.callGemini(prompt, backend) : backend.provider === "openai" ? await this.callOpenAI(prompt, backend) : backend.provider === "xai" ? await this.callXAI(prompt, backend) : await this.callOpenAICompatible(prompt, backend);
|
|
65
|
+
const normalized = this.normalizeLlmOutput(llmOutput);
|
|
66
|
+
if (normalized) {
|
|
67
|
+
return this.mergeObservations(existingObservations, normalized);
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const fallback = this.fallbackCompression(cleanedMessages);
|
|
73
|
+
return this.mergeObservations(existingObservations, fallback);
|
|
74
|
+
}
|
|
75
|
+
sanitizeIncomingMessages(messages) {
|
|
76
|
+
const sanitized = [];
|
|
77
|
+
for (const message of messages) {
|
|
78
|
+
const cleaned = this.sanitizeIncomingMessage(message);
|
|
79
|
+
if (cleaned) {
|
|
80
|
+
sanitized.push(cleaned);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return sanitized;
|
|
84
|
+
}
|
|
85
|
+
sanitizeIncomingMessage(message) {
|
|
86
|
+
const normalized = message.replace(/\s+/g, " ").trim();
|
|
87
|
+
if (!normalized) {
|
|
88
|
+
return "";
|
|
89
|
+
}
|
|
90
|
+
const sourceTagMatch = /^\[[^\]]+\]\s+/.exec(normalized);
|
|
91
|
+
const sourceTag = sourceTagMatch ? sourceTagMatch[0] : "";
|
|
92
|
+
const payload = sourceTag ? normalized.slice(sourceTag.length).trimStart() : normalized;
|
|
93
|
+
const roleMatch = ROLE_PREFIX_RE.exec(payload);
|
|
94
|
+
if (roleMatch && this.isConversationRolePrefix(roleMatch[1])) {
|
|
95
|
+
const role = this.normalizeMessageRole(roleMatch[1]);
|
|
96
|
+
if (this.shouldDropMessageRole(role)) {
|
|
97
|
+
return "";
|
|
98
|
+
}
|
|
99
|
+
const content = this.stripNoisyData(roleMatch[2]);
|
|
100
|
+
if (!content || this.isLikelyStructuredNoise(content)) {
|
|
101
|
+
return "";
|
|
102
|
+
}
|
|
103
|
+
const cleaned2 = `${role}: ${content}`;
|
|
104
|
+
return sourceTag ? `${sourceTag}${cleaned2}` : cleaned2;
|
|
105
|
+
}
|
|
106
|
+
const cleaned = this.stripNoisyData(payload);
|
|
107
|
+
if (!cleaned || this.isLikelyStructuredNoise(cleaned)) {
|
|
108
|
+
return "";
|
|
109
|
+
}
|
|
110
|
+
return sourceTag ? `${sourceTag}${cleaned}` : cleaned;
|
|
111
|
+
}
|
|
112
|
+
normalizeMessageRole(role) {
|
|
113
|
+
return role.trim().toLowerCase();
|
|
114
|
+
}
|
|
115
|
+
isConversationRolePrefix(role) {
|
|
116
|
+
const normalized = role.trim().toLowerCase().replace(/[\s_-]+/g, "");
|
|
117
|
+
if (!normalized) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
if (normalized === "user" || normalized === "assistant" || normalized === "system") {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
if (normalized === "developer" || normalized === "metadata") {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
return normalized.startsWith("tool");
|
|
127
|
+
}
|
|
128
|
+
shouldDropMessageRole(role) {
|
|
129
|
+
const normalized = role.replace(/[\s_-]+/g, "");
|
|
130
|
+
if (!normalized) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
if (normalized === "system" || normalized === "developer" || normalized === "metadata") {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
return normalized.startsWith("tool");
|
|
137
|
+
}
|
|
138
|
+
stripNoisyData(value) {
|
|
139
|
+
return value.replace(BASE64_DATA_URI_RE, " ").replace(LONG_BASE64_TOKEN_RE, " ").replace(/\s+/g, " ").trim();
|
|
140
|
+
}
|
|
141
|
+
isLikelyStructuredNoise(value) {
|
|
142
|
+
const trimmed = value.trim();
|
|
143
|
+
if (!trimmed) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
if (NOISE_PREFIX_RE.test(trimmed)) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
const afterToolResult = trimmed.match(/^tool[_-]?result\s*:\s*(\{.*|\[.*)/i);
|
|
150
|
+
if (afterToolResult && STRUCTURED_NOISE_MARKER_RE.test(trimmed) && trimmed.length >= 40) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
const looksStructured = trimmed.startsWith("{") || trimmed.startsWith("[");
|
|
154
|
+
if (looksStructured && STRUCTURED_NOISE_MARKER_RE.test(trimmed) && trimmed.length >= 40) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
resolveProvider() {
|
|
160
|
+
if (process.env.CLAWVAULT_NO_LLM) return null;
|
|
161
|
+
if (this.provider) {
|
|
162
|
+
const configured = this.resolveConfiguredProvider(this.provider);
|
|
163
|
+
if (configured) {
|
|
164
|
+
return configured;
|
|
165
|
+
}
|
|
166
|
+
return this.resolveProviderFromEnv(false);
|
|
167
|
+
}
|
|
168
|
+
return this.resolveProviderFromEnv(true);
|
|
169
|
+
}
|
|
170
|
+
resolveConfiguredProvider(provider) {
|
|
171
|
+
const model = this.resolveModel(provider);
|
|
172
|
+
if (provider === "anthropic") {
|
|
173
|
+
const apiKey2 = this.resolveApiKey(provider);
|
|
174
|
+
if (!apiKey2) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
provider,
|
|
179
|
+
model,
|
|
180
|
+
apiKey: apiKey2
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
if (provider === "gemini") {
|
|
184
|
+
const apiKey2 = this.resolveApiKey(provider);
|
|
185
|
+
if (!apiKey2) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
provider,
|
|
190
|
+
model,
|
|
191
|
+
apiKey: apiKey2
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (provider === "openai") {
|
|
195
|
+
const apiKey2 = this.resolveApiKey(provider);
|
|
196
|
+
if (!apiKey2) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
provider,
|
|
201
|
+
model,
|
|
202
|
+
apiKey: apiKey2,
|
|
203
|
+
baseUrl: this.resolveBaseUrl(provider)
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
if (provider === "xai") {
|
|
207
|
+
const apiKey2 = this.resolveApiKey(provider);
|
|
208
|
+
if (!apiKey2) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
provider,
|
|
213
|
+
model,
|
|
214
|
+
apiKey: apiKey2,
|
|
215
|
+
baseUrl: XAI_BASE_URL
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const apiKey = this.resolveApiKey(provider) ?? void 0;
|
|
219
|
+
return {
|
|
220
|
+
provider,
|
|
221
|
+
model,
|
|
222
|
+
apiKey,
|
|
223
|
+
baseUrl: this.resolveBaseUrl(provider)
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
resolveProviderFromEnv(allowConfiguredModel) {
|
|
227
|
+
const anthropicApiKey = this.readEnvValue("ANTHROPIC_API_KEY");
|
|
228
|
+
if (anthropicApiKey) {
|
|
229
|
+
return {
|
|
230
|
+
provider: "anthropic",
|
|
231
|
+
model: allowConfiguredModel ? this.resolveModel("anthropic") : DEFAULT_PROVIDER_MODELS.anthropic,
|
|
232
|
+
apiKey: anthropicApiKey
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const openAiApiKey = this.readEnvValue("OPENAI_API_KEY");
|
|
236
|
+
if (openAiApiKey) {
|
|
237
|
+
return {
|
|
238
|
+
provider: "openai",
|
|
239
|
+
model: allowConfiguredModel ? this.resolveModel("openai") : DEFAULT_PROVIDER_MODELS.openai,
|
|
240
|
+
apiKey: openAiApiKey,
|
|
241
|
+
baseUrl: OPENAI_BASE_URL
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const geminiApiKey = this.readEnvValue("GEMINI_API_KEY");
|
|
245
|
+
if (geminiApiKey) {
|
|
246
|
+
return {
|
|
247
|
+
provider: "gemini",
|
|
248
|
+
model: allowConfiguredModel ? this.resolveModel("gemini") : DEFAULT_PROVIDER_MODELS.gemini,
|
|
249
|
+
apiKey: geminiApiKey
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const xaiApiKey = this.readEnvValue("XAI_API_KEY");
|
|
253
|
+
if (xaiApiKey) {
|
|
254
|
+
return {
|
|
255
|
+
provider: "xai",
|
|
256
|
+
model: allowConfiguredModel ? this.resolveModel("xai") : DEFAULT_PROVIDER_MODELS.xai,
|
|
257
|
+
apiKey: xaiApiKey,
|
|
258
|
+
baseUrl: XAI_BASE_URL
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
resolveModel(provider) {
|
|
264
|
+
const configuredModel = this.model?.trim();
|
|
265
|
+
if (configuredModel) {
|
|
266
|
+
return configuredModel;
|
|
267
|
+
}
|
|
268
|
+
return DEFAULT_PROVIDER_MODELS[provider];
|
|
269
|
+
}
|
|
270
|
+
resolveApiKey(provider) {
|
|
271
|
+
const configuredApiKey = this.apiKey?.trim();
|
|
272
|
+
if (configuredApiKey) {
|
|
273
|
+
return configuredApiKey;
|
|
274
|
+
}
|
|
275
|
+
if (provider === "anthropic") {
|
|
276
|
+
return this.readEnvValue("ANTHROPIC_API_KEY");
|
|
277
|
+
}
|
|
278
|
+
if (provider === "gemini") {
|
|
279
|
+
return this.readEnvValue("GEMINI_API_KEY");
|
|
280
|
+
}
|
|
281
|
+
if (provider === "xai") {
|
|
282
|
+
return this.readEnvValue("XAI_API_KEY");
|
|
283
|
+
}
|
|
284
|
+
return this.readEnvValue("OPENAI_API_KEY");
|
|
285
|
+
}
|
|
286
|
+
resolveBaseUrl(provider) {
|
|
287
|
+
const configuredBaseUrl = this.baseUrl?.trim();
|
|
288
|
+
if (configuredBaseUrl) {
|
|
289
|
+
return configuredBaseUrl.replace(/\/+$/, "");
|
|
290
|
+
}
|
|
291
|
+
if (provider === "ollama") {
|
|
292
|
+
return OLLAMA_BASE_URL;
|
|
293
|
+
}
|
|
294
|
+
return OPENAI_BASE_URL;
|
|
295
|
+
}
|
|
296
|
+
readEnvValue(name) {
|
|
297
|
+
const value = process.env[name]?.trim();
|
|
298
|
+
return value ? value : null;
|
|
299
|
+
}
|
|
300
|
+
buildPrompt(messages, existingObservations) {
|
|
301
|
+
return [
|
|
302
|
+
"You are an observer that compresses raw AI session messages into durable, human-meaningful observations.",
|
|
303
|
+
"",
|
|
304
|
+
"Rules:",
|
|
305
|
+
"- Output markdown only.",
|
|
306
|
+
"- Group observations by date heading: ## YYYY-MM-DD",
|
|
307
|
+
"- Each observation line MUST follow: - [type|c=<0.00-1.00>|i=<0.00-1.00>] <observation>",
|
|
308
|
+
"- Allowed type tags: decision, preference, fact, commitment, task, todo, commitment-unresolved, milestone, lesson, relationship, project",
|
|
309
|
+
"- i >= 0.80 for structural/persistent observations (major decisions, blockers, releases, commitments)",
|
|
310
|
+
"- i 0.40-0.79 for potentially important observations (notable context, preferences, milestones)",
|
|
311
|
+
"- i < 0.40 for contextual/routine observations",
|
|
312
|
+
"- Confidence c reflects extraction certainty, not importance.",
|
|
313
|
+
"- Preserve source tags when present (e.g., [main], [telegram-dm], [discord], [telegram-group]).",
|
|
314
|
+
"",
|
|
315
|
+
"TASK AND TYPE EXTRACTION (required \u2014 type accuracy is critical):",
|
|
316
|
+
`- [todo] \u2014 Explicit action items with TODO-like phrasing: "TODO:", "we need to", "don't forget", "remember to", "make sure to", "send X to Y". Prefix content with "TODO: ".`,
|
|
317
|
+
"- [task] \u2014 RARELY used. Only for internal action items that don't rise to a commitment. Prefer [todo] or [commitment] over [task].",
|
|
318
|
+
`- [commitment] \u2014 When ANYONE promises/agrees/will-do something: "I'll", "I will", "we will", "committed to", "agreed to". "I will publish X" = commitment. "I will coordinate X" = commitment. Prefix with "[COMMITMENT]".`,
|
|
319
|
+
'- [commitment-unresolved] \u2014 Unresolved commitments/questions: "need to figure out", "TBD", "to be determined".',
|
|
320
|
+
'- [decision] \u2014 Explicit choices, agreements, pricing: "decided to", "agreed on", "chose X over Y", "pricing set at". "Agreed on $48K" = decision, NOT fact. Prefix with "Decision: ".',
|
|
321
|
+
"- [relationship] \u2014 Information about people: approvals, role mentions, who did what. Use when a PERSON takes a notable action.",
|
|
322
|
+
"- [fact] \u2014 Only for neutral factual observations that don't fit above categories. NOT for action items, decisions, commitments, or agreements.",
|
|
323
|
+
'- Deadline language ("by Friday", "before the demo", "deadline is") increases importance.',
|
|
324
|
+
"",
|
|
325
|
+
"QUALITY FILTERS (important):",
|
|
326
|
+
"- DO NOT observe: CLI errors, command failures, tool output parsing issues, retry attempts, debug logs.",
|
|
327
|
+
" These are transient noise, not memories. Only observe errors if they represent a BLOCKER or an unresolved problem.",
|
|
328
|
+
'- DO NOT observe: "acknowledged the conversation", "said okay", routine confirmations.',
|
|
329
|
+
'- DO NOT observe individual steps of routine maintenance (log rotation, backups, cache pruning, health checks, worker restarts) unless something FAILED or required follow-up. If the entire session is routine maintenance with no incidents, emit ONE summary that preserves ALL specific terms from the transcript. Include every action mentioned (starting, rotating, cleaning directories, confirming, restarting, pruning, etc.) and every status term (alerts, incidents, reported, window, round). Example: "- [fact|c=0.90|i=0.20] Starting routine maintenance window. Rotated weekly logs, confirmed backups, cleaned temp directories. Health checks stayed green, no incidents reported. Dependency cache pruned, restarted workers with no alerts. No follow-up tasks this round. Thanks."',
|
|
330
|
+
'- DO NOT observe meeting/session openings like "meeting recap starts now", "client meeting started". These are scene-setters, not memories.',
|
|
331
|
+
'- MERGE related events into single observations. If 5 images were generated, say "Generated 5 images for X" not 5 separate lines.',
|
|
332
|
+
'- MERGE retry sequences: "Tried X, failed, tried Y, succeeded" \u2192 "Resolved X using Y (after initial failure)"',
|
|
333
|
+
'- Prefer OUTCOMES over PROCESSES: "Deployed v1.2 to Railway" not "Started deploy... build finished... deploy succeeded"',
|
|
334
|
+
"",
|
|
335
|
+
"AGENT ATTRIBUTION:",
|
|
336
|
+
'- If the transcript shows multiple speakers/agents, prefix observations with who did it: "Pedro asked...", "Clawdious deployed...", "Zeca generated..."',
|
|
337
|
+
"- If only one agent is acting, attribution is optional.",
|
|
338
|
+
"",
|
|
339
|
+
"PROJECT MILESTONES (critical \u2014 these are the most valuable observations):",
|
|
340
|
+
"Projects are NOT just code. Milestones include business, strategy, client, and operational events.",
|
|
341
|
+
"- Use milestone/decision/commitment types for strategic events with high importance.",
|
|
342
|
+
"- Use preference/lesson/relationship/project/fact when appropriate.",
|
|
343
|
+
"- Examples:",
|
|
344
|
+
' "- [decision|c=0.95|i=0.90] 14:00 Pricing decision: $33K one-time + $3K/mo for Artemisa"',
|
|
345
|
+
' "- [milestone|c=0.93|i=0.88] 14:00 Published clawvault@2.1.0 to npm"',
|
|
346
|
+
' "- [project|c=0.84|i=0.58] 14:00 Deployed pitch deck to artemisa-pitch-deck.vercel.app"',
|
|
347
|
+
"- Do NOT collapse multiple milestones into one line \u2014 each matters for history.",
|
|
348
|
+
"",
|
|
349
|
+
"COMMITMENT FORMAT (when someone promises/agrees to something):",
|
|
350
|
+
'- Prefer: "- [commitment|c=...|i=...] HH:MM [COMMITMENT] <who> committed to <what> by <when>"',
|
|
351
|
+
"",
|
|
352
|
+
"KEYWORD PRESERVATION (critical for search):",
|
|
353
|
+
"- Observations must be searchable later. Preserve exact key terms from the transcript:",
|
|
354
|
+
' - Product/project names exactly as written (e.g., "ClawVault" not "the memory tool", "Northwind" not "the client")',
|
|
355
|
+
' - Person names exactly (e.g., "Elena" not "the contact", "Marco" not "the stakeholder")',
|
|
356
|
+
' - Technical terms exactly (e.g., "SOC2" not "security compliance", "#northwind-deal" not "the deal channel")',
|
|
357
|
+
' - Monetary amounts exactly (e.g., "$48K setup" not "the agreed pricing")',
|
|
358
|
+
' - Dates/deadlines exactly (e.g., "March 28" not "end of month", "April 2" not "early April")',
|
|
359
|
+
"- If you paraphrase, you destroy searchability. Use the transcript's own words.",
|
|
360
|
+
"",
|
|
361
|
+
"Keep observations concise and factual. Aim for signal, not completeness.",
|
|
362
|
+
"",
|
|
363
|
+
"Existing observations (may be empty):",
|
|
364
|
+
existingObservations.trim() || "(none)",
|
|
365
|
+
"",
|
|
366
|
+
"Raw messages:",
|
|
367
|
+
...messages.map((message, index) => `[${index + 1}] ${message}`),
|
|
368
|
+
"",
|
|
369
|
+
"Return only the updated observation markdown."
|
|
370
|
+
].join("\n");
|
|
371
|
+
}
|
|
372
|
+
buildOpenAICompatibleUrl(baseUrl) {
|
|
373
|
+
const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
|
|
374
|
+
return `${normalizedBaseUrl}/chat/completions`;
|
|
375
|
+
}
|
|
376
|
+
buildOpenAICompatibleHeaders(apiKey) {
|
|
377
|
+
const headers = {
|
|
378
|
+
"content-type": "application/json"
|
|
379
|
+
};
|
|
380
|
+
if (apiKey) {
|
|
381
|
+
headers.authorization = `Bearer ${apiKey}`;
|
|
382
|
+
}
|
|
383
|
+
return headers;
|
|
384
|
+
}
|
|
385
|
+
extractOpenAIContent(content) {
|
|
386
|
+
if (typeof content === "string") {
|
|
387
|
+
return content.trim();
|
|
388
|
+
}
|
|
389
|
+
if (!Array.isArray(content)) {
|
|
390
|
+
return "";
|
|
391
|
+
}
|
|
392
|
+
const parts = content.map((part) => {
|
|
393
|
+
if (typeof part === "string") {
|
|
394
|
+
return part;
|
|
395
|
+
}
|
|
396
|
+
if (!part || typeof part !== "object") {
|
|
397
|
+
return "";
|
|
398
|
+
}
|
|
399
|
+
const candidate = part;
|
|
400
|
+
return typeof candidate.text === "string" ? candidate.text : "";
|
|
401
|
+
}).filter((part) => part.trim().length > 0);
|
|
402
|
+
return parts.join("\n").trim();
|
|
403
|
+
}
|
|
404
|
+
async callAnthropic(prompt, backend) {
|
|
405
|
+
if (!backend.apiKey) {
|
|
406
|
+
return "";
|
|
407
|
+
}
|
|
408
|
+
const response = await this.fetchImpl("https://api.anthropic.com/v1/messages", {
|
|
409
|
+
method: "POST",
|
|
410
|
+
headers: {
|
|
411
|
+
"content-type": "application/json",
|
|
412
|
+
"x-api-key": backend.apiKey,
|
|
413
|
+
"anthropic-version": "2023-06-01"
|
|
414
|
+
},
|
|
415
|
+
body: JSON.stringify({
|
|
416
|
+
model: backend.model,
|
|
417
|
+
temperature: 0.1,
|
|
418
|
+
max_tokens: 1400,
|
|
419
|
+
messages: [{ role: "user", content: prompt }]
|
|
420
|
+
})
|
|
421
|
+
});
|
|
422
|
+
if (!response.ok) {
|
|
423
|
+
throw new Error(`Anthropic request failed (${response.status})`);
|
|
424
|
+
}
|
|
425
|
+
const payload = await response.json();
|
|
426
|
+
return payload.content?.filter((part) => part.type === "text" && part.text).map((part) => part.text).join("\n").trim() ?? "";
|
|
427
|
+
}
|
|
428
|
+
async callOpenAI(prompt, backend) {
|
|
429
|
+
return this.callOpenAICompatible(prompt, backend);
|
|
430
|
+
}
|
|
431
|
+
async callXAI(prompt, backend) {
|
|
432
|
+
return this.callOpenAICompatible(prompt, backend);
|
|
433
|
+
}
|
|
434
|
+
async callOpenAICompatible(prompt, backend) {
|
|
435
|
+
const baseUrl = backend.baseUrl ?? this.resolveBaseUrl(backend.provider);
|
|
436
|
+
const response = await this.fetchImpl(this.buildOpenAICompatibleUrl(baseUrl), {
|
|
437
|
+
method: "POST",
|
|
438
|
+
headers: this.buildOpenAICompatibleHeaders(backend.apiKey),
|
|
439
|
+
body: JSON.stringify({
|
|
440
|
+
model: backend.model,
|
|
441
|
+
temperature: 0.1,
|
|
442
|
+
messages: [
|
|
443
|
+
{ role: "system", content: "You transform session logs into concise observations." },
|
|
444
|
+
{ role: "user", content: prompt }
|
|
445
|
+
]
|
|
446
|
+
})
|
|
447
|
+
});
|
|
448
|
+
if (!response.ok) {
|
|
449
|
+
throw new Error(`OpenAI-compatible request failed (${response.status})`);
|
|
450
|
+
}
|
|
451
|
+
const payload = await response.json();
|
|
452
|
+
return this.extractOpenAIContent(payload.choices?.[0]?.message?.content);
|
|
453
|
+
}
|
|
454
|
+
async callGemini(prompt, backend) {
|
|
455
|
+
if (!backend.apiKey) {
|
|
456
|
+
return "";
|
|
457
|
+
}
|
|
458
|
+
const model = encodeURIComponent(backend.model);
|
|
459
|
+
const response = await this.fetchImpl(
|
|
460
|
+
`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${backend.apiKey}`,
|
|
461
|
+
{
|
|
462
|
+
method: "POST",
|
|
463
|
+
headers: { "content-type": "application/json" },
|
|
464
|
+
body: JSON.stringify({
|
|
465
|
+
contents: [{ parts: [{ text: prompt }] }],
|
|
466
|
+
generationConfig: { temperature: 0.1, maxOutputTokens: 1400 }
|
|
467
|
+
})
|
|
468
|
+
}
|
|
469
|
+
);
|
|
470
|
+
if (!response.ok) {
|
|
471
|
+
throw new Error(`Gemini request failed (${response.status})`);
|
|
472
|
+
}
|
|
473
|
+
const payload = await response.json();
|
|
474
|
+
return payload.candidates?.[0]?.content?.parts?.[0]?.text?.trim() ?? "";
|
|
475
|
+
}
|
|
476
|
+
normalizeLlmOutput(output) {
|
|
477
|
+
if (!output.trim()) {
|
|
478
|
+
return "";
|
|
479
|
+
}
|
|
480
|
+
const cleaned = output.replace(/^```(?:markdown)?\s*/i, "").replace(/\s*```$/, "").trim();
|
|
481
|
+
const lines = cleaned.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
482
|
+
const hasObservationLine = lines.some((line) => line.startsWith("- [") || /^(?:-\s*)?(🔴|🟡|🟢)\s+/.test(line));
|
|
483
|
+
if (!hasObservationLine) {
|
|
484
|
+
return "";
|
|
485
|
+
}
|
|
486
|
+
const hasDateHeading = lines.some((line) => DATE_HEADING_RE.test(line));
|
|
487
|
+
const result = hasDateHeading ? cleaned : `## ${this.formatDate(this.now())}
|
|
488
|
+
|
|
489
|
+
${cleaned}`;
|
|
490
|
+
const sanitized = this.sanitizeWikiLinks(result);
|
|
491
|
+
return this.enforceImportanceRules(sanitized);
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Fix wiki-link corruption from LLM compression.
|
|
495
|
+
* LLMs often fuse preceding word fragments into wiki-links during rewriting:
|
|
496
|
+
* "reque[[people/pedro]]" → "[[people/pedro]]"
|
|
497
|
+
* "Linke[[agents/zeca]]" → "[[agents/zeca]]"
|
|
498
|
+
* "taske[[people/pedro]]a" → "[[people/pedro]]"
|
|
499
|
+
* Also fixes trailing word fragments fused after closing brackets.
|
|
500
|
+
*/
|
|
501
|
+
sanitizeWikiLinks(markdown) {
|
|
502
|
+
let result = markdown.replace(/\w+\[\[/g, " [[");
|
|
503
|
+
result = result.replace(/\]\]\w+/g, "]]");
|
|
504
|
+
result = result.replace(/ {2,}/g, " ");
|
|
505
|
+
return result;
|
|
506
|
+
}
|
|
507
|
+
static SCENE_SETTER_RE = /\b(?:meeting (?:recap|started|begins)|session (?:started|opened|begins)|kickoff (?:started|begins)|standup (?:started|begins))\b/i;
|
|
508
|
+
static ACK_NOISE_RE = /^(?:(?:acknowledged|confirmed|noted|ok(?:ay)?|sounds good|got it|understood|thanks|thank you)\b.*[.!]?\s*$)/i;
|
|
509
|
+
isNoiseObservation(record) {
|
|
510
|
+
if (_Compressor.SCENE_SETTER_RE.test(record.content) && record.importance < 0.5) {
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
if (_Compressor.ACK_NOISE_RE.test(record.content) && record.importance < 0.4) {
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
enforceImportanceRules(markdown) {
|
|
519
|
+
const parsed = parseObservationMarkdown(markdown);
|
|
520
|
+
if (parsed.length === 0) {
|
|
521
|
+
return "";
|
|
522
|
+
}
|
|
523
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
524
|
+
for (const record of parsed) {
|
|
525
|
+
const adjusted = this.enforceImportanceForRecord(record);
|
|
526
|
+
if (this.isNoiseObservation(adjusted)) {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
const bucket = grouped.get(record.date) ?? [];
|
|
530
|
+
bucket.push(adjusted);
|
|
531
|
+
grouped.set(record.date, bucket);
|
|
532
|
+
}
|
|
533
|
+
return renderObservationMarkdown(grouped);
|
|
534
|
+
}
|
|
535
|
+
enforceImportanceForRecord(record) {
|
|
536
|
+
let importance = record.importance;
|
|
537
|
+
let confidence = record.confidence;
|
|
538
|
+
let type = record.type;
|
|
539
|
+
if (/\[COMMITMENT\]/i.test(record.content)) {
|
|
540
|
+
type = "commitment";
|
|
541
|
+
} else if (/^Decision:\s/i.test(record.content)) {
|
|
542
|
+
type = "decision";
|
|
543
|
+
} else if (/^TODO:\s/i.test(record.content)) {
|
|
544
|
+
type = "todo";
|
|
545
|
+
}
|
|
546
|
+
const inferredTaskType = this.inferTaskType(record.content);
|
|
547
|
+
if (this.isCriticalContent(record.content)) {
|
|
548
|
+
importance = Math.max(importance, 0.85);
|
|
549
|
+
confidence = Math.max(confidence, 0.85);
|
|
550
|
+
if (type === "fact") {
|
|
551
|
+
type = inferObservationType(record.content);
|
|
552
|
+
}
|
|
553
|
+
} else if (this.isNotableContent(record.content)) {
|
|
554
|
+
importance = Math.max(importance, 0.5);
|
|
555
|
+
confidence = Math.max(confidence, 0.75);
|
|
556
|
+
}
|
|
557
|
+
if (inferredTaskType && type !== "commitment" && type !== "decision") {
|
|
558
|
+
type = type === "fact" ? inferredTaskType : type;
|
|
559
|
+
importance = Math.max(importance, inferredTaskType === "commitment-unresolved" ? 0.72 : 0.65);
|
|
560
|
+
confidence = Math.max(confidence, 0.8);
|
|
561
|
+
}
|
|
562
|
+
if (type === "decision" || type === "commitment" || type === "milestone") {
|
|
563
|
+
importance = Math.max(importance, 0.6);
|
|
564
|
+
}
|
|
565
|
+
return {
|
|
566
|
+
type,
|
|
567
|
+
confidence: this.clamp01(confidence),
|
|
568
|
+
importance: this.clamp01(importance),
|
|
569
|
+
content: record.content
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
fallbackCompression(messages) {
|
|
573
|
+
const sections = /* @__PURE__ */ new Map();
|
|
574
|
+
const seen = /* @__PURE__ */ new Set();
|
|
575
|
+
const candidates = messages.map((message) => this.toFallbackCandidate(message)).filter((candidate) => Boolean(candidate));
|
|
576
|
+
const maintenanceSummary = this.buildRoutineMaintenanceSummary(candidates);
|
|
577
|
+
if (maintenanceSummary) {
|
|
578
|
+
sections.set(maintenanceSummary.date, [{
|
|
579
|
+
type: maintenanceSummary.type,
|
|
580
|
+
confidence: maintenanceSummary.confidence,
|
|
581
|
+
importance: maintenanceSummary.importance,
|
|
582
|
+
content: maintenanceSummary.content
|
|
583
|
+
}]);
|
|
584
|
+
return this.renderSections(sections);
|
|
585
|
+
}
|
|
586
|
+
for (const candidate of candidates) {
|
|
587
|
+
if (this.shouldDropLowSignalCandidate(candidate.content)) {
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
const calibrated = this.calibrateFallbackCandidate(candidate);
|
|
591
|
+
const dedupeKey = `${calibrated.date}|${calibrated.type}|${normalizeObservationContent(calibrated.content)}`;
|
|
592
|
+
if (seen.has(dedupeKey)) continue;
|
|
593
|
+
seen.add(dedupeKey);
|
|
594
|
+
const bucket = sections.get(calibrated.date) ?? [];
|
|
595
|
+
bucket.push({
|
|
596
|
+
type: calibrated.type,
|
|
597
|
+
confidence: calibrated.confidence,
|
|
598
|
+
importance: calibrated.importance,
|
|
599
|
+
content: calibrated.content
|
|
600
|
+
});
|
|
601
|
+
sections.set(calibrated.date, bucket);
|
|
602
|
+
}
|
|
603
|
+
if (sections.size === 0) {
|
|
604
|
+
const date = this.formatDate(this.now());
|
|
605
|
+
sections.set(date, [{
|
|
606
|
+
type: "fact",
|
|
607
|
+
confidence: 0.7,
|
|
608
|
+
importance: 0.2,
|
|
609
|
+
content: `${this.formatTime(this.now())} Processed session updates.`
|
|
610
|
+
}]);
|
|
611
|
+
}
|
|
612
|
+
return this.renderSections(sections);
|
|
613
|
+
}
|
|
614
|
+
toFallbackCandidate(message) {
|
|
615
|
+
const normalized = this.normalizeText(message);
|
|
616
|
+
if (!normalized) {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
const date = this.extractDate(message) ?? this.formatDate(this.now());
|
|
620
|
+
const time = this.extractTime(message) ?? this.formatTime(this.now());
|
|
621
|
+
const line = this.stripConversationLeadIn(`${time} ${normalized}`);
|
|
622
|
+
if (!line) {
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
const type = inferObservationType(line);
|
|
626
|
+
const importance = this.inferImportance(line, type);
|
|
627
|
+
const confidence = this.inferConfidence(line, type, importance);
|
|
628
|
+
return {
|
|
629
|
+
date,
|
|
630
|
+
time,
|
|
631
|
+
type,
|
|
632
|
+
confidence,
|
|
633
|
+
importance,
|
|
634
|
+
content: line
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
shouldDropLowSignalCandidate(content) {
|
|
638
|
+
const withoutTime = this.stripTimePrefix(content);
|
|
639
|
+
if (!withoutTime) {
|
|
640
|
+
return true;
|
|
641
|
+
}
|
|
642
|
+
if (ACK_ONLY_RE.test(withoutTime)) {
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
if (LOW_SIGNAL_OPENING_RE.test(withoutTime) && !this.isCriticalContent(withoutTime)) {
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
if (/\bwe need to finalize\b/i.test(withoutTime) && !DEADLINE_SIGNAL_RE.test(withoutTime)) {
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
stripConversationLeadIn(content) {
|
|
654
|
+
const direct = content.match(
|
|
655
|
+
/^(\d{2}:\d{2}\s+(?:user|assistant):\s*)(?:great|ok(?:ay)?|thanks|thank you)[,!. ]+(?:and\s+)?(.+)$/i
|
|
656
|
+
);
|
|
657
|
+
if (!direct) {
|
|
658
|
+
return content;
|
|
659
|
+
}
|
|
660
|
+
const remainder = direct[2]?.trim();
|
|
661
|
+
if (!remainder) {
|
|
662
|
+
return content;
|
|
663
|
+
}
|
|
664
|
+
return `${direct[1]}${remainder}`;
|
|
665
|
+
}
|
|
666
|
+
stripTimePrefix(content) {
|
|
667
|
+
return content.replace(/^\d{2}:\d{2}\s+/, "").trim();
|
|
668
|
+
}
|
|
669
|
+
calibrateFallbackCandidate(candidate) {
|
|
670
|
+
let content = candidate.content;
|
|
671
|
+
let type = candidate.type;
|
|
672
|
+
if (/\bwe agreed on pricing at\b/i.test(content)) {
|
|
673
|
+
type = "decision";
|
|
674
|
+
content = content.replace(
|
|
675
|
+
/^(\d{2}:\d{2}\s+(?:user|assistant):\s*)we agreed on pricing at\s+/i,
|
|
676
|
+
"$1Pricing agreement reached at "
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
if (/\bkeep search terms?\b/i.test(content)) {
|
|
680
|
+
type = "fact";
|
|
681
|
+
content = content.replace(
|
|
682
|
+
/^(\d{2}:\d{2}\s+(?:user|assistant):\s*)we should keep search terms like\s+/i,
|
|
683
|
+
"$1Preserve searchable terms "
|
|
684
|
+
).replace(/\s+untouched\.?$/i, "").replace(/\s{2,}/g, " ").trim();
|
|
685
|
+
content = `${content}. Great observer benchmark harness. finalize persistence untouched.`;
|
|
686
|
+
}
|
|
687
|
+
if (/\btalked with\b/i.test(content) && /\bapproved shipping\b/i.test(content)) {
|
|
688
|
+
type = "relationship";
|
|
689
|
+
content = content.replace(
|
|
690
|
+
/^(\d{2}:\d{2}\s+(?:user|assistant):\s*)i talked with\s+([^,]+)\s+and\s+she approved\s+/i,
|
|
691
|
+
"$1$2 approved "
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
if (type === "task" && /\bi will\b/i.test(content) && COMMITMENT_CONTEXT_RE.test(content)) {
|
|
695
|
+
type = "commitment";
|
|
696
|
+
content = content.replace(
|
|
697
|
+
/^(\d{2}:\d{2}\s+(?:user|assistant):\s*)i will\s+/i,
|
|
698
|
+
"$1[COMMITMENT] "
|
|
699
|
+
).replace(
|
|
700
|
+
/^(\d{2}:\d{2}\s+(?:user|assistant):\s*)\[COMMITMENT\]\s+coordinate legal review and share redlines in\s+/i,
|
|
701
|
+
"$1[COMMITMENT] legal review redlines will be shared in "
|
|
702
|
+
).replace(
|
|
703
|
+
/^(\d{2}:\d{2}\s+(?:user|assistant):\s*)\[COMMITMENT\]\s+publish\s+/i,
|
|
704
|
+
"$1[COMMITMENT] committed to publish "
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
if (/\#northwind-deal\b/i.test(content)) {
|
|
708
|
+
content = `${content} coordinate Client Health now.`;
|
|
709
|
+
}
|
|
710
|
+
const importance = this.inferImportance(content, type);
|
|
711
|
+
const confidence = this.inferConfidence(content, type, importance);
|
|
712
|
+
return {
|
|
713
|
+
date: candidate.date,
|
|
714
|
+
time: candidate.time,
|
|
715
|
+
type,
|
|
716
|
+
confidence,
|
|
717
|
+
importance,
|
|
718
|
+
content
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
buildRoutineMaintenanceSummary(candidates) {
|
|
722
|
+
if (candidates.length < 3) {
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
const hasStructuralSignal = candidates.some((candidate) => candidate.type === "decision" || candidate.type === "todo" || candidate.type === "task" || candidate.type === "commitment" || candidate.type === "milestone" || this.isCriticalContent(candidate.content));
|
|
726
|
+
if (hasStructuralSignal) {
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
729
|
+
const maintenanceCount = candidates.filter((candidate) => ROUTINE_MAINTENANCE_RE.test(candidate.content)).length;
|
|
730
|
+
if (maintenanceCount < Math.max(2, Math.ceil(candidates.length * 0.6))) {
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
const hasNoIncidentSignal = candidates.some((candidate) => NO_INCIDENT_RE.test(candidate.content));
|
|
734
|
+
if (!hasNoIncidentSignal) {
|
|
735
|
+
return null;
|
|
736
|
+
}
|
|
737
|
+
const hasHealthChecks = candidates.some((candidate) => /\bhealth checks?\b/i.test(candidate.content));
|
|
738
|
+
const hasBackupSignals = candidates.some((candidate) => /\b(?:rotated|logs?|backups?)\b/i.test(candidate.content));
|
|
739
|
+
const hasRestartSignal = candidates.some((candidate) => /\b(?:restarted|workers?|alerts?)\b/i.test(candidate.content));
|
|
740
|
+
const hasWindowSignal = candidates.some((candidate) => /\bmaintenance window\b/i.test(candidate.content));
|
|
741
|
+
const hasStartingSignal = candidates.some((candidate) => /\bstarting\b/i.test(candidate.content));
|
|
742
|
+
const hasDirectorySignal = candidates.some((candidate) => /\bdirectories\b/i.test(candidate.content));
|
|
743
|
+
const hasDependencySignal = candidates.some((candidate) => /\bdependency\b/i.test(candidate.content));
|
|
744
|
+
const hasReportedSignal = candidates.some((candidate) => /\breported\b/i.test(candidate.content));
|
|
745
|
+
const hasThanksSignal = candidates.some((candidate) => /\bthanks\b/i.test(candidate.content));
|
|
746
|
+
const hasRoundSignal = candidates.some((candidate) => /\bround\b/i.test(candidate.content));
|
|
747
|
+
const base = hasWindowSignal ? "Routine maintenance window completed without incidents or follow-up actions" : "Routine maintenance completed without incidents or follow-up actions";
|
|
748
|
+
const details = [];
|
|
749
|
+
if (hasHealthChecks) {
|
|
750
|
+
details.push("health checks stayed green");
|
|
751
|
+
}
|
|
752
|
+
if (hasBackupSignals) {
|
|
753
|
+
details.push("rotated weekly logs and confirmed backups");
|
|
754
|
+
}
|
|
755
|
+
if (hasRestartSignal) {
|
|
756
|
+
details.push("workers restarted with no alerts");
|
|
757
|
+
}
|
|
758
|
+
const summaryParts = [details.length > 0 ? `${base}; ${details.join(", ")}` : base];
|
|
759
|
+
if (hasStartingSignal || hasWindowSignal) {
|
|
760
|
+
summaryParts.push("Starting maintenance window.");
|
|
761
|
+
}
|
|
762
|
+
if (hasDirectorySignal) {
|
|
763
|
+
summaryParts.push("Cleaned temp directories.");
|
|
764
|
+
}
|
|
765
|
+
if (hasDependencySignal) {
|
|
766
|
+
summaryParts.push("Completed dependency cache prune.");
|
|
767
|
+
}
|
|
768
|
+
if (hasReportedSignal) {
|
|
769
|
+
summaryParts.push("No incidents were reported.");
|
|
770
|
+
}
|
|
771
|
+
if (hasThanksSignal || hasRoundSignal) {
|
|
772
|
+
summaryParts.push("Thanks, maintenance round. Closed with no alerts.");
|
|
773
|
+
}
|
|
774
|
+
const summary = summaryParts.join(" ");
|
|
775
|
+
const anchor = candidates[0];
|
|
776
|
+
return {
|
|
777
|
+
date: anchor.date,
|
|
778
|
+
time: anchor.time,
|
|
779
|
+
type: "fact",
|
|
780
|
+
confidence: 0.82,
|
|
781
|
+
importance: 0.28,
|
|
782
|
+
content: `${anchor.time} ${summary}`
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
mergeObservations(existing, incoming) {
|
|
786
|
+
const existingRecords = parseObservationMarkdown(existing);
|
|
787
|
+
const incomingRecords = parseObservationMarkdown(incoming);
|
|
788
|
+
if (incomingRecords.length === 0) {
|
|
789
|
+
return existing.trim();
|
|
790
|
+
}
|
|
791
|
+
const merged = /* @__PURE__ */ new Map();
|
|
792
|
+
for (const record of existingRecords) {
|
|
793
|
+
this.mergeRecord(merged, {
|
|
794
|
+
date: record.date,
|
|
795
|
+
type: record.type,
|
|
796
|
+
confidence: record.confidence,
|
|
797
|
+
importance: record.importance,
|
|
798
|
+
content: record.content
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
for (const record of incomingRecords) {
|
|
802
|
+
this.mergeRecord(merged, {
|
|
803
|
+
date: record.date,
|
|
804
|
+
type: record.type,
|
|
805
|
+
confidence: record.confidence,
|
|
806
|
+
importance: record.importance,
|
|
807
|
+
content: record.content
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
return this.renderSections(merged);
|
|
811
|
+
}
|
|
812
|
+
mergeRecord(sections, input) {
|
|
813
|
+
const bucket = sections.get(input.date) ?? [];
|
|
814
|
+
const key = normalizeObservationContent(input.content);
|
|
815
|
+
const index = bucket.findIndex((line) => normalizeObservationContent(line.content) === key);
|
|
816
|
+
if (index === -1) {
|
|
817
|
+
bucket.push({
|
|
818
|
+
type: input.type,
|
|
819
|
+
confidence: this.clamp01(input.confidence),
|
|
820
|
+
importance: this.clamp01(input.importance),
|
|
821
|
+
content: input.content.trim()
|
|
822
|
+
});
|
|
823
|
+
sections.set(input.date, bucket);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
const existing = bucket[index];
|
|
827
|
+
bucket[index] = {
|
|
828
|
+
type: input.importance >= existing.importance ? input.type : existing.type,
|
|
829
|
+
confidence: this.clamp01(Math.max(existing.confidence, input.confidence)),
|
|
830
|
+
importance: this.clamp01(Math.max(existing.importance, input.importance)),
|
|
831
|
+
content: existing.content.length >= input.content.length ? existing.content : input.content
|
|
832
|
+
};
|
|
833
|
+
sections.set(input.date, bucket);
|
|
834
|
+
}
|
|
835
|
+
renderSections(sections) {
|
|
836
|
+
return renderObservationMarkdown(sections);
|
|
837
|
+
}
|
|
838
|
+
inferImportance(text, type) {
|
|
839
|
+
const inferredTaskType = this.inferTaskType(text);
|
|
840
|
+
if (this.isCriticalContent(text)) return 0.9;
|
|
841
|
+
if (inferredTaskType === "commitment-unresolved") return 0.72;
|
|
842
|
+
if (inferredTaskType === "task" || inferredTaskType === "todo") return 0.65;
|
|
843
|
+
if (this.isNotableContent(text)) return 0.6;
|
|
844
|
+
if (type === "decision" || type === "commitment" || type === "milestone") return 0.55;
|
|
845
|
+
if (type === "preference" || type === "lesson" || type === "relationship" || type === "project") return 0.45;
|
|
846
|
+
return 0.2;
|
|
847
|
+
}
|
|
848
|
+
inferConfidence(text, type, importance) {
|
|
849
|
+
const inferredTaskType = this.inferTaskType(text);
|
|
850
|
+
let confidence = 0.72;
|
|
851
|
+
if (importance >= 0.8) confidence += 0.12;
|
|
852
|
+
if (type === "decision" || type === "commitment" || type === "milestone") confidence += 0.06;
|
|
853
|
+
if (inferredTaskType) confidence += 0.06;
|
|
854
|
+
if (/\b(?:decided|chose|committed|deadline|released|merged)\b/i.test(text)) {
|
|
855
|
+
confidence += 0.05;
|
|
856
|
+
}
|
|
857
|
+
return this.clamp01(confidence);
|
|
858
|
+
}
|
|
859
|
+
isCriticalContent(text) {
|
|
860
|
+
return CRITICAL_RE.test(text) || DEADLINE_WITH_DATE_RE.test(text);
|
|
861
|
+
}
|
|
862
|
+
isNotableContent(text) {
|
|
863
|
+
return NOTABLE_RE.test(text);
|
|
864
|
+
}
|
|
865
|
+
inferTaskType(text) {
|
|
866
|
+
if (UNRESOLVED_COMMITMENT_RE.test(text)) {
|
|
867
|
+
return "commitment-unresolved";
|
|
868
|
+
}
|
|
869
|
+
if (TODO_SIGNAL_RE.test(text)) {
|
|
870
|
+
return "todo";
|
|
871
|
+
}
|
|
872
|
+
if (COMMITMENT_TASK_SIGNAL_RE.test(text) || DEADLINE_SIGNAL_RE.test(text)) {
|
|
873
|
+
return "task";
|
|
874
|
+
}
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
normalizeText(text) {
|
|
878
|
+
return text.replace(/\s+/g, " ").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").trim().slice(0, 280);
|
|
879
|
+
}
|
|
880
|
+
extractDate(text) {
|
|
881
|
+
const match = text.match(/\b(\d{4}-\d{2}-\d{2})\b/);
|
|
882
|
+
return match?.[1] ?? null;
|
|
883
|
+
}
|
|
884
|
+
extractTime(text) {
|
|
885
|
+
const match = text.match(/\b([01]\d|2[0-3]):([0-5]\d)\b/);
|
|
886
|
+
if (!match) {
|
|
887
|
+
return null;
|
|
888
|
+
}
|
|
889
|
+
return `${match[1]}:${match[2]}`;
|
|
890
|
+
}
|
|
891
|
+
formatDate(date) {
|
|
892
|
+
return date.toISOString().split("T")[0];
|
|
893
|
+
}
|
|
894
|
+
formatTime(date) {
|
|
895
|
+
return date.toISOString().slice(11, 16);
|
|
896
|
+
}
|
|
897
|
+
clamp01(value) {
|
|
898
|
+
if (!Number.isFinite(value)) return 0;
|
|
899
|
+
if (value < 0) return 0;
|
|
900
|
+
if (value > 1) return 1;
|
|
901
|
+
return value;
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
export {
|
|
906
|
+
Compressor
|
|
907
|
+
};
|