@oooooooooo/ch0de 0.0.0-stage → 1.9.56
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/API-TESTING.md +76 -0
- package/DEBTAUR-INTEGRATION.md +71 -0
- package/EDITING.md +80 -0
- package/EXPERT-PANEL.md +16 -0
- package/INTEGRATIONS.md +75 -0
- package/LICENSE +21 -0
- package/MODELS-DEV-LICENSE +21 -0
- package/PROVIDER-CONNECTIONS.md +48 -0
- package/PROVIDERS.md +17 -0
- package/README.md +523 -2
- package/RELIABILITY.md +95 -0
- package/UX-1.3.md +83 -0
- package/UX-1.4.1.md +13 -0
- package/UX-1.4.md +48 -0
- package/UX-1.5.md +13 -0
- package/UX-1.6.md +7 -0
- package/UX-1.7.3.md +13 -0
- package/UX-1.7.4.md +13 -0
- package/UX-1.7.md +19 -0
- package/acceptance-check.js +27 -0
- package/agent-budget.js +4 -0
- package/api-testing.js +140 -0
- package/automatic-re.js +19 -0
- package/benchmarks/public-baselines.json +29 -0
- package/benchmarks/render-1.4.0.json +8 -0
- package/benchmarks/targets.js +11 -0
- package/browser-check.js +1062 -0
- package/build-progress.js +10 -0
- package/build-quality.js +49 -0
- package/chode.js +7937 -0
- package/debtaur-engine.js +305 -0
- package/debtaur-orchestration.js +456 -0
- package/debtaur-worker.cjs +76 -0
- package/doctor.js +24 -0
- package/edit-transactions.js +79 -0
- package/elapsed.js +28 -0
- package/evidence-ledger.js +26 -0
- package/expert-panel.cjs +42 -0
- package/helios-ledger.js +405 -0
- package/install.cjs +25 -0
- package/integrations.js +30 -0
- package/json-output.js +8 -0
- package/local-memory.js +613 -0
- package/local-models.js +52 -0
- package/mcp-client.js +50 -0
- package/memory.js +511 -0
- package/model-catalog.js +46 -0
- package/native-tools.js +27 -0
- package/nosignups.js +325 -0
- package/observation-progress.js +33 -0
- package/orchestrator.js +299 -0
- package/package.json +160 -6
- package/plan-z6.js +490 -0
- package/project-memory.js +27 -0
- package/provider-capacity.js +13 -0
- package/provider-catalog.js +246 -0
- package/provider-connect.js +66 -0
- package/provider-credentials.js +5 -0
- package/provider-directory.js +8 -0
- package/provider-directory.json +32300 -0
- package/provider-errors.js +11 -0
- package/provider-picker.js +188 -0
- package/provider-policy.js +4 -0
- package/provider-race.js +25 -0
- package/provider-request.js +45 -0
- package/provider-store.js +13 -0
- package/rea-environment.js +5 -0
- package/read-window.js +21 -0
- package/recovery-context.js +3 -0
- package/reliability-memory.js +38 -0
- package/reliability-services.js +34 -0
- package/reliability-store.js +16 -0
- package/request-deadline.js +4 -0
- package/serve-frontend.html +172 -0
- package/settings-reset.js +11 -0
- package/skill-catalog.js +16 -0
- package/task-group.js +22 -0
- package/terminal-graphics.js +39 -0
- package/terminal-logo.js +76 -0
- package/terminal-ui.js +79 -0
- package/tool-call-validation.js +26 -0
- package/tool-health.js +22 -0
- package/tool-history.js +5 -0
- package/toolbox.js +156 -0
- package/tui-provider-picker.js +134 -0
- package/tui-worker.js +32 -0
- package/web-context.js +232 -0
- package/wolfram-math.js +6 -0
- package/worker-transport.js +11 -0
- package/workspace-commands.js +45 -0
- package/workspace-log.js +56 -0
- package/workspace-store.js +7 -0
- package/workspace-tui.js +113 -0
- package/workspace-view.js +109 -0
- package/worktrees.js +18 -0
- package/z6-integration.js +227 -0
package/local-memory.js
ADDED
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Local Memory Module for Chode
|
|
5
|
+
*
|
|
6
|
+
* Filesystem-based persistent memory with Z6P-inspired design:
|
|
7
|
+
* - Hash-bound state vectors for integrity
|
|
8
|
+
* - Local, traversable storage in .chode/memory/
|
|
9
|
+
* - Session compression after N messages
|
|
10
|
+
* - Handoff state for provider switching
|
|
11
|
+
* - Constraint validation hooks
|
|
12
|
+
*
|
|
13
|
+
* Replaces HEMO vault dependency (remote HTTP) with local-first approach.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const crypto = require('crypto');
|
|
19
|
+
|
|
20
|
+
// Resolved lazily so in-process cwd changes (tests, chdir) keep memory local.
|
|
21
|
+
function dirs() {
|
|
22
|
+
const memory = path.join(process.cwd(), '.chode', 'memory');
|
|
23
|
+
return { memory: memory, sessions: path.join(memory, 'sessions'), audit: path.join(memory, 'audit') };
|
|
24
|
+
}
|
|
25
|
+
const COMPRESSION_THRESHOLD = 20;
|
|
26
|
+
const MAX_MESSAGES = 50;
|
|
27
|
+
|
|
28
|
+
// Z6 state vector indices
|
|
29
|
+
const Z6_STATE = {
|
|
30
|
+
STEP: 0, // Current step index (0-5)
|
|
31
|
+
STATUS: 1, // Session status (0=active, 1=paused, 2=completed, 3=failed, 4=blocked, 5=waiting)
|
|
32
|
+
PROVIDER_HEALTH: 2, // Provider health score (0-5)
|
|
33
|
+
CAPACITY: 3, // Available capacity (0-5)
|
|
34
|
+
QUEUE_DEPTH: 4, // Pending tasks (0-5)
|
|
35
|
+
ERROR_STATE: 5 // Error state (0=none, 1=recoverable, 2=unrecoverable, 3=rate-limited, 4=auth-failed, 5=timeout)
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function ensureDirs() {
|
|
39
|
+
const { memory: MEMORY_DIR, sessions: SESSIONS_DIR, audit: AUDIT_DIR } = dirs();
|
|
40
|
+
if (!fs.existsSync(MEMORY_DIR)) fs.mkdirSync(MEMORY_DIR, { recursive: true });
|
|
41
|
+
if (!fs.existsSync(SESSIONS_DIR)) fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
42
|
+
if (!fs.existsSync(AUDIT_DIR)) fs.mkdirSync(AUDIT_DIR, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function hash(data) {
|
|
46
|
+
return crypto.createHash('sha256').update(data).digest('hex');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function selfHash(data, primaryHash) {
|
|
50
|
+
return crypto.createHash('sha256').update(data + primaryHash).digest('hex');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Canonical payload every session hash is computed over. Both the write path
|
|
54
|
+
// and the verification path must build this object with identical key order.
|
|
55
|
+
function sessionStatePayload(id, stateVector, plan, handoff) {
|
|
56
|
+
return JSON.stringify({ id: id, stateVector: stateVector || [0, 0, 0, 0, 0, 0], plan: plan, handoff: handoff });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function verifyStoredState(data, where) {
|
|
60
|
+
const payload = sessionStatePayload(data.id, data.stateVector, data.plan, data.handoff);
|
|
61
|
+
const expected = hash(payload);
|
|
62
|
+
const expectedSelf = selfHash(payload, expected);
|
|
63
|
+
if (data.hash !== expected || data.selfHash !== expectedSelf) {
|
|
64
|
+
throw new Error(where + ' corrupted: hash mismatch');
|
|
65
|
+
}
|
|
66
|
+
return { hash: expected, selfHash: expectedSelf };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Keys an integrity layer added on top of a payload; stripping them recovers
|
|
70
|
+
// the exact bytes the hash was taken over (insertion order is preserved).
|
|
71
|
+
function stripIntegrityKeys(record) {
|
|
72
|
+
const copy = { ...record };
|
|
73
|
+
delete copy.timestamp;
|
|
74
|
+
delete copy.hash;
|
|
75
|
+
delete copy.selfHash;
|
|
76
|
+
return copy;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Session record with Z6 state vector
|
|
81
|
+
*/
|
|
82
|
+
class Session {
|
|
83
|
+
constructor(id, metadata = {}) {
|
|
84
|
+
this.id = id;
|
|
85
|
+
this.createdAt = Date.now();
|
|
86
|
+
this.updatedAt = Date.now();
|
|
87
|
+
this.messages = [];
|
|
88
|
+
this.plan = null;
|
|
89
|
+
this.handoff = null;
|
|
90
|
+
this.stateVector = [0, 0, 0, 0, 0, 0]; // Z6 state
|
|
91
|
+
this.metadata = metadata;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
statePayload() {
|
|
95
|
+
return sessionStatePayload(this.id, this.stateVector, this.plan, this.handoff);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
computeHash() {
|
|
99
|
+
return hash(this.statePayload());
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
computeSelfHash() {
|
|
103
|
+
const payload = this.statePayload();
|
|
104
|
+
return selfHash(payload, hash(payload));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
serialize() {
|
|
108
|
+
return {
|
|
109
|
+
id: this.id,
|
|
110
|
+
createdAt: this.createdAt,
|
|
111
|
+
updatedAt: this.updatedAt,
|
|
112
|
+
messages: this.messages,
|
|
113
|
+
plan: this.plan,
|
|
114
|
+
handoff: this.handoff,
|
|
115
|
+
stateVector: this.stateVector,
|
|
116
|
+
metadata: this.metadata,
|
|
117
|
+
hash: this.computeHash(),
|
|
118
|
+
selfHash: this.computeSelfHash()
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
static deserialize(data) {
|
|
123
|
+
const s = new Session(data.id, data.metadata);
|
|
124
|
+
s.createdAt = data.createdAt;
|
|
125
|
+
s.updatedAt = data.updatedAt;
|
|
126
|
+
s.messages = []; // Messages loaded from individual files, not meta.json
|
|
127
|
+
s.plan = data.plan;
|
|
128
|
+
s.handoff = data.handoff;
|
|
129
|
+
s.stateVector = data.stateVector || [0, 0, 0, 0, 0, 0];
|
|
130
|
+
return s;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Message record with hash-bound integrity
|
|
136
|
+
*/
|
|
137
|
+
class Message {
|
|
138
|
+
constructor(role, content, metadata = {}) {
|
|
139
|
+
this.id = crypto.randomUUID();
|
|
140
|
+
this.role = role;
|
|
141
|
+
this.content = content;
|
|
142
|
+
this.timestamp = Date.now();
|
|
143
|
+
this.metadata = metadata;
|
|
144
|
+
this.hash = null;
|
|
145
|
+
this.selfHash = null;
|
|
146
|
+
this.computeHashes();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
computeHashes() {
|
|
150
|
+
const data = JSON.stringify({
|
|
151
|
+
id: this.id,
|
|
152
|
+
role: this.role,
|
|
153
|
+
content: this.content,
|
|
154
|
+
timestamp: this.timestamp,
|
|
155
|
+
metadata: this.metadata
|
|
156
|
+
});
|
|
157
|
+
this.hash = hash(data);
|
|
158
|
+
this.selfHash = selfHash(data, this.hash);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
serialize() {
|
|
162
|
+
return {
|
|
163
|
+
...this,
|
|
164
|
+
hash: this.hash,
|
|
165
|
+
selfHash: this.selfHash
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Verify a message record read from disk against its stored hashes.
|
|
171
|
+
function verifyMessageRecord(msgData, where) {
|
|
172
|
+
const payload = JSON.stringify({
|
|
173
|
+
id: msgData.id,
|
|
174
|
+
role: msgData.role,
|
|
175
|
+
content: msgData.content,
|
|
176
|
+
timestamp: msgData.timestamp,
|
|
177
|
+
metadata: msgData.metadata
|
|
178
|
+
});
|
|
179
|
+
const expected = hash(payload);
|
|
180
|
+
const expectedSelf = selfHash(payload, expected);
|
|
181
|
+
if (msgData.hash !== expected || msgData.selfHash !== expectedSelf) {
|
|
182
|
+
throw new Error(where + ' corrupted: hash mismatch');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Local Memory Manager
|
|
188
|
+
*/
|
|
189
|
+
class LocalMemoryManager {
|
|
190
|
+
constructor() {
|
|
191
|
+
ensureDirs();
|
|
192
|
+
this.sessions = new Map();
|
|
193
|
+
this.auditLog = [];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Create or load a session
|
|
198
|
+
*/
|
|
199
|
+
getOrCreateSession(sessionId, metadata = {}) {
|
|
200
|
+
const { sessions: SESSIONS_DIR } = dirs();
|
|
201
|
+
const sessionPath = path.join(SESSIONS_DIR, sessionId, 'meta.json');
|
|
202
|
+
|
|
203
|
+
if (fs.existsSync(sessionPath)) {
|
|
204
|
+
let data;
|
|
205
|
+
try {
|
|
206
|
+
data = JSON.parse(fs.readFileSync(sessionPath, 'utf8'));
|
|
207
|
+
} catch (e) {
|
|
208
|
+
throw new Error('Session corrupted (parse error): ' + sessionId);
|
|
209
|
+
}
|
|
210
|
+
verifyStoredState(data, 'Session');
|
|
211
|
+
const session = Session.deserialize(data);
|
|
212
|
+
|
|
213
|
+
// Load messages and verify each one's integrity
|
|
214
|
+
const messagesDir = path.join(SESSIONS_DIR, sessionId, 'messages');
|
|
215
|
+
if (fs.existsSync(messagesDir)) {
|
|
216
|
+
const files = fs.readdirSync(messagesDir).sort();
|
|
217
|
+
for (const file of files) {
|
|
218
|
+
let msgData;
|
|
219
|
+
try {
|
|
220
|
+
msgData = JSON.parse(fs.readFileSync(path.join(messagesDir, file), 'utf8'));
|
|
221
|
+
} catch (e) {
|
|
222
|
+
throw new Error('Message corrupted (parse error): ' + sessionId + '/' + file);
|
|
223
|
+
}
|
|
224
|
+
verifyMessageRecord(msgData, 'Message');
|
|
225
|
+
session.messages.push(msgData);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
this.sessions.set(sessionId, session);
|
|
230
|
+
return session;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const session = new Session(sessionId, metadata);
|
|
234
|
+
this.sessions.set(sessionId, session);
|
|
235
|
+
this.saveSession(session);
|
|
236
|
+
return session;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Save session to disk
|
|
241
|
+
*/
|
|
242
|
+
saveSession(session) {
|
|
243
|
+
const { sessions: SESSIONS_DIR } = dirs();
|
|
244
|
+
session.updatedAt = Date.now();
|
|
245
|
+
const sessionDir = path.join(SESSIONS_DIR, session.id);
|
|
246
|
+
if (!fs.existsSync(sessionDir)) fs.mkdirSync(sessionDir, { recursive: true });
|
|
247
|
+
|
|
248
|
+
// Save metadata atomically (temp file + rename, like build-progress) so a
|
|
249
|
+
// crash mid-write never leaves a truncated meta.json behind.
|
|
250
|
+
const metaPath = path.join(sessionDir, 'meta.json');
|
|
251
|
+
const tempPath = metaPath + '.' + process.pid + '.' + crypto.randomBytes(4).toString('hex') + '.tmp';
|
|
252
|
+
try {
|
|
253
|
+
fs.writeFileSync(tempPath, JSON.stringify(session.serialize(), null, 2), 'utf8');
|
|
254
|
+
fs.renameSync(tempPath, metaPath);
|
|
255
|
+
} finally {
|
|
256
|
+
try { fs.unlinkSync(tempPath); } catch (_) {}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Save messages individually (for traversability)
|
|
260
|
+
const messagesDir = path.join(sessionDir, 'messages');
|
|
261
|
+
if (!fs.existsSync(messagesDir)) fs.mkdirSync(messagesDir, { recursive: true });
|
|
262
|
+
|
|
263
|
+
for (let i = 0; i < session.messages.length; i++) {
|
|
264
|
+
const msg = session.messages[i];
|
|
265
|
+
const msgPath = path.join(messagesDir, `${String(i + 1).padStart(4, '0')}.json`);
|
|
266
|
+
if (!fs.existsSync(msgPath)) {
|
|
267
|
+
const msgData = msg.serialize ? msg.serialize() : msg;
|
|
268
|
+
fs.writeFileSync(msgPath, JSON.stringify(msgData, null, 2), 'utf8');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Log state transition to audit
|
|
273
|
+
this.logTransition(session);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Add a message to session
|
|
278
|
+
*/
|
|
279
|
+
addMessage(sessionId, role, content, metadata = {}) {
|
|
280
|
+
const session = this.getOrCreateSession(sessionId);
|
|
281
|
+
const message = new Message(role, content, metadata);
|
|
282
|
+
session.messages.push(message);
|
|
283
|
+
|
|
284
|
+
// Compress if over threshold
|
|
285
|
+
if (session.messages.length > COMPRESSION_THRESHOLD * 2) {
|
|
286
|
+
this.compressSession(session);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Trim to max
|
|
290
|
+
if (session.messages.length > MAX_MESSAGES) {
|
|
291
|
+
session.messages = session.messages.slice(-MAX_MESSAGES);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
this.saveSession(session);
|
|
295
|
+
return message;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Compress old messages (keep recent + summary)
|
|
300
|
+
*/
|
|
301
|
+
compressSession(session) {
|
|
302
|
+
const keepCount = COMPRESSION_THRESHOLD;
|
|
303
|
+
const toRemove = session.messages.length - keepCount;
|
|
304
|
+
|
|
305
|
+
if (toRemove <= 0) return;
|
|
306
|
+
|
|
307
|
+
// Create summary of removed messages
|
|
308
|
+
const summary = new Message('system', `[SUMMARY: ${toRemove} messages compressed at ${new Date().toISOString()}]`, {
|
|
309
|
+
compressedCount: toRemove
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Keep last N messages
|
|
313
|
+
session.messages = [summary, ...session.messages.slice(-keepCount)];
|
|
314
|
+
|
|
315
|
+
// Re-save all messages (overwrites old files)
|
|
316
|
+
const { sessions: SESSIONS_DIR } = dirs();
|
|
317
|
+
const messagesDir = path.join(SESSIONS_DIR, session.id, 'messages');
|
|
318
|
+
if (fs.existsSync(messagesDir)) {
|
|
319
|
+
fs.rmSync(messagesDir, { recursive: true, force: true });
|
|
320
|
+
}
|
|
321
|
+
fs.mkdirSync(messagesDir, { recursive: true });
|
|
322
|
+
|
|
323
|
+
for (let i = 0; i < session.messages.length; i++) {
|
|
324
|
+
const msg = session.messages[i];
|
|
325
|
+
const msgPath = path.join(messagesDir, `${String(i + 1).padStart(4, '0')}.json`);
|
|
326
|
+
const msgData = msg.serialize ? msg.serialize() : msg;
|
|
327
|
+
fs.writeFileSync(msgPath, JSON.stringify(msgData, null, 2), 'utf8');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
this.saveSession(session);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Set handoff state for provider switching
|
|
335
|
+
*/
|
|
336
|
+
setHandoff(sessionId, handoff) {
|
|
337
|
+
const session = this.getOrCreateSession(sessionId);
|
|
338
|
+
// Never overwrite keys the payload already owns (a Z6 handoff carries its
|
|
339
|
+
// own state hashes); only plain handoffs get the memory-level envelope.
|
|
340
|
+
if (handoff === null || typeof handoff !== 'object' || Array.isArray(handoff)) {
|
|
341
|
+
throw new Error('Handoff must be a plain object');
|
|
342
|
+
}
|
|
343
|
+
const payload = JSON.stringify({ ...handoff });
|
|
344
|
+
const payloadHash = hash(payload);
|
|
345
|
+
const record = { ...handoff, timestamp: Date.now() };
|
|
346
|
+
if (record.hash === undefined) record.hash = payloadHash;
|
|
347
|
+
if (record.selfHash === undefined) record.selfHash = selfHash(payload, payloadHash);
|
|
348
|
+
session.handoff = record;
|
|
349
|
+
session.stateVector[Z6_STATE.STEP] = handoff.step || 0;
|
|
350
|
+
this.saveSession(session);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Get handoff state (verifies stored hash; throws on tampering)
|
|
355
|
+
*/
|
|
356
|
+
getHandoff(sessionId) {
|
|
357
|
+
const session = this.getOrCreateSession(sessionId);
|
|
358
|
+
const stored = session.handoff;
|
|
359
|
+
if (!stored) return null;
|
|
360
|
+
const envelopePayload = JSON.stringify(stripIntegrityKeys(stored));
|
|
361
|
+
const envelopeHash = hash(envelopePayload);
|
|
362
|
+
let ok = stored.hash === envelopeHash && stored.selfHash === selfHash(envelopePayload, envelopeHash);
|
|
363
|
+
if (!ok && stored.state && typeof stored.state === 'object') {
|
|
364
|
+
// Self-hashed shape (Z6P handoff): hash covers the state sub-object.
|
|
365
|
+
const stateJson = JSON.stringify(stored.state);
|
|
366
|
+
const stateHash = hash(stateJson);
|
|
367
|
+
ok = stored.hash === stateHash &&
|
|
368
|
+
stored.selfHash === crypto.createHash('sha256').update(stateJson + stateHash).digest('hex');
|
|
369
|
+
}
|
|
370
|
+
if (!ok) throw new Error('Handoff corrupted: hash mismatch');
|
|
371
|
+
return stored;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Set plan (Z6Plan serialized)
|
|
376
|
+
*/
|
|
377
|
+
setPlan(sessionId, plan) {
|
|
378
|
+
const session = this.getOrCreateSession(sessionId);
|
|
379
|
+
session.plan = plan;
|
|
380
|
+
this.saveSession(session);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Get plan
|
|
385
|
+
*/
|
|
386
|
+
getPlan(sessionId) {
|
|
387
|
+
const session = this.getOrCreateSession(sessionId);
|
|
388
|
+
return session.plan;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Update state vector
|
|
393
|
+
*/
|
|
394
|
+
updateStateVector(sessionId, updates) {
|
|
395
|
+
const session = this.getOrCreateSession(sessionId);
|
|
396
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
397
|
+
if (Z6_STATE[key] !== undefined) {
|
|
398
|
+
// Z6 modulo: ensure result is in [0, 5] even for negative values
|
|
399
|
+
session.stateVector[Z6_STATE[key]] = ((value % 6) + 6) % 6;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
this.saveSession(session);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Get state vector
|
|
407
|
+
*/
|
|
408
|
+
getStateVector(sessionId) {
|
|
409
|
+
const session = this.getOrCreateSession(sessionId);
|
|
410
|
+
return [...session.stateVector];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* List all sessions
|
|
415
|
+
*/
|
|
416
|
+
listSessions() {
|
|
417
|
+
const { sessions: SESSIONS_DIR } = dirs();
|
|
418
|
+
const sessions = [];
|
|
419
|
+
if (fs.existsSync(SESSIONS_DIR)) {
|
|
420
|
+
const dirs = fs.readdirSync(SESSIONS_DIR);
|
|
421
|
+
for (const dir of dirs) {
|
|
422
|
+
const metaPath = path.join(SESSIONS_DIR, dir, 'meta.json');
|
|
423
|
+
if (fs.existsSync(metaPath)) {
|
|
424
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
425
|
+
sessions.push({
|
|
426
|
+
id: meta.id,
|
|
427
|
+
createdAt: meta.createdAt,
|
|
428
|
+
updatedAt: meta.updatedAt,
|
|
429
|
+
messageCount: meta.messages?.length || 0,
|
|
430
|
+
stateVector: meta.stateVector,
|
|
431
|
+
hasPlan: !!meta.plan,
|
|
432
|
+
hasHandoff: !!meta.handoff
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return sessions.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Get session summary (without full messages)
|
|
442
|
+
*/
|
|
443
|
+
getSessionSummary(sessionId) {
|
|
444
|
+
const session = this.getOrCreateSession(sessionId);
|
|
445
|
+
return {
|
|
446
|
+
id: session.id,
|
|
447
|
+
createdAt: session.createdAt,
|
|
448
|
+
updatedAt: session.updatedAt,
|
|
449
|
+
messageCount: session.messages.length,
|
|
450
|
+
stateVector: session.stateVector,
|
|
451
|
+
hasPlan: !!session.plan,
|
|
452
|
+
hasHandoff: !!session.handoff,
|
|
453
|
+
hash: session.computeHash(),
|
|
454
|
+
selfHash: session.computeSelfHash()
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Log state transition to audit trail
|
|
460
|
+
*/
|
|
461
|
+
logTransition(session) {
|
|
462
|
+
const { audit: AUDIT_DIR } = dirs();
|
|
463
|
+
if (!fs.existsSync(AUDIT_DIR)) fs.mkdirSync(AUDIT_DIR, { recursive: true });
|
|
464
|
+
const date = new Date().toISOString().split('T')[0];
|
|
465
|
+
const auditFile = path.join(AUDIT_DIR, `${date}.log`);
|
|
466
|
+
|
|
467
|
+
const entry = {
|
|
468
|
+
timestamp: Date.now(),
|
|
469
|
+
sessionId: session.id,
|
|
470
|
+
stateVector: session.stateVector,
|
|
471
|
+
hash: session.computeHash(),
|
|
472
|
+
selfHash: session.computeSelfHash()
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
fs.appendFileSync(auditFile, JSON.stringify(entry) + '\n', 'utf8');
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Delete a session
|
|
480
|
+
*/
|
|
481
|
+
deleteSession(sessionId) {
|
|
482
|
+
const { sessions: SESSIONS_DIR } = dirs();
|
|
483
|
+
const sessionDir = path.join(SESSIONS_DIR, sessionId);
|
|
484
|
+
if (fs.existsSync(sessionDir)) {
|
|
485
|
+
fs.rmSync(sessionDir, { recursive: true, force: true });
|
|
486
|
+
this.sessions.delete(sessionId);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Cleanup old sessions (older than days)
|
|
492
|
+
*/
|
|
493
|
+
cleanup(maxAgeDays = 7) {
|
|
494
|
+
const cutoff = Date.now() - (maxAgeDays * 24 * 60 * 60 * 1000);
|
|
495
|
+
const sessions = this.listSessions();
|
|
496
|
+
let deleted = 0;
|
|
497
|
+
|
|
498
|
+
for (const session of sessions) {
|
|
499
|
+
if (session.updatedAt < cutoff) {
|
|
500
|
+
this.deleteSession(session.id);
|
|
501
|
+
deleted++;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return deleted;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Export session for inspection
|
|
510
|
+
*/
|
|
511
|
+
exportSession(sessionId) {
|
|
512
|
+
const session = this.getOrCreateSession(sessionId);
|
|
513
|
+
return session.serialize();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Verify session integrity from DISK (not the in-memory copy) so tampering
|
|
518
|
+
* with meta.json is actually detected. Tautological in-memory checks are useless.
|
|
519
|
+
*/
|
|
520
|
+
verifySession(sessionId) {
|
|
521
|
+
const { sessions: SESSIONS_DIR } = dirs();
|
|
522
|
+
const sessionDir = path.join(SESSIONS_DIR, sessionId);
|
|
523
|
+
const metaPath = path.join(sessionDir, 'meta.json');
|
|
524
|
+
if (!fs.existsSync(metaPath)) {
|
|
525
|
+
return { valid: false, reason: 'not_found' };
|
|
526
|
+
}
|
|
527
|
+
let data;
|
|
528
|
+
try {
|
|
529
|
+
data = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
530
|
+
} catch (e) {
|
|
531
|
+
return { valid: false, reason: 'parse_error' };
|
|
532
|
+
}
|
|
533
|
+
let expected;
|
|
534
|
+
try {
|
|
535
|
+
expected = verifyStoredState(data, 'Session');
|
|
536
|
+
} catch (e) {
|
|
537
|
+
return { valid: false, reason: 'hash_mismatch' };
|
|
538
|
+
}
|
|
539
|
+
const messagesDir = path.join(sessionDir, 'messages');
|
|
540
|
+
let messageCount = 0;
|
|
541
|
+
if (fs.existsSync(messagesDir)) {
|
|
542
|
+
try { messageCount = fs.readdirSync(messagesDir).length; }
|
|
543
|
+
catch (e) { messageCount = 0; }
|
|
544
|
+
}
|
|
545
|
+
return {
|
|
546
|
+
valid: true,
|
|
547
|
+
reason: null,
|
|
548
|
+
hash: expected.hash,
|
|
549
|
+
selfHash: expected.selfHash,
|
|
550
|
+
messageCount: messageCount,
|
|
551
|
+
stateVector: data.stateVector || [0, 0, 0, 0, 0, 0]
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Singleton instance
|
|
557
|
+
let instance = null;
|
|
558
|
+
|
|
559
|
+
function getMemoryManager() {
|
|
560
|
+
if (!instance) {
|
|
561
|
+
instance = new LocalMemoryManager();
|
|
562
|
+
}
|
|
563
|
+
return instance;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// CLI interface
|
|
567
|
+
if (require.main === module) {
|
|
568
|
+
const cmd = process.argv[2];
|
|
569
|
+
const memory = getMemoryManager();
|
|
570
|
+
|
|
571
|
+
if (cmd === 'list') {
|
|
572
|
+
const sessions = memory.listSessions();
|
|
573
|
+
console.log('Sessions:');
|
|
574
|
+
for (const s of sessions) {
|
|
575
|
+
console.log(` ${s.id} - ${new Date(s.updatedAt).toLocaleString()} (${s.messageCount} msgs)`);
|
|
576
|
+
}
|
|
577
|
+
} else if (cmd === 'show' && process.argv[3]) {
|
|
578
|
+
const summary = memory.getSessionSummary(process.argv[3]);
|
|
579
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
580
|
+
} else if (cmd === 'export' && process.argv[3]) {
|
|
581
|
+
const data = memory.exportSession(process.argv[3]);
|
|
582
|
+
console.log(JSON.stringify(data, null, 2));
|
|
583
|
+
} else if (cmd === 'verify' && process.argv[3]) {
|
|
584
|
+
const result = memory.verifySession(process.argv[3]);
|
|
585
|
+
console.log(result.valid ? '✓ Valid' : '✗ Invalid', result);
|
|
586
|
+
} else if (cmd === 'cleanup') {
|
|
587
|
+
const days = parseInt(process.argv[3]) || 7;
|
|
588
|
+
const deleted = memory.cleanup(days);
|
|
589
|
+
console.log(`Deleted ${deleted} sessions older than ${days} days`);
|
|
590
|
+
} else if (cmd === 'handoff' && process.argv[3]) {
|
|
591
|
+
const handoff = memory.getHandoff(process.argv[3]);
|
|
592
|
+
console.log(handoff ? JSON.stringify(handoff, null, 2) : 'No handoff state');
|
|
593
|
+
} else {
|
|
594
|
+
console.log('Usage: node local-memory.js <command> [args]');
|
|
595
|
+
console.log('Commands:');
|
|
596
|
+
console.log(' list - List all sessions');
|
|
597
|
+
console.log(' show <session-id> - Show session summary');
|
|
598
|
+
console.log(' export <session-id> - Export full session');
|
|
599
|
+
console.log(' verify <session-id> - Verify session integrity');
|
|
600
|
+
console.log(' cleanup [days] - Delete sessions older than N days (default: 7)');
|
|
601
|
+
console.log(' handoff <session-id> - Show handoff state');
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
module.exports = {
|
|
606
|
+
getMemoryManager,
|
|
607
|
+
LocalMemoryManager,
|
|
608
|
+
Session,
|
|
609
|
+
Message,
|
|
610
|
+
Z6_STATE,
|
|
611
|
+
COMPRESSION_THRESHOLD,
|
|
612
|
+
MAX_MESSAGES
|
|
613
|
+
};
|
package/local-models.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const http = require('http');
|
|
3
|
+
const https = require('https');
|
|
4
|
+
|
|
5
|
+
function base(value = process.env.OLLAMA_URL || 'http://localhost:11434') {
|
|
6
|
+
const url = new URL(value);
|
|
7
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash)
|
|
8
|
+
throw Error('OLLAMA_URL must be an HTTP(S) base URL without credentials, query or fragment.');
|
|
9
|
+
return url.href.replace(/\/+$/, '');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// No subprocess or shell; bound both response bytes and total elapsed time.
|
|
13
|
+
function discover(options = {}) {
|
|
14
|
+
return new Promise(resolve => {
|
|
15
|
+
let target;
|
|
16
|
+
try { target = new URL(base(options.url) + '/api/tags'); }
|
|
17
|
+
catch { resolve({ok: false, models: [], reason: 'invalid-url'}); return; }
|
|
18
|
+
let settled = false, request, timer;
|
|
19
|
+
const finish = result => {
|
|
20
|
+
if (settled) return;
|
|
21
|
+
settled = true;
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
resolve(result);
|
|
24
|
+
};
|
|
25
|
+
const fail = reason => finish({ok: false, models: [], reason});
|
|
26
|
+
timer = setTimeout(() => { fail('timeout'); request?.destroy(); }, options.timeoutMs || 2000);
|
|
27
|
+
try {
|
|
28
|
+
request = (target.protocol === 'https:' ? https : http).get(target, response => {
|
|
29
|
+
if (response.statusCode !== 200) { fail('http-' + response.statusCode); response.destroy(); return; }
|
|
30
|
+
const chunks = []; let size = 0;
|
|
31
|
+
response.on('data', chunk => {
|
|
32
|
+
size += chunk.length;
|
|
33
|
+
if (size > 1024 * 1024) { fail('response-too-large'); response.destroy(); return; }
|
|
34
|
+
chunks.push(chunk);
|
|
35
|
+
});
|
|
36
|
+
response.on('error', () => fail('connection'));
|
|
37
|
+
response.on('end', () => {
|
|
38
|
+
if (settled) return;
|
|
39
|
+
try {
|
|
40
|
+
const data = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
41
|
+
if (!Array.isArray(data.models)) { fail('invalid-response'); return; }
|
|
42
|
+
const models = [...new Set(data.models.map(m => m && (m.name || m.model))
|
|
43
|
+
.filter(name => typeof name === 'string' && name.length <= 200 && !/[\s\x00-\x1f]/.test(name)))].slice(0, 500);
|
|
44
|
+
finish({ok: true, models, reason: models.length ? null : 'no-models'});
|
|
45
|
+
} catch { fail('invalid-response'); }
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
request.on('error', () => fail('connection'));
|
|
49
|
+
} catch { fail('connection'); }
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
module.exports = {base, discover};
|