@sesamespace/hivemind 0.6.0 → 0.7.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/dist/{chunk-TBFM7HVN.js → chunk-2SYP4OUR.js} +3 -3
- package/dist/{chunk-LQ7CYHSQ.js → chunk-DXQ3GROV.js} +940 -288
- package/dist/chunk-DXQ3GROV.js.map +1 -0
- package/dist/{chunk-I5DY3ULS.js → chunk-FIPJ6P5W.js} +2 -2
- package/dist/{chunk-HPWW7KKB.js → chunk-LW7KOSXM.js} +2 -2
- package/dist/{chunk-OTIMWYTH.js → chunk-PN4GE2PM.js} +2 -2
- package/dist/commands/fleet.js +3 -3
- package/dist/commands/start.js +3 -3
- package/dist/commands/watchdog.js +3 -3
- package/dist/index.js +14 -2
- package/dist/main.js +5 -5
- package/dist/start.js +1 -1
- package/package.json +7 -13
- package/dist/chunk-LQ7CYHSQ.js.map +0 -1
- /package/dist/{chunk-TBFM7HVN.js.map → chunk-2SYP4OUR.js.map} +0 -0
- /package/dist/{chunk-I5DY3ULS.js.map → chunk-FIPJ6P5W.js.map} +0 -0
- /package/dist/{chunk-HPWW7KKB.js.map → chunk-LW7KOSXM.js.map} +0 -0
- /package/dist/{chunk-OTIMWYTH.js.map → chunk-PN4GE2PM.js.map} +0 -0
|
@@ -37,27 +37,43 @@ var LLMClient = class {
|
|
|
37
37
|
body.tools = tools;
|
|
38
38
|
body.tool_choice = "auto";
|
|
39
39
|
}
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
40
|
+
const RETRYABLE_CODES = [429, 500, 502, 503, 529];
|
|
41
|
+
const MAX_RETRIES = 3;
|
|
42
|
+
let lastError = null;
|
|
43
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
44
|
+
if (attempt > 0) {
|
|
45
|
+
const delayMs = Math.pow(2, attempt) * 1e3;
|
|
46
|
+
console.log(`[llm] Retry ${attempt}/${MAX_RETRIES} after ${delayMs}ms...`);
|
|
47
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
48
|
+
}
|
|
49
|
+
const resp = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: {
|
|
52
|
+
"Content-Type": "application/json",
|
|
53
|
+
...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
|
|
54
|
+
},
|
|
55
|
+
body: JSON.stringify(body)
|
|
56
|
+
});
|
|
57
|
+
if (!resp.ok) {
|
|
58
|
+
const text = await resp.text();
|
|
59
|
+
lastError = new Error(`LLM request failed: ${resp.status} ${text}`);
|
|
60
|
+
if (RETRYABLE_CODES.includes(resp.status) && attempt < MAX_RETRIES) {
|
|
61
|
+
console.warn(`[llm] Retryable error ${resp.status}: ${text.slice(0, 200)}`);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
throw lastError;
|
|
65
|
+
}
|
|
66
|
+
const data = await resp.json();
|
|
67
|
+
const choice = data.choices[0];
|
|
68
|
+
return {
|
|
69
|
+
content: choice.message.content ?? "",
|
|
70
|
+
model: data.model,
|
|
71
|
+
tool_calls: choice.message.tool_calls,
|
|
72
|
+
finish_reason: choice.finish_reason,
|
|
73
|
+
usage: data.usage
|
|
74
|
+
};
|
|
51
75
|
}
|
|
52
|
-
|
|
53
|
-
const choice = data.choices[0];
|
|
54
|
-
return {
|
|
55
|
-
content: choice.message.content ?? "",
|
|
56
|
-
model: data.model,
|
|
57
|
-
tool_calls: choice.message.tool_calls,
|
|
58
|
-
finish_reason: choice.finish_reason,
|
|
59
|
-
usage: data.usage
|
|
60
|
-
};
|
|
76
|
+
throw lastError ?? new Error("LLM request failed after retries");
|
|
61
77
|
}
|
|
62
78
|
};
|
|
63
79
|
|
|
@@ -472,6 +488,92 @@ var TaskEngine = class {
|
|
|
472
488
|
// packages/runtime/src/prompt.ts
|
|
473
489
|
import { readFileSync, existsSync, readdirSync } from "fs";
|
|
474
490
|
import { resolve } from "path";
|
|
491
|
+
function loadMemoryFiles(workspace, contextName) {
|
|
492
|
+
const result = { global: null, context: null };
|
|
493
|
+
if (!workspace) return result;
|
|
494
|
+
const globalPath = resolve(workspace, "MEMORY.md");
|
|
495
|
+
if (existsSync(globalPath)) {
|
|
496
|
+
try {
|
|
497
|
+
const content = readFileSync(globalPath, "utf-8").trim();
|
|
498
|
+
if (content) result.global = content;
|
|
499
|
+
} catch {
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (contextName !== "global") {
|
|
503
|
+
const contextPath = resolve(workspace, "contexts", contextName, "MEMORY.md");
|
|
504
|
+
if (existsSync(contextPath)) {
|
|
505
|
+
try {
|
|
506
|
+
const content = readFileSync(contextPath, "utf-8").trim();
|
|
507
|
+
if (content) result.context = content;
|
|
508
|
+
} catch {
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return result;
|
|
513
|
+
}
|
|
514
|
+
function discoverSkills(workspace) {
|
|
515
|
+
if (!workspace) return [];
|
|
516
|
+
const skillsDir = resolve(workspace, "skills");
|
|
517
|
+
if (!existsSync(skillsDir)) return [];
|
|
518
|
+
const skills = [];
|
|
519
|
+
try {
|
|
520
|
+
const entries = readdirSync(skillsDir);
|
|
521
|
+
for (const entry of entries) {
|
|
522
|
+
const skillMdPath = resolve(skillsDir, entry, "SKILL.md");
|
|
523
|
+
if (!existsSync(skillMdPath)) continue;
|
|
524
|
+
try {
|
|
525
|
+
const content = readFileSync(skillMdPath, "utf-8");
|
|
526
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
527
|
+
let name = entry;
|
|
528
|
+
let description = "";
|
|
529
|
+
if (fmMatch) {
|
|
530
|
+
const fm = fmMatch[1];
|
|
531
|
+
const nameMatch = fm.match(/^name:\s*(.+)$/m);
|
|
532
|
+
const descMatch = fm.match(/^description:\s*(.+)$/m);
|
|
533
|
+
if (nameMatch) name = nameMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
534
|
+
if (descMatch) description = descMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
535
|
+
}
|
|
536
|
+
skills.push({ name, description, path: skillMdPath });
|
|
537
|
+
} catch {
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
} catch {
|
|
541
|
+
}
|
|
542
|
+
return skills;
|
|
543
|
+
}
|
|
544
|
+
function buildEventsDoc(dataDir) {
|
|
545
|
+
if (!dataDir) return "";
|
|
546
|
+
return `
|
|
547
|
+
## Events (Scheduling)
|
|
548
|
+
You can schedule events by writing JSON files to the events directory.
|
|
549
|
+
|
|
550
|
+
### Event Types
|
|
551
|
+
- **immediate**: Triggers as soon as the file is created
|
|
552
|
+
\`{"type": "immediate", "channelId": "<channel>", "text": "Your message"}\`
|
|
553
|
+
- **one-shot**: Triggers once at a specific time, then auto-deletes
|
|
554
|
+
\`{"type": "one-shot", "channelId": "<channel>", "text": "Reminder text", "at": "2026-03-01T09:00:00-08:00"}\`
|
|
555
|
+
- **periodic**: Triggers on a cron schedule
|
|
556
|
+
\`{"type": "periodic", "channelId": "<channel>", "text": "Check inbox", "schedule": "0 9 * * 1-5", "timezone": "America/Los_Angeles"}\`
|
|
557
|
+
|
|
558
|
+
### Cron Format
|
|
559
|
+
\`minute hour day-of-month month day-of-week\`
|
|
560
|
+
- \`0 9 * * *\` = daily at 9:00
|
|
561
|
+
- \`0 9 * * 1-5\` = weekdays at 9:00
|
|
562
|
+
- \`*/30 * * * *\` = every 30 minutes
|
|
563
|
+
|
|
564
|
+
### Creating Events
|
|
565
|
+
Use the write_file tool to create JSON files in the events directory.
|
|
566
|
+
Use unique filenames (include timestamp or description): \`reminder-dentist-1709312400.json\`
|
|
567
|
+
|
|
568
|
+
### Managing Events
|
|
569
|
+
- List: use shell tool \`ls ~/hivemind/data/events/\`
|
|
570
|
+
- Delete/cancel: use shell tool \`rm ~/hivemind/data/events/filename.json\`
|
|
571
|
+
|
|
572
|
+
### Silent Completion
|
|
573
|
+
For periodic events with nothing to report, respond with exactly \`[SILENT]\` (no other text).
|
|
574
|
+
This prevents spamming the channel.
|
|
575
|
+
`;
|
|
576
|
+
}
|
|
475
577
|
var charterCache = null;
|
|
476
578
|
function loadCharter(path) {
|
|
477
579
|
if (charterCache && charterCache.path === path) return charterCache.content;
|
|
@@ -530,7 +632,19 @@ function loadWorkspaceFiles(dir) {
|
|
|
530
632
|
}
|
|
531
633
|
return files;
|
|
532
634
|
}
|
|
533
|
-
function buildSystemPrompt(
|
|
635
|
+
function buildSystemPrompt(configOrOpts, episodes, contextName = "global", l3Knowledge = []) {
|
|
636
|
+
let config;
|
|
637
|
+
let dataDir;
|
|
638
|
+
if ("config" in configOrOpts) {
|
|
639
|
+
config = configOrOpts.config;
|
|
640
|
+
episodes = configOrOpts.episodes;
|
|
641
|
+
contextName = configOrOpts.contextName ?? "global";
|
|
642
|
+
l3Knowledge = configOrOpts.l3Knowledge ?? [];
|
|
643
|
+
dataDir = configOrOpts.dataDir;
|
|
644
|
+
} else {
|
|
645
|
+
config = configOrOpts;
|
|
646
|
+
}
|
|
647
|
+
const resolvedEpisodes = episodes ?? [];
|
|
534
648
|
let prompt = `You are ${config.name}. ${config.personality}
|
|
535
649
|
`;
|
|
536
650
|
let identityText = prompt;
|
|
@@ -575,6 +689,36 @@ You are currently working in the "${contextName}" project context.
|
|
|
575
689
|
`;
|
|
576
690
|
prompt += contextInfo;
|
|
577
691
|
}
|
|
692
|
+
const memoryFiles = loadMemoryFiles(config.workspace, contextName);
|
|
693
|
+
if (memoryFiles.global || memoryFiles.context) {
|
|
694
|
+
prompt += "\n## Working Memory (agent-managed)\n";
|
|
695
|
+
if (memoryFiles.global) {
|
|
696
|
+
prompt += `
|
|
697
|
+
### Global Memory
|
|
698
|
+
${memoryFiles.global}
|
|
699
|
+
`;
|
|
700
|
+
}
|
|
701
|
+
if (memoryFiles.context) {
|
|
702
|
+
prompt += `
|
|
703
|
+
### ${contextName} Memory
|
|
704
|
+
${memoryFiles.context}
|
|
705
|
+
`;
|
|
706
|
+
}
|
|
707
|
+
prompt += "\nUpdate these files with write_file when you learn something important. They persist across sessions.\n";
|
|
708
|
+
}
|
|
709
|
+
const skills = discoverSkills(config.workspace);
|
|
710
|
+
if (skills.length > 0) {
|
|
711
|
+
prompt += "\n## Available Skills\n\n";
|
|
712
|
+
for (const skill of skills) {
|
|
713
|
+
prompt += `- **${skill.name}**: ${skill.description} \u2192 read \`${skill.path}\` for instructions
|
|
714
|
+
`;
|
|
715
|
+
}
|
|
716
|
+
prompt += "\nTo use a skill, read its SKILL.md with read_file. To create a new skill, write a SKILL.md to workspace/skills/<name>/SKILL.md\n";
|
|
717
|
+
}
|
|
718
|
+
const eventsDoc = buildEventsDoc(dataDir);
|
|
719
|
+
if (eventsDoc) {
|
|
720
|
+
prompt += eventsDoc;
|
|
721
|
+
}
|
|
578
722
|
if (l3Knowledge.length > 0) {
|
|
579
723
|
prompt += "\n## Established Knowledge (learned patterns)\n\n";
|
|
580
724
|
for (const entry of l3Knowledge) {
|
|
@@ -582,9 +726,9 @@ You are currently working in the "${contextName}" project context.
|
|
|
582
726
|
`;
|
|
583
727
|
}
|
|
584
728
|
}
|
|
585
|
-
if (
|
|
729
|
+
if (resolvedEpisodes.length > 0) {
|
|
586
730
|
prompt += "\n## Relevant memories from previous conversations\n\n";
|
|
587
|
-
for (const ep of
|
|
731
|
+
for (const ep of resolvedEpisodes) {
|
|
588
732
|
const timeAgo = formatTimeAgo(ep.timestamp);
|
|
589
733
|
const ctxLabel = ep.context_name !== contextName ? ` [from: ${ep.context_name}]` : "";
|
|
590
734
|
prompt += `[${timeAgo}]${ctxLabel} ${ep.role}: ${ep.content}
|
|
@@ -597,7 +741,7 @@ You are currently working in the "${contextName}" project context.
|
|
|
597
741
|
components: {
|
|
598
742
|
identity: identityText,
|
|
599
743
|
l3Knowledge: l3Knowledge.map((e) => e.content),
|
|
600
|
-
l2Episodes:
|
|
744
|
+
l2Episodes: resolvedEpisodes.map((ep) => ({
|
|
601
745
|
id: ep.id,
|
|
602
746
|
content: ep.content,
|
|
603
747
|
score: ep.score,
|
|
@@ -630,7 +774,260 @@ function formatTimeAgo(timestamp) {
|
|
|
630
774
|
return date.toLocaleDateString();
|
|
631
775
|
}
|
|
632
776
|
|
|
777
|
+
// packages/runtime/src/session.ts
|
|
778
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, appendFileSync } from "fs";
|
|
779
|
+
import { join as join2 } from "path";
|
|
780
|
+
import { randomUUID } from "crypto";
|
|
781
|
+
var SessionStore = class {
|
|
782
|
+
sessionDir;
|
|
783
|
+
constructor(dataDir) {
|
|
784
|
+
this.sessionDir = join2(dataDir, "sessions");
|
|
785
|
+
if (!existsSync2(this.sessionDir)) {
|
|
786
|
+
mkdirSync(this.sessionDir, { recursive: true });
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
filePath(contextName) {
|
|
790
|
+
const safe = contextName.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
791
|
+
return join2(this.sessionDir, `${safe}.jsonl`);
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Append a message to the session JSONL file.
|
|
795
|
+
*/
|
|
796
|
+
append(contextName, message, parentId) {
|
|
797
|
+
const fp = this.filePath(contextName);
|
|
798
|
+
const entry = {
|
|
799
|
+
id: randomUUID(),
|
|
800
|
+
parentId: parentId ?? null,
|
|
801
|
+
role: message.role,
|
|
802
|
+
content: message.content,
|
|
803
|
+
tool_calls: message.tool_calls,
|
|
804
|
+
tool_call_id: message.tool_call_id,
|
|
805
|
+
timestamp: Date.now()
|
|
806
|
+
};
|
|
807
|
+
appendFileSync(fp, JSON.stringify(entry) + "\n");
|
|
808
|
+
return entry.id;
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Load all messages from a context's session file.
|
|
812
|
+
*/
|
|
813
|
+
load(contextName) {
|
|
814
|
+
const fp = this.filePath(contextName);
|
|
815
|
+
if (!existsSync2(fp)) return [];
|
|
816
|
+
const lines = readFileSync2(fp, "utf-8").trim().split("\n").filter(Boolean);
|
|
817
|
+
const messages = [];
|
|
818
|
+
for (const line of lines) {
|
|
819
|
+
try {
|
|
820
|
+
const entry = JSON.parse(line);
|
|
821
|
+
const msg = {
|
|
822
|
+
role: entry.role,
|
|
823
|
+
content: entry.content
|
|
824
|
+
};
|
|
825
|
+
if (entry.tool_calls) msg.tool_calls = entry.tool_calls;
|
|
826
|
+
if (entry.tool_call_id) msg.tool_call_id = entry.tool_call_id;
|
|
827
|
+
messages.push(msg);
|
|
828
|
+
} catch {
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return messages;
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Get the N most recent messages from a context.
|
|
835
|
+
*/
|
|
836
|
+
getRecentMessages(contextName, n) {
|
|
837
|
+
const all = this.load(contextName);
|
|
838
|
+
return all.slice(-n);
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Simple string search across a context's history.
|
|
842
|
+
* Returns matching entries with timestamps.
|
|
843
|
+
*/
|
|
844
|
+
grep(contextName, query, limit = 20) {
|
|
845
|
+
const fp = this.filePath(contextName);
|
|
846
|
+
if (!existsSync2(fp)) return [];
|
|
847
|
+
const lines = readFileSync2(fp, "utf-8").trim().split("\n").filter(Boolean);
|
|
848
|
+
const results = [];
|
|
849
|
+
const lowerQuery = query.toLowerCase();
|
|
850
|
+
for (const line of lines) {
|
|
851
|
+
try {
|
|
852
|
+
const entry = JSON.parse(line);
|
|
853
|
+
if (entry.content && entry.content.toLowerCase().includes(lowerQuery)) {
|
|
854
|
+
results.push({
|
|
855
|
+
role: entry.role,
|
|
856
|
+
content: entry.content,
|
|
857
|
+
timestamp: entry.timestamp
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
} catch {
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
return results.slice(-limit);
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Get total message count for a context.
|
|
867
|
+
*/
|
|
868
|
+
messageCount(contextName) {
|
|
869
|
+
const fp = this.filePath(contextName);
|
|
870
|
+
if (!existsSync2(fp)) return 0;
|
|
871
|
+
const content = readFileSync2(fp, "utf-8").trim();
|
|
872
|
+
if (!content) return 0;
|
|
873
|
+
return content.split("\n").length;
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
function estimateTokens(text) {
|
|
877
|
+
if (!text) return 0;
|
|
878
|
+
return Math.ceil(text.length / 4);
|
|
879
|
+
}
|
|
880
|
+
function estimateMessageTokens(messages) {
|
|
881
|
+
let total = 0;
|
|
882
|
+
for (const msg of messages) {
|
|
883
|
+
if (msg.content) total += estimateTokens(msg.content);
|
|
884
|
+
if (msg.tool_calls) {
|
|
885
|
+
for (const call of msg.tool_calls) {
|
|
886
|
+
total += estimateTokens(call.function.name);
|
|
887
|
+
total += estimateTokens(call.function.arguments);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
total += messages.length * 4;
|
|
892
|
+
return total;
|
|
893
|
+
}
|
|
894
|
+
var MODEL_CONTEXT_WINDOWS = {
|
|
895
|
+
"claude-sonnet-4.5": 2e5,
|
|
896
|
+
"claude-sonnet-4-5": 2e5,
|
|
897
|
+
"anthropic/claude-sonnet-4.5": 2e5,
|
|
898
|
+
"anthropic/claude-sonnet-4-5-20250514": 2e5,
|
|
899
|
+
"claude-opus-4": 2e5,
|
|
900
|
+
"anthropic/claude-opus-4": 2e5,
|
|
901
|
+
"claude-3.5-sonnet": 2e5,
|
|
902
|
+
"gpt-4o": 128e3,
|
|
903
|
+
"gpt-4o-mini": 128e3,
|
|
904
|
+
"gpt-4-turbo": 128e3,
|
|
905
|
+
"gemini-2.5-pro": 1e6,
|
|
906
|
+
"gemini-2.0-flash": 1e6
|
|
907
|
+
};
|
|
908
|
+
function getModelContextWindow(model) {
|
|
909
|
+
if (MODEL_CONTEXT_WINDOWS[model]) return MODEL_CONTEXT_WINDOWS[model];
|
|
910
|
+
const parts = model.split("/");
|
|
911
|
+
const modelName = parts[parts.length - 1];
|
|
912
|
+
if (MODEL_CONTEXT_WINDOWS[modelName]) return MODEL_CONTEXT_WINDOWS[modelName];
|
|
913
|
+
for (const [key, value] of Object.entries(MODEL_CONTEXT_WINDOWS)) {
|
|
914
|
+
if (model.includes(key)) return value;
|
|
915
|
+
}
|
|
916
|
+
return 2e5;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// packages/runtime/src/compaction.ts
|
|
920
|
+
var CompactionManager = class {
|
|
921
|
+
/** Compact when total tokens exceed this fraction of context window */
|
|
922
|
+
COMPACT_THRESHOLD = 0.75;
|
|
923
|
+
/** Keep this many recent turns (turn = user + assistant) */
|
|
924
|
+
KEEP_RECENT_TURNS = 15;
|
|
925
|
+
/** Reserve tokens for response */
|
|
926
|
+
RESERVE_TOKENS = 16384;
|
|
927
|
+
/**
|
|
928
|
+
* Check if compaction is needed.
|
|
929
|
+
*/
|
|
930
|
+
shouldCompact(messages, systemPromptTokens, model) {
|
|
931
|
+
const contextWindow = getModelContextWindow(model);
|
|
932
|
+
const messageTokens = estimateMessageTokens(messages);
|
|
933
|
+
const totalTokens = systemPromptTokens + messageTokens;
|
|
934
|
+
const threshold = (contextWindow - this.RESERVE_TOKENS) * this.COMPACT_THRESHOLD;
|
|
935
|
+
return totalTokens > threshold;
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Perform lossless compaction:
|
|
939
|
+
* 1. Save all old messages to L2 (full preservation)
|
|
940
|
+
* 2. Ask LLM to summarize old messages
|
|
941
|
+
* 3. Return summary + recent messages
|
|
942
|
+
*/
|
|
943
|
+
async compact(messages, contextName, memoryClient, llmClient, model) {
|
|
944
|
+
const tokensBefore = estimateMessageTokens(messages);
|
|
945
|
+
const keepCount = this.KEEP_RECENT_TURNS * 2;
|
|
946
|
+
const splitIndex = Math.max(0, messages.length - keepCount);
|
|
947
|
+
if (splitIndex <= 2) {
|
|
948
|
+
return {
|
|
949
|
+
messages,
|
|
950
|
+
compactedCount: 0,
|
|
951
|
+
tokensBefore,
|
|
952
|
+
tokensAfter: tokensBefore,
|
|
953
|
+
episodesSaved: 0
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
const oldMessages = messages.slice(0, splitIndex);
|
|
957
|
+
const recentMessages = messages.slice(splitIndex);
|
|
958
|
+
let episodesSaved = 0;
|
|
959
|
+
if (memoryClient) {
|
|
960
|
+
for (const msg of oldMessages) {
|
|
961
|
+
if (msg.content && (msg.role === "user" || msg.role === "assistant")) {
|
|
962
|
+
try {
|
|
963
|
+
await memoryClient.storeEpisode({
|
|
964
|
+
context_name: contextName,
|
|
965
|
+
role: msg.role,
|
|
966
|
+
content: `[compacted] ${msg.content}`
|
|
967
|
+
});
|
|
968
|
+
episodesSaved++;
|
|
969
|
+
} catch {
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
console.log(`[compaction] Saved ${episodesSaved} episodes to L2 before compacting`);
|
|
974
|
+
}
|
|
975
|
+
const summaryPrompt = [
|
|
976
|
+
{
|
|
977
|
+
role: "system",
|
|
978
|
+
content: `You are a context compactor. Summarize the following conversation into a concise but complete context summary. Preserve:
|
|
979
|
+
- Key decisions and their reasoning
|
|
980
|
+
- Important facts, names, and technical details mentioned
|
|
981
|
+
- The current state of any ongoing work
|
|
982
|
+
- Any commitments or action items
|
|
983
|
+
- The emotional/social tone if relevant
|
|
984
|
+
|
|
985
|
+
Be concise but don't lose critical information. Write in present tense as a context briefing.`
|
|
986
|
+
},
|
|
987
|
+
{
|
|
988
|
+
role: "user",
|
|
989
|
+
content: `Summarize this conversation:
|
|
990
|
+
|
|
991
|
+
${oldMessages.filter((m) => m.content).map((m) => `[${m.role}]: ${m.content}`).join("\n\n")}`
|
|
992
|
+
}
|
|
993
|
+
];
|
|
994
|
+
let summary;
|
|
995
|
+
try {
|
|
996
|
+
const response = await llmClient.chat(summaryPrompt);
|
|
997
|
+
summary = response.content;
|
|
998
|
+
} catch (err) {
|
|
999
|
+
console.error("[compaction] LLM summary failed, using fallback:", err.message);
|
|
1000
|
+
summary = oldMessages.filter((m) => m.content && m.role !== "system").map((m) => `[${m.role}]: ${m.content.slice(0, 200)}`).join("\n");
|
|
1001
|
+
}
|
|
1002
|
+
const compactedMessages = [
|
|
1003
|
+
{
|
|
1004
|
+
role: "user",
|
|
1005
|
+
content: `[Context from earlier conversation \u2014 ${oldMessages.length} messages compacted]
|
|
1006
|
+
|
|
1007
|
+
${summary}`
|
|
1008
|
+
},
|
|
1009
|
+
{
|
|
1010
|
+
role: "assistant",
|
|
1011
|
+
content: "Understood. I have the context from our earlier conversation. Continuing from where we left off."
|
|
1012
|
+
},
|
|
1013
|
+
...recentMessages
|
|
1014
|
+
];
|
|
1015
|
+
const tokensAfter = estimateMessageTokens(compactedMessages);
|
|
1016
|
+
console.log(
|
|
1017
|
+
`[compaction] Compacted ${oldMessages.length} messages (${tokensBefore} \u2192 ${tokensAfter} tokens, saved ${episodesSaved} episodes to L2)`
|
|
1018
|
+
);
|
|
1019
|
+
return {
|
|
1020
|
+
messages: compactedMessages,
|
|
1021
|
+
compactedCount: oldMessages.length,
|
|
1022
|
+
tokensBefore,
|
|
1023
|
+
tokensAfter,
|
|
1024
|
+
episodesSaved
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
|
|
633
1029
|
// packages/runtime/src/agent.ts
|
|
1030
|
+
import { resolve as resolve2 } from "path";
|
|
634
1031
|
var Agent = class {
|
|
635
1032
|
config;
|
|
636
1033
|
llm;
|
|
@@ -644,14 +1041,34 @@ var Agent = class {
|
|
|
644
1041
|
// Run promotion every N messages
|
|
645
1042
|
MAX_TOOL_ITERATIONS = 25;
|
|
646
1043
|
requestLogger = null;
|
|
1044
|
+
sessionStore;
|
|
1045
|
+
compactionManager;
|
|
1046
|
+
dataDir;
|
|
647
1047
|
constructor(config, contextName = "global") {
|
|
648
1048
|
this.config = config;
|
|
649
1049
|
this.llm = new LLMClient(config.llm);
|
|
650
1050
|
this.memory = new MemoryClient(config.memory);
|
|
651
1051
|
this.contextManager = new ContextManager(this.memory);
|
|
1052
|
+
this.dataDir = resolve2(
|
|
1053
|
+
process.env.HIVEMIND_HOME || resolve2(process.env.HOME || "/root", "hivemind"),
|
|
1054
|
+
"data"
|
|
1055
|
+
);
|
|
1056
|
+
this.sessionStore = new SessionStore(this.dataDir);
|
|
1057
|
+
this.compactionManager = new CompactionManager();
|
|
652
1058
|
if (contextName !== "global") {
|
|
653
1059
|
this.contextManager.switchContext(contextName);
|
|
654
1060
|
}
|
|
1061
|
+
this.restoreFromSession(contextName);
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Restore L1 conversation history from persisted session.
|
|
1065
|
+
*/
|
|
1066
|
+
restoreFromSession(contextName) {
|
|
1067
|
+
const messages = this.sessionStore.getRecentMessages(contextName, 40);
|
|
1068
|
+
if (messages.length > 0) {
|
|
1069
|
+
this.conversationHistories.set(contextName, messages);
|
|
1070
|
+
console.log(`[session] Restored ${messages.length} messages for context "${contextName}"`);
|
|
1071
|
+
}
|
|
655
1072
|
}
|
|
656
1073
|
setToolRegistry(registry) {
|
|
657
1074
|
this.toolRegistry = registry;
|
|
@@ -675,7 +1092,10 @@ var Agent = class {
|
|
|
675
1092
|
}
|
|
676
1093
|
}
|
|
677
1094
|
if (!this.conversationHistories.has(contextName)) {
|
|
678
|
-
this.
|
|
1095
|
+
this.restoreFromSession(contextName);
|
|
1096
|
+
if (!this.conversationHistories.has(contextName)) {
|
|
1097
|
+
this.conversationHistories.set(contextName, []);
|
|
1098
|
+
}
|
|
679
1099
|
}
|
|
680
1100
|
const conversationHistory = this.conversationHistories.get(contextName);
|
|
681
1101
|
const relevantEpisodes = await this.memory.search(userMessage, contextName, this.config.memory.top_k).catch((err) => {
|
|
@@ -692,7 +1112,28 @@ var Agent = class {
|
|
|
692
1112
|
}
|
|
693
1113
|
}
|
|
694
1114
|
const l3Knowledge = await this.memory.getL3Knowledge(contextName).catch(() => []);
|
|
695
|
-
const systemPromptResult = buildSystemPrompt(
|
|
1115
|
+
const systemPromptResult = buildSystemPrompt({
|
|
1116
|
+
config: this.config.agent,
|
|
1117
|
+
episodes: relevantEpisodes,
|
|
1118
|
+
contextName,
|
|
1119
|
+
l3Knowledge,
|
|
1120
|
+
dataDir: this.dataDir
|
|
1121
|
+
});
|
|
1122
|
+
const systemPromptTokens = estimateTokens(systemPromptResult.text);
|
|
1123
|
+
if (this.compactionManager.shouldCompact(conversationHistory, systemPromptTokens, this.config.llm.model)) {
|
|
1124
|
+
console.log("[compaction] Context approaching limit, compacting...");
|
|
1125
|
+
const result = await this.compactionManager.compact(
|
|
1126
|
+
conversationHistory,
|
|
1127
|
+
contextName,
|
|
1128
|
+
this.memory,
|
|
1129
|
+
this.llm,
|
|
1130
|
+
this.config.llm.model
|
|
1131
|
+
);
|
|
1132
|
+
this.conversationHistories.set(contextName, result.messages);
|
|
1133
|
+
const updatedHistory = this.conversationHistories.get(contextName);
|
|
1134
|
+
conversationHistory.length = 0;
|
|
1135
|
+
conversationHistory.push(...updatedHistory);
|
|
1136
|
+
}
|
|
696
1137
|
const messages = buildMessages(systemPromptResult.text, conversationHistory, userMessage);
|
|
697
1138
|
const llmStart = Date.now();
|
|
698
1139
|
const toolDefs = this.toolRegistry?.getDefinitions() ?? [];
|
|
@@ -737,7 +1178,7 @@ var Agent = class {
|
|
|
737
1178
|
},
|
|
738
1179
|
conversationHistory: conversationHistory.map((m) => ({
|
|
739
1180
|
role: m.role,
|
|
740
|
-
content: m.content
|
|
1181
|
+
content: m.content ?? ""
|
|
741
1182
|
})),
|
|
742
1183
|
userMessage,
|
|
743
1184
|
response: {
|
|
@@ -761,6 +1202,12 @@ var Agent = class {
|
|
|
761
1202
|
{ role: "user", content: userMessage },
|
|
762
1203
|
{ role: "assistant", content: response.content }
|
|
763
1204
|
);
|
|
1205
|
+
try {
|
|
1206
|
+
this.sessionStore.append(contextName, { role: "user", content: userMessage });
|
|
1207
|
+
this.sessionStore.append(contextName, { role: "assistant", content: response.content });
|
|
1208
|
+
} catch (err) {
|
|
1209
|
+
console.error("[session] Failed to persist messages:", err.message);
|
|
1210
|
+
}
|
|
764
1211
|
if (conversationHistory.length > 40) {
|
|
765
1212
|
const trimmed = conversationHistory.slice(-40);
|
|
766
1213
|
this.conversationHistories.set(contextName, trimmed);
|
|
@@ -802,192 +1249,6 @@ var Agent = class {
|
|
|
802
1249
|
let stripped = message.replace(/^\[.+?\s+in\s+group\s+chat\]:\s*/, "");
|
|
803
1250
|
stripped = stripped.replace(/^\[.+?\]:\s*/, "");
|
|
804
1251
|
const cmdText = stripped;
|
|
805
|
-
if (/^hm[:\s]status\b/i.test(cmdText)) {
|
|
806
|
-
const memoryOk = await this.memory.healthCheck();
|
|
807
|
-
const contexts = this.contextManager.listContexts();
|
|
808
|
-
let contextStats = "";
|
|
809
|
-
try {
|
|
810
|
-
const daemonContexts = await this.memory.listContexts();
|
|
811
|
-
contextStats = daemonContexts.map((c) => ` - ${c.name}: ${c.episode_count} episodes`).join("\n");
|
|
812
|
-
} catch {
|
|
813
|
-
contextStats = " (unable to query memory daemon)";
|
|
814
|
-
}
|
|
815
|
-
const response = [
|
|
816
|
-
`## Agent Status`,
|
|
817
|
-
``,
|
|
818
|
-
`**Name:** ${this.config.agent.name}`,
|
|
819
|
-
`**Active Context:** ${activeCtx}`,
|
|
820
|
-
`**Memory Daemon:** ${memoryOk ? "\u2705 connected" : "\u274C unreachable"} (${this.config.memory.daemon_url})`,
|
|
821
|
-
`**Model:** ${this.config.llm.model}`,
|
|
822
|
-
`**Temperature:** ${this.config.llm.temperature}`,
|
|
823
|
-
`**Max Tokens:** ${this.config.llm.max_tokens}`,
|
|
824
|
-
`**Memory Top-K:** ${this.config.memory.top_k}`,
|
|
825
|
-
`**Messages Processed:** ${this.messageCount}`,
|
|
826
|
-
`**L1 Contexts in Memory:** ${this.conversationHistories.size}`,
|
|
827
|
-
``,
|
|
828
|
-
`### Contexts`,
|
|
829
|
-
contextStats
|
|
830
|
-
].join("\n");
|
|
831
|
-
return { content: response, model: "system", context: activeCtx };
|
|
832
|
-
}
|
|
833
|
-
if (/^hm[:\s]memory\s+stats\b/i.test(cmdText)) {
|
|
834
|
-
try {
|
|
835
|
-
const contexts = await this.memory.listContexts();
|
|
836
|
-
let totalEpisodes = 0;
|
|
837
|
-
let totalL3 = 0;
|
|
838
|
-
let lines = [];
|
|
839
|
-
for (const ctx of contexts) {
|
|
840
|
-
totalEpisodes += ctx.episode_count;
|
|
841
|
-
let l3Count = 0;
|
|
842
|
-
try {
|
|
843
|
-
const l3 = await this.memory.getL3Knowledge(ctx.name);
|
|
844
|
-
l3Count = l3.length;
|
|
845
|
-
totalL3 += l3Count;
|
|
846
|
-
} catch {
|
|
847
|
-
}
|
|
848
|
-
lines.push(` - **${ctx.name}**: ${ctx.episode_count} L2 episodes, ${l3Count} L3 entries`);
|
|
849
|
-
}
|
|
850
|
-
const response = [
|
|
851
|
-
`## Memory Stats`,
|
|
852
|
-
``,
|
|
853
|
-
`**Total L2 Episodes:** ${totalEpisodes}`,
|
|
854
|
-
`**Total L3 Knowledge:** ${totalL3}`,
|
|
855
|
-
`**Contexts:** ${contexts.length}`,
|
|
856
|
-
``,
|
|
857
|
-
`### Per Context`,
|
|
858
|
-
...lines
|
|
859
|
-
].join("\n");
|
|
860
|
-
return { content: response, model: "system", context: activeCtx };
|
|
861
|
-
} catch (err) {
|
|
862
|
-
return { content: `Memory stats failed: ${err.message}`, model: "system", context: activeCtx };
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
const memSearchMatch = cmdText.match(/^hm[:\s]memory\s+search\s+(.+)/i);
|
|
866
|
-
if (memSearchMatch) {
|
|
867
|
-
const query = memSearchMatch[1].trim();
|
|
868
|
-
try {
|
|
869
|
-
const results = await this.memory.search(query, activeCtx, this.config.memory.top_k);
|
|
870
|
-
if (results.length === 0) {
|
|
871
|
-
return { content: `No memories found for "${query}" in context "${activeCtx}".`, model: "system", context: activeCtx };
|
|
872
|
-
}
|
|
873
|
-
let response = `## Memory Search: "${query}"
|
|
874
|
-
|
|
875
|
-
`;
|
|
876
|
-
for (const ep of results) {
|
|
877
|
-
const ctxLabel = ep.context_name !== activeCtx ? ` [from: ${ep.context_name}]` : "";
|
|
878
|
-
response += `- **[${ep.role}]** (score: ${ep.score.toFixed(3)})${ctxLabel} ${ep.content.slice(0, 300)}${ep.content.length > 300 ? "..." : ""}
|
|
879
|
-
`;
|
|
880
|
-
}
|
|
881
|
-
return { content: response, model: "system", context: activeCtx };
|
|
882
|
-
} catch (err) {
|
|
883
|
-
return { content: `Memory search failed: ${err.message}`, model: "system", context: activeCtx };
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
const l3Match = cmdText.match(/^hm[:\s]memory\s+l3(?:\s+(\S+))?/i);
|
|
887
|
-
if (l3Match) {
|
|
888
|
-
const targetCtx = l3Match[1] || activeCtx;
|
|
889
|
-
try {
|
|
890
|
-
const entries = await this.memory.getL3Knowledge(targetCtx);
|
|
891
|
-
if (entries.length === 0) {
|
|
892
|
-
return { content: `No L3 knowledge in context "${targetCtx}".`, model: "system", context: activeCtx };
|
|
893
|
-
}
|
|
894
|
-
let response = `## L3 Knowledge: ${targetCtx}
|
|
895
|
-
|
|
896
|
-
`;
|
|
897
|
-
for (const entry of entries) {
|
|
898
|
-
response += `- **[${entry.id.slice(0, 8)}]** (density: ${entry.connection_density}, accesses: ${entry.access_count})
|
|
899
|
-
${entry.content}
|
|
900
|
-
`;
|
|
901
|
-
}
|
|
902
|
-
return { content: response, model: "system", context: activeCtx };
|
|
903
|
-
} catch (err) {
|
|
904
|
-
return { content: `L3 query failed: ${err.message}`, model: "system", context: activeCtx };
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
if (/^hm[:\s]config\b/i.test(cmdText)) {
|
|
908
|
-
const response = [
|
|
909
|
-
`## Configuration`,
|
|
910
|
-
``,
|
|
911
|
-
`**Agent:** ${this.config.agent.name}`,
|
|
912
|
-
`**Personality:** ${this.config.agent.personality}`,
|
|
913
|
-
`**Workspace:** ${this.config.agent.workspace || "(none)"}`,
|
|
914
|
-
``,
|
|
915
|
-
`### LLM`,
|
|
916
|
-
`- Model: ${this.config.llm.model}`,
|
|
917
|
-
`- Base URL: ${this.config.llm.base_url}`,
|
|
918
|
-
`- Max Tokens: ${this.config.llm.max_tokens}`,
|
|
919
|
-
`- Temperature: ${this.config.llm.temperature}`,
|
|
920
|
-
``,
|
|
921
|
-
`### Memory`,
|
|
922
|
-
`- Daemon URL: ${this.config.memory.daemon_url}`,
|
|
923
|
-
`- Top-K: ${this.config.memory.top_k}`,
|
|
924
|
-
`- Embedding Model: ${this.config.memory.embedding_model}`,
|
|
925
|
-
``,
|
|
926
|
-
`### Sesame`,
|
|
927
|
-
`- API: ${this.config.sesame.api_url}`,
|
|
928
|
-
`- WebSocket: ${this.config.sesame.ws_url}`,
|
|
929
|
-
`- API Key: ${this.config.sesame.api_key ? "***" + this.config.sesame.api_key.slice(-4) : "(not set)"}`
|
|
930
|
-
].join("\n");
|
|
931
|
-
return { content: response, model: "system", context: activeCtx };
|
|
932
|
-
}
|
|
933
|
-
if (/^hm[:\s]contexts\b/i.test(cmdText)) {
|
|
934
|
-
const localContexts = this.contextManager.listContexts();
|
|
935
|
-
let daemonInfo = "";
|
|
936
|
-
try {
|
|
937
|
-
const daemonContexts = await this.memory.listContexts();
|
|
938
|
-
daemonInfo = "\n### Memory Daemon Contexts\n" + daemonContexts.map(
|
|
939
|
-
(c) => ` - **${c.name}**: ${c.episode_count} episodes (created: ${c.created_at})`
|
|
940
|
-
).join("\n");
|
|
941
|
-
} catch {
|
|
942
|
-
daemonInfo = "\n(Memory daemon unreachable)";
|
|
943
|
-
}
|
|
944
|
-
const localInfo = localContexts.map((c) => {
|
|
945
|
-
const active = c.name === activeCtx ? " \u2190 active" : "";
|
|
946
|
-
const historyLen = this.conversationHistories.get(c.name)?.length || 0;
|
|
947
|
-
return ` - **${c.name}**${active}: ${historyLen / 2} turns in L1`;
|
|
948
|
-
}).join("\n");
|
|
949
|
-
const response = [
|
|
950
|
-
`## Contexts`,
|
|
951
|
-
``,
|
|
952
|
-
`**Active:** ${activeCtx}`,
|
|
953
|
-
``,
|
|
954
|
-
`### Local (L1 Working Memory)`,
|
|
955
|
-
localInfo,
|
|
956
|
-
daemonInfo
|
|
957
|
-
].join("\n");
|
|
958
|
-
return { content: response, model: "system", context: activeCtx };
|
|
959
|
-
}
|
|
960
|
-
if (/^hm[:\s]help\b/i.test(cmdText)) {
|
|
961
|
-
const response = [
|
|
962
|
-
`## Available Commands`,
|
|
963
|
-
``,
|
|
964
|
-
`### Introspection`,
|
|
965
|
-
`- \`hm:status\` \u2014 Agent health, memory status, config summary`,
|
|
966
|
-
`- \`hm:config\` \u2014 Full configuration details`,
|
|
967
|
-
`- \`hm:contexts\` \u2014 List all contexts with L1/L2 info`,
|
|
968
|
-
`- \`hm:memory stats\` \u2014 Episode counts, L3 entries per context`,
|
|
969
|
-
`- \`hm:memory search <query>\` \u2014 Search L2 memories with scores`,
|
|
970
|
-
`- \`hm:memory l3 [context]\` \u2014 Show L3 knowledge entries`,
|
|
971
|
-
`- \`hm:help\` \u2014 This help message`,
|
|
972
|
-
``,
|
|
973
|
-
`### Context Management`,
|
|
974
|
-
`- \`switch to <name>\` \u2014 Switch active context`,
|
|
975
|
-
`- \`create context <name>\` \u2014 Create a new context`,
|
|
976
|
-
`- \`list contexts\` \u2014 List contexts (legacy)`,
|
|
977
|
-
``,
|
|
978
|
-
`### Memory`,
|
|
979
|
-
`- \`search all: <query>\` \u2014 Cross-context search`,
|
|
980
|
-
`- \`share <episode_id> with <context>\` \u2014 Share episode across contexts`,
|
|
981
|
-
``,
|
|
982
|
-
`### Tasks`,
|
|
983
|
-
`- \`task add <title>\` \u2014 Create a task`,
|
|
984
|
-
`- \`task list [status]\` \u2014 List tasks`,
|
|
985
|
-
`- \`task start <id>\` \u2014 Start a task`,
|
|
986
|
-
`- \`task complete <id>\` \u2014 Complete a task`,
|
|
987
|
-
`- \`task next\` \u2014 Pick up next available task`
|
|
988
|
-
].join("\n");
|
|
989
|
-
return { content: response, model: "system", context: activeCtx };
|
|
990
|
-
}
|
|
991
1252
|
const searchAllMatch = cmdText.match(/^(?:search\s+all|cross-context\s+search)[:\s]+(.+)/i);
|
|
992
1253
|
if (searchAllMatch) {
|
|
993
1254
|
const query = searchAllMatch[1].trim();
|
|
@@ -1088,9 +1349,219 @@ var Agent = class {
|
|
|
1088
1349
|
}
|
|
1089
1350
|
};
|
|
1090
1351
|
|
|
1352
|
+
// packages/runtime/src/events.ts
|
|
1353
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync3, unlinkSync, watch } from "fs";
|
|
1354
|
+
import { join as join3 } from "path";
|
|
1355
|
+
function matchCronField(field, value, _max) {
|
|
1356
|
+
if (field === "*") return true;
|
|
1357
|
+
for (const part of field.split(",")) {
|
|
1358
|
+
if (part.includes("-")) {
|
|
1359
|
+
const [lo, hi] = part.split("-").map(Number);
|
|
1360
|
+
if (value >= lo && value <= hi) return true;
|
|
1361
|
+
} else if (part.includes("/")) {
|
|
1362
|
+
const [base, step] = part.split("/");
|
|
1363
|
+
const stepN = Number(step);
|
|
1364
|
+
const baseN = base === "*" ? 0 : Number(base);
|
|
1365
|
+
if ((value - baseN) % stepN === 0 && value >= baseN) return true;
|
|
1366
|
+
} else {
|
|
1367
|
+
if (Number(part) === value) return true;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
return false;
|
|
1371
|
+
}
|
|
1372
|
+
function cronMatches(expr, date) {
|
|
1373
|
+
const parts = expr.trim().split(/\s+/);
|
|
1374
|
+
if (parts.length !== 5) return false;
|
|
1375
|
+
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
|
|
1376
|
+
return matchCronField(minute, date.getMinutes(), 59) && matchCronField(hour, date.getHours(), 23) && matchCronField(dayOfMonth, date.getDate(), 31) && matchCronField(month, date.getMonth() + 1, 12) && matchCronField(dayOfWeek, date.getDay(), 6);
|
|
1377
|
+
}
|
|
1378
|
+
var EventsWatcher = class {
|
|
1379
|
+
eventsDir;
|
|
1380
|
+
handler;
|
|
1381
|
+
watcher = null;
|
|
1382
|
+
cronInterval = null;
|
|
1383
|
+
oneShotTimers = /* @__PURE__ */ new Map();
|
|
1384
|
+
knownFiles = /* @__PURE__ */ new Set();
|
|
1385
|
+
startTime;
|
|
1386
|
+
debounceTimers = /* @__PURE__ */ new Map();
|
|
1387
|
+
constructor(dataDir, handler) {
|
|
1388
|
+
this.eventsDir = join3(dataDir, "events");
|
|
1389
|
+
this.handler = handler;
|
|
1390
|
+
this.startTime = Date.now();
|
|
1391
|
+
}
|
|
1392
|
+
start() {
|
|
1393
|
+
if (!existsSync3(this.eventsDir)) {
|
|
1394
|
+
mkdirSync2(this.eventsDir, { recursive: true });
|
|
1395
|
+
}
|
|
1396
|
+
console.log(`[events] Watching ${this.eventsDir}`);
|
|
1397
|
+
this.scanExisting();
|
|
1398
|
+
this.watcher = watch(this.eventsDir, (_eventType, filename) => {
|
|
1399
|
+
if (!filename || !filename.endsWith(".json")) return;
|
|
1400
|
+
this.debounce(filename, () => this.handleFileChange(filename));
|
|
1401
|
+
});
|
|
1402
|
+
this.cronInterval = setInterval(() => this.checkPeriodicEvents(), 6e4);
|
|
1403
|
+
console.log(`[events] Started, tracking ${this.knownFiles.size} files`);
|
|
1404
|
+
}
|
|
1405
|
+
stop() {
|
|
1406
|
+
if (this.watcher) {
|
|
1407
|
+
this.watcher.close();
|
|
1408
|
+
this.watcher = null;
|
|
1409
|
+
}
|
|
1410
|
+
if (this.cronInterval) {
|
|
1411
|
+
clearInterval(this.cronInterval);
|
|
1412
|
+
this.cronInterval = null;
|
|
1413
|
+
}
|
|
1414
|
+
for (const timer of this.oneShotTimers.values()) {
|
|
1415
|
+
clearTimeout(timer);
|
|
1416
|
+
}
|
|
1417
|
+
this.oneShotTimers.clear();
|
|
1418
|
+
for (const timer of this.debounceTimers.values()) {
|
|
1419
|
+
clearTimeout(timer);
|
|
1420
|
+
}
|
|
1421
|
+
this.debounceTimers.clear();
|
|
1422
|
+
}
|
|
1423
|
+
debounce(filename, fn) {
|
|
1424
|
+
const existing = this.debounceTimers.get(filename);
|
|
1425
|
+
if (existing) clearTimeout(existing);
|
|
1426
|
+
this.debounceTimers.set(
|
|
1427
|
+
filename,
|
|
1428
|
+
setTimeout(() => {
|
|
1429
|
+
this.debounceTimers.delete(filename);
|
|
1430
|
+
fn();
|
|
1431
|
+
}, 200)
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
scanExisting() {
|
|
1435
|
+
try {
|
|
1436
|
+
const files = readdirSync2(this.eventsDir).filter((f) => f.endsWith(".json"));
|
|
1437
|
+
for (const file of files) {
|
|
1438
|
+
this.knownFiles.add(file);
|
|
1439
|
+
this.processEventFile(file);
|
|
1440
|
+
}
|
|
1441
|
+
} catch {
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
handleFileChange(filename) {
|
|
1445
|
+
const filePath = join3(this.eventsDir, filename);
|
|
1446
|
+
if (!existsSync3(filePath)) {
|
|
1447
|
+
this.knownFiles.delete(filename);
|
|
1448
|
+
const timer = this.oneShotTimers.get(filename);
|
|
1449
|
+
if (timer) {
|
|
1450
|
+
clearTimeout(timer);
|
|
1451
|
+
this.oneShotTimers.delete(filename);
|
|
1452
|
+
}
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
if (!this.knownFiles.has(filename)) {
|
|
1456
|
+
this.knownFiles.add(filename);
|
|
1457
|
+
}
|
|
1458
|
+
this.processEventFile(filename);
|
|
1459
|
+
}
|
|
1460
|
+
processEventFile(filename) {
|
|
1461
|
+
const filePath = join3(this.eventsDir, filename);
|
|
1462
|
+
let event;
|
|
1463
|
+
try {
|
|
1464
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
1465
|
+
event = JSON.parse(content);
|
|
1466
|
+
} catch (err) {
|
|
1467
|
+
console.error(`[events] Failed to parse ${filename}:`, err.message);
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
if (!event.type || !event.channelId || !event.text) {
|
|
1471
|
+
console.warn(`[events] Invalid event in ${filename}: missing required fields`);
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
switch (event.type) {
|
|
1475
|
+
case "immediate":
|
|
1476
|
+
this.handleImmediate(filename, event);
|
|
1477
|
+
break;
|
|
1478
|
+
case "one-shot":
|
|
1479
|
+
this.scheduleOneShot(filename, event);
|
|
1480
|
+
break;
|
|
1481
|
+
case "periodic":
|
|
1482
|
+
console.log(`[events] Registered periodic event: ${filename} (${event.schedule})`);
|
|
1483
|
+
break;
|
|
1484
|
+
default:
|
|
1485
|
+
console.warn(`[events] Unknown event type in ${filename}: ${event.type}`);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
async handleImmediate(filename, event) {
|
|
1489
|
+
console.log(`[events] Triggering immediate event: ${filename}`);
|
|
1490
|
+
try {
|
|
1491
|
+
await this.handler(event.channelId, event.text, filename, "immediate");
|
|
1492
|
+
} catch (err) {
|
|
1493
|
+
console.error(`[events] Handler error for ${filename}:`, err.message);
|
|
1494
|
+
}
|
|
1495
|
+
this.deleteEvent(filename);
|
|
1496
|
+
}
|
|
1497
|
+
scheduleOneShot(filename, event) {
|
|
1498
|
+
const targetTime = new Date(event.at).getTime();
|
|
1499
|
+
const now = Date.now();
|
|
1500
|
+
const delay = targetTime - now;
|
|
1501
|
+
if (delay <= 0) {
|
|
1502
|
+
console.log(`[events] One-shot event ${filename} is past due, triggering now`);
|
|
1503
|
+
this.handler(event.channelId, event.text, filename, "one-shot").catch((err) => {
|
|
1504
|
+
console.error(`[events] Handler error for ${filename}:`, err.message);
|
|
1505
|
+
});
|
|
1506
|
+
this.deleteEvent(filename);
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
const existing = this.oneShotTimers.get(filename);
|
|
1510
|
+
if (existing) clearTimeout(existing);
|
|
1511
|
+
console.log(`[events] Scheduled one-shot: ${filename} in ${Math.round(delay / 1e3)}s`);
|
|
1512
|
+
const timer = setTimeout(async () => {
|
|
1513
|
+
this.oneShotTimers.delete(filename);
|
|
1514
|
+
try {
|
|
1515
|
+
await this.handler(event.channelId, event.text, filename, "one-shot");
|
|
1516
|
+
} catch (err) {
|
|
1517
|
+
console.error(`[events] Handler error for ${filename}:`, err.message);
|
|
1518
|
+
}
|
|
1519
|
+
this.deleteEvent(filename);
|
|
1520
|
+
}, delay);
|
|
1521
|
+
this.oneShotTimers.set(filename, timer);
|
|
1522
|
+
}
|
|
1523
|
+
checkPeriodicEvents() {
|
|
1524
|
+
const now = /* @__PURE__ */ new Date();
|
|
1525
|
+
for (const filename of this.knownFiles) {
|
|
1526
|
+
const filePath = join3(this.eventsDir, filename);
|
|
1527
|
+
if (!existsSync3(filePath)) continue;
|
|
1528
|
+
try {
|
|
1529
|
+
const event = JSON.parse(readFileSync3(filePath, "utf-8"));
|
|
1530
|
+
if (event.type !== "periodic") continue;
|
|
1531
|
+
let checkDate = now;
|
|
1532
|
+
if (event.timezone) {
|
|
1533
|
+
try {
|
|
1534
|
+
const tzStr = now.toLocaleString("en-US", { timeZone: event.timezone });
|
|
1535
|
+
checkDate = new Date(tzStr);
|
|
1536
|
+
} catch {
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
if (cronMatches(event.schedule, checkDate)) {
|
|
1540
|
+
console.log(`[events] Triggering periodic event: ${filename}`);
|
|
1541
|
+
this.handler(event.channelId, event.text, filename, "periodic").catch((err) => {
|
|
1542
|
+
console.error(`[events] Handler error for ${filename}:`, err.message);
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
} catch {
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
deleteEvent(filename) {
|
|
1550
|
+
try {
|
|
1551
|
+
const filePath = join3(this.eventsDir, filename);
|
|
1552
|
+
if (existsSync3(filePath)) {
|
|
1553
|
+
unlinkSync(filePath);
|
|
1554
|
+
}
|
|
1555
|
+
this.knownFiles.delete(filename);
|
|
1556
|
+
} catch (err) {
|
|
1557
|
+
console.error(`[events] Failed to delete ${filename}:`, err.message);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
|
|
1091
1562
|
// packages/runtime/src/config.ts
|
|
1092
|
-
import { readFileSync as
|
|
1093
|
-
import { resolve as
|
|
1563
|
+
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
|
|
1564
|
+
import { resolve as resolve3, dirname as dirname2 } from "path";
|
|
1094
1565
|
|
|
1095
1566
|
// node_modules/.pnpm/smol-toml@1.6.0/node_modules/smol-toml/dist/error.js
|
|
1096
1567
|
function getLineColFromPtr(string, ptr) {
|
|
@@ -1814,12 +2285,12 @@ function deepMerge(target, source) {
|
|
|
1814
2285
|
return result;
|
|
1815
2286
|
}
|
|
1816
2287
|
function loadConfig(path) {
|
|
1817
|
-
const raw =
|
|
2288
|
+
const raw = readFileSync4(path, "utf-8");
|
|
1818
2289
|
let parsed = parse(raw);
|
|
1819
|
-
const configDir =
|
|
1820
|
-
const localPath =
|
|
1821
|
-
if (
|
|
1822
|
-
const localRaw =
|
|
2290
|
+
const configDir = dirname2(path);
|
|
2291
|
+
const localPath = resolve3(configDir, "local.toml");
|
|
2292
|
+
if (existsSync4(localPath)) {
|
|
2293
|
+
const localRaw = readFileSync4(localPath, "utf-8");
|
|
1823
2294
|
const localParsed = parse(localRaw);
|
|
1824
2295
|
parsed = deepMerge(parsed, localParsed);
|
|
1825
2296
|
console.log(`[config] Merged overrides from ${localPath}`);
|
|
@@ -1868,20 +2339,20 @@ function loadConfig(path) {
|
|
|
1868
2339
|
parsed.sentinel.stop_flag_file = process.env.SENTINEL_STOP_FLAG_FILE;
|
|
1869
2340
|
}
|
|
1870
2341
|
if (parsed.agent.workspace && !parsed.agent.workspace.startsWith("/")) {
|
|
1871
|
-
const configDir2 =
|
|
1872
|
-
parsed.agent.workspace =
|
|
2342
|
+
const configDir2 = dirname2(path);
|
|
2343
|
+
parsed.agent.workspace = resolve3(configDir2, "..", parsed.agent.workspace);
|
|
1873
2344
|
}
|
|
1874
2345
|
return parsed;
|
|
1875
2346
|
}
|
|
1876
2347
|
|
|
1877
2348
|
// packages/runtime/src/sesame.ts
|
|
1878
|
-
import { readFileSync as
|
|
1879
|
-
import { resolve as
|
|
2349
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
2350
|
+
import { resolve as resolve4, dirname as dirname3 } from "path";
|
|
1880
2351
|
import { fileURLToPath } from "url";
|
|
1881
2352
|
var RUNTIME_VERSION = "unknown";
|
|
1882
2353
|
try {
|
|
1883
|
-
const __dirname2 =
|
|
1884
|
-
const pkg = JSON.parse(
|
|
2354
|
+
const __dirname2 = dirname3(fileURLToPath(import.meta.url));
|
|
2355
|
+
const pkg = JSON.parse(readFileSync5(resolve4(__dirname2, "../package.json"), "utf-8"));
|
|
1885
2356
|
RUNTIME_VERSION = pkg.version ?? "unknown";
|
|
1886
2357
|
} catch {
|
|
1887
2358
|
}
|
|
@@ -1947,12 +2418,7 @@ var SesameClient2 = class {
|
|
|
1947
2418
|
this.sdk.on("message", (event) => {
|
|
1948
2419
|
const msg = event.data || event.message || event;
|
|
1949
2420
|
const senderId = msg.senderId || msg.sender?.id;
|
|
1950
|
-
if (senderId === this.agentId)
|
|
1951
|
-
const content = msg.content?.trim() || "";
|
|
1952
|
-
const isHmCommand = /^hm[:\s]/i.test(content);
|
|
1953
|
-
const isTextCommand = /^(?:list contexts|switch to|create context|search all:|task )/i.test(content);
|
|
1954
|
-
if (!isHmCommand && !isTextCommand) return;
|
|
1955
|
-
}
|
|
2421
|
+
if (senderId === this.agentId) return;
|
|
1956
2422
|
if (!this.messageHandler || !msg.content) return;
|
|
1957
2423
|
const channelInfo = this.channels.get(msg.channelId);
|
|
1958
2424
|
this.messageHandler({
|
|
@@ -2055,24 +2521,24 @@ var HEALTH_PORT = 9484;
|
|
|
2055
2521
|
var HEALTH_PATH = "/health";
|
|
2056
2522
|
|
|
2057
2523
|
// packages/runtime/src/request-logger.ts
|
|
2058
|
-
import { randomUUID } from "crypto";
|
|
2059
|
-
import { mkdirSync, existsSync as
|
|
2060
|
-
import { dirname as
|
|
2524
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2525
|
+
import { mkdirSync as mkdirSync3, existsSync as existsSync5, appendFileSync as appendFileSync2, readFileSync as readFileSync6, writeFileSync } from "fs";
|
|
2526
|
+
import { dirname as dirname4 } from "path";
|
|
2061
2527
|
var RequestLogger = class {
|
|
2062
2528
|
logPath;
|
|
2063
2529
|
maxAgeDays = 7;
|
|
2064
2530
|
constructor(dbPath) {
|
|
2065
2531
|
this.logPath = dbPath.replace(/\.db$/, ".jsonl");
|
|
2066
2532
|
if (this.logPath === dbPath) this.logPath = dbPath + ".jsonl";
|
|
2067
|
-
const dir =
|
|
2068
|
-
if (!
|
|
2533
|
+
const dir = dirname4(this.logPath);
|
|
2534
|
+
if (!existsSync5(dir)) mkdirSync3(dir, { recursive: true });
|
|
2069
2535
|
this.prune();
|
|
2070
2536
|
}
|
|
2071
2537
|
prune() {
|
|
2072
|
-
if (!
|
|
2538
|
+
if (!existsSync5(this.logPath)) return;
|
|
2073
2539
|
const cutoff = new Date(Date.now() - this.maxAgeDays * 24 * 60 * 60 * 1e3).toISOString();
|
|
2074
2540
|
try {
|
|
2075
|
-
const lines =
|
|
2541
|
+
const lines = readFileSync6(this.logPath, "utf-8").split("\n").filter(Boolean);
|
|
2076
2542
|
const kept = [];
|
|
2077
2543
|
let pruned = 0;
|
|
2078
2544
|
for (const line of lines) {
|
|
@@ -2094,7 +2560,7 @@ var RequestLogger = class {
|
|
|
2094
2560
|
}
|
|
2095
2561
|
}
|
|
2096
2562
|
log(entry) {
|
|
2097
|
-
const id =
|
|
2563
|
+
const id = randomUUID2();
|
|
2098
2564
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2099
2565
|
const sysTokens = Math.ceil(entry.systemPromptComponents.fullText.length / 4);
|
|
2100
2566
|
const histTokens = Math.ceil(
|
|
@@ -2124,7 +2590,7 @@ var RequestLogger = class {
|
|
|
2124
2590
|
token_est_user: userTokens,
|
|
2125
2591
|
token_est_total: sysTokens + histTokens + userTokens
|
|
2126
2592
|
};
|
|
2127
|
-
|
|
2593
|
+
appendFileSync2(this.logPath, JSON.stringify(record) + "\n");
|
|
2128
2594
|
return id;
|
|
2129
2595
|
}
|
|
2130
2596
|
getRequests(opts = {}) {
|
|
@@ -2148,9 +2614,9 @@ var RequestLogger = class {
|
|
|
2148
2614
|
close() {
|
|
2149
2615
|
}
|
|
2150
2616
|
readAll() {
|
|
2151
|
-
if (!
|
|
2617
|
+
if (!existsSync5(this.logPath)) return [];
|
|
2152
2618
|
try {
|
|
2153
|
-
const lines =
|
|
2619
|
+
const lines = readFileSync6(this.logPath, "utf-8").split("\n").filter(Boolean);
|
|
2154
2620
|
const entries = [];
|
|
2155
2621
|
for (const line of lines) {
|
|
2156
2622
|
try {
|
|
@@ -2167,17 +2633,17 @@ var RequestLogger = class {
|
|
|
2167
2633
|
|
|
2168
2634
|
// packages/runtime/src/dashboard.ts
|
|
2169
2635
|
import { createServer } from "http";
|
|
2170
|
-
import { readFileSync as
|
|
2171
|
-
import { resolve as
|
|
2636
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
2637
|
+
import { resolve as resolve5, dirname as dirname5 } from "path";
|
|
2172
2638
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2173
|
-
var __dirname =
|
|
2639
|
+
var __dirname = dirname5(fileURLToPath2(import.meta.url));
|
|
2174
2640
|
var DASHBOARD_PORT = 9485;
|
|
2175
2641
|
var spaHtml = null;
|
|
2176
2642
|
function getSpaHtml() {
|
|
2177
2643
|
if (!spaHtml) {
|
|
2178
|
-
for (const dir of [__dirname,
|
|
2644
|
+
for (const dir of [__dirname, resolve5(__dirname, "../src")]) {
|
|
2179
2645
|
try {
|
|
2180
|
-
spaHtml =
|
|
2646
|
+
spaHtml = readFileSync7(resolve5(dir, "dashboard.html"), "utf-8");
|
|
2181
2647
|
break;
|
|
2182
2648
|
} catch {
|
|
2183
2649
|
}
|
|
@@ -2349,7 +2815,7 @@ var ToolRegistry = class {
|
|
|
2349
2815
|
|
|
2350
2816
|
// packages/runtime/src/tools/shell.ts
|
|
2351
2817
|
import { execSync } from "child_process";
|
|
2352
|
-
import { resolve as
|
|
2818
|
+
import { resolve as resolve6 } from "path";
|
|
2353
2819
|
var MAX_OUTPUT = 5e4;
|
|
2354
2820
|
function registerShellTool(registry, workspaceDir) {
|
|
2355
2821
|
registry.register(
|
|
@@ -2376,7 +2842,7 @@ function registerShellTool(registry, workspaceDir) {
|
|
|
2376
2842
|
async (params) => {
|
|
2377
2843
|
const command = params.command;
|
|
2378
2844
|
const timeout = (params.timeout || 30) * 1e3;
|
|
2379
|
-
const cwd = params.workdir ?
|
|
2845
|
+
const cwd = params.workdir ? resolve6(workspaceDir, params.workdir) : workspaceDir;
|
|
2380
2846
|
try {
|
|
2381
2847
|
const output = execSync(command, {
|
|
2382
2848
|
cwd,
|
|
@@ -2403,8 +2869,8 @@ ${output || err.message}`;
|
|
|
2403
2869
|
}
|
|
2404
2870
|
|
|
2405
2871
|
// packages/runtime/src/tools/files.ts
|
|
2406
|
-
import { readFileSync as
|
|
2407
|
-
import { resolve as
|
|
2872
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync2, existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
|
|
2873
|
+
import { resolve as resolve7, dirname as dirname6 } from "path";
|
|
2408
2874
|
var MAX_READ = 1e5;
|
|
2409
2875
|
function registerFileTools(registry, workspaceDir) {
|
|
2410
2876
|
registry.register(
|
|
@@ -2430,11 +2896,11 @@ function registerFileTools(registry, workspaceDir) {
|
|
|
2430
2896
|
},
|
|
2431
2897
|
async (params) => {
|
|
2432
2898
|
const filePath = resolvePath(workspaceDir, params.path);
|
|
2433
|
-
if (!
|
|
2899
|
+
if (!existsSync6(filePath)) {
|
|
2434
2900
|
return `Error: File not found: ${filePath}`;
|
|
2435
2901
|
}
|
|
2436
2902
|
try {
|
|
2437
|
-
let content =
|
|
2903
|
+
let content = readFileSync8(filePath, "utf-8");
|
|
2438
2904
|
if (params.offset || params.limit) {
|
|
2439
2905
|
const lines = content.split("\n");
|
|
2440
2906
|
const start = (params.offset || 1) - 1;
|
|
@@ -2471,8 +2937,8 @@ function registerFileTools(registry, workspaceDir) {
|
|
|
2471
2937
|
async (params) => {
|
|
2472
2938
|
const filePath = resolvePath(workspaceDir, params.path);
|
|
2473
2939
|
try {
|
|
2474
|
-
const dir =
|
|
2475
|
-
if (!
|
|
2940
|
+
const dir = dirname6(filePath);
|
|
2941
|
+
if (!existsSync6(dir)) mkdirSync4(dir, { recursive: true });
|
|
2476
2942
|
writeFileSync2(filePath, params.content);
|
|
2477
2943
|
return `Written ${params.content.length} bytes to ${filePath}`;
|
|
2478
2944
|
} catch (err) {
|
|
@@ -2503,11 +2969,11 @@ function registerFileTools(registry, workspaceDir) {
|
|
|
2503
2969
|
},
|
|
2504
2970
|
async (params) => {
|
|
2505
2971
|
const filePath = resolvePath(workspaceDir, params.path);
|
|
2506
|
-
if (!
|
|
2972
|
+
if (!existsSync6(filePath)) {
|
|
2507
2973
|
return `Error: File not found: ${filePath}`;
|
|
2508
2974
|
}
|
|
2509
2975
|
try {
|
|
2510
|
-
const content =
|
|
2976
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
2511
2977
|
const oldText = params.old_text;
|
|
2512
2978
|
const newText = params.new_text;
|
|
2513
2979
|
if (!content.includes(oldText)) {
|
|
@@ -2535,18 +3001,18 @@ function registerFileTools(registry, workspaceDir) {
|
|
|
2535
3001
|
required: []
|
|
2536
3002
|
},
|
|
2537
3003
|
async (params) => {
|
|
2538
|
-
const { readdirSync:
|
|
3004
|
+
const { readdirSync: readdirSync3, statSync: statSync2 } = await import("fs");
|
|
2539
3005
|
const dirPath = params.path ? resolvePath(workspaceDir, params.path) : workspaceDir;
|
|
2540
|
-
if (!
|
|
3006
|
+
if (!existsSync6(dirPath)) {
|
|
2541
3007
|
return `Error: Directory not found: ${dirPath}`;
|
|
2542
3008
|
}
|
|
2543
3009
|
try {
|
|
2544
|
-
const entries =
|
|
3010
|
+
const entries = readdirSync3(dirPath);
|
|
2545
3011
|
const results = [];
|
|
2546
3012
|
for (const entry of entries) {
|
|
2547
3013
|
if (entry.startsWith(".")) continue;
|
|
2548
3014
|
try {
|
|
2549
|
-
const stat =
|
|
3015
|
+
const stat = statSync2(resolve7(dirPath, entry));
|
|
2550
3016
|
results.push(stat.isDirectory() ? `${entry}/` : entry);
|
|
2551
3017
|
} catch {
|
|
2552
3018
|
results.push(entry);
|
|
@@ -2563,7 +3029,7 @@ function resolvePath(workspace, path) {
|
|
|
2563
3029
|
if (path.startsWith("/") || path.startsWith("~")) {
|
|
2564
3030
|
return path.replace(/^~/, process.env.HOME || "/root");
|
|
2565
3031
|
}
|
|
2566
|
-
return
|
|
3032
|
+
return resolve7(workspace, path);
|
|
2567
3033
|
}
|
|
2568
3034
|
|
|
2569
3035
|
// packages/runtime/src/tools/web.ts
|
|
@@ -2664,32 +3130,184 @@ function registerWebTools(registry, config) {
|
|
|
2664
3130
|
);
|
|
2665
3131
|
}
|
|
2666
3132
|
|
|
3133
|
+
// packages/runtime/src/tools/memory.ts
|
|
3134
|
+
function registerMemoryTools(registry, daemonUrl) {
|
|
3135
|
+
registry.register(
|
|
3136
|
+
"memory_search",
|
|
3137
|
+
"Search your episodic memory for relevant past conversations and experiences. Returns scored results from your memory system. Use this when you want to recall something from a previous conversation or check what you know about a topic.",
|
|
3138
|
+
{
|
|
3139
|
+
type: "object",
|
|
3140
|
+
properties: {
|
|
3141
|
+
query: {
|
|
3142
|
+
type: "string",
|
|
3143
|
+
description: "What to search for in memory"
|
|
3144
|
+
},
|
|
3145
|
+
context: {
|
|
3146
|
+
type: "string",
|
|
3147
|
+
description: "Context to search in (default: current active context)"
|
|
3148
|
+
},
|
|
3149
|
+
top_k: {
|
|
3150
|
+
type: "number",
|
|
3151
|
+
description: "Number of results to return (default: 10)"
|
|
3152
|
+
}
|
|
3153
|
+
},
|
|
3154
|
+
required: ["query"]
|
|
3155
|
+
},
|
|
3156
|
+
async (params) => {
|
|
3157
|
+
const query = params.query;
|
|
3158
|
+
const context = params.context || "global";
|
|
3159
|
+
const topK = params.top_k || 10;
|
|
3160
|
+
try {
|
|
3161
|
+
const resp = await fetch(`${daemonUrl}/api/search`, {
|
|
3162
|
+
method: "POST",
|
|
3163
|
+
headers: { "Content-Type": "application/json" },
|
|
3164
|
+
body: JSON.stringify({ query, context_name: context, top_k: topK })
|
|
3165
|
+
});
|
|
3166
|
+
if (!resp.ok) return `Memory search failed: ${resp.status}`;
|
|
3167
|
+
const data = await resp.json();
|
|
3168
|
+
if (!data.results?.length) return `No memories found for "${query}" in context "${context}".`;
|
|
3169
|
+
return data.results.map((r, i) => `${i + 1}. [score: ${r.score.toFixed(3)}] [${r.context_name}] [${r.role}] ${r.content.slice(0, 200)}`).join("\n");
|
|
3170
|
+
} catch (err) {
|
|
3171
|
+
return `Memory daemon unreachable: ${err.message}`;
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
);
|
|
3175
|
+
registry.register(
|
|
3176
|
+
"memory_stats",
|
|
3177
|
+
"Get statistics about your memory system \u2014 episode counts, contexts, L3 knowledge entries. Use for self-diagnosis or understanding your memory state.",
|
|
3178
|
+
{
|
|
3179
|
+
type: "object",
|
|
3180
|
+
properties: {},
|
|
3181
|
+
required: []
|
|
3182
|
+
},
|
|
3183
|
+
async () => {
|
|
3184
|
+
try {
|
|
3185
|
+
const healthResp = await fetch(`${daemonUrl}/health`);
|
|
3186
|
+
const health = healthResp.ok ? await healthResp.json() : { status: "unreachable" };
|
|
3187
|
+
const ctxResp = await fetch(`${daemonUrl}/api/contexts`);
|
|
3188
|
+
const contexts = ctxResp.ok ? (await ctxResp.json()).contexts : [];
|
|
3189
|
+
const l3Counts = {};
|
|
3190
|
+
for (const ctx of contexts) {
|
|
3191
|
+
try {
|
|
3192
|
+
const l3Resp = await fetch(`${daemonUrl}/api/l3/${encodeURIComponent(ctx.name)}`);
|
|
3193
|
+
if (l3Resp.ok) {
|
|
3194
|
+
const l3Data = await l3Resp.json();
|
|
3195
|
+
l3Counts[ctx.name] = l3Data.entries?.length ?? 0;
|
|
3196
|
+
}
|
|
3197
|
+
} catch {
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
const totalEpisodes = contexts.reduce((sum, c) => sum + c.episode_count, 0);
|
|
3201
|
+
const totalL3 = Object.values(l3Counts).reduce((sum, c) => sum + c, 0);
|
|
3202
|
+
let output = `Memory Daemon: ${health.status || "unknown"}
|
|
3203
|
+
`;
|
|
3204
|
+
output += `Total Episodes (L2): ${totalEpisodes}
|
|
3205
|
+
`;
|
|
3206
|
+
output += `Total Knowledge (L3): ${totalL3}
|
|
3207
|
+
`;
|
|
3208
|
+
output += `
|
|
3209
|
+
Contexts:
|
|
3210
|
+
`;
|
|
3211
|
+
for (const ctx of contexts) {
|
|
3212
|
+
output += ` - ${ctx.name}: ${ctx.episode_count} episodes, ${l3Counts[ctx.name] ?? 0} L3 entries
|
|
3213
|
+
`;
|
|
3214
|
+
}
|
|
3215
|
+
return output;
|
|
3216
|
+
} catch (err) {
|
|
3217
|
+
return `Memory daemon unreachable: ${err.message}`;
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
);
|
|
3221
|
+
registry.register(
|
|
3222
|
+
"memory_l3",
|
|
3223
|
+
"View your promoted L3 knowledge \u2014 high-level patterns, decisions, and insights that have been distilled from your episodic memories. This is your 'long-term wisdom'.",
|
|
3224
|
+
{
|
|
3225
|
+
type: "object",
|
|
3226
|
+
properties: {
|
|
3227
|
+
context: {
|
|
3228
|
+
type: "string",
|
|
3229
|
+
description: "Context to get L3 knowledge for (default: global)"
|
|
3230
|
+
}
|
|
3231
|
+
},
|
|
3232
|
+
required: []
|
|
3233
|
+
},
|
|
3234
|
+
async (params) => {
|
|
3235
|
+
const context = params.context || "global";
|
|
3236
|
+
try {
|
|
3237
|
+
const resp = await fetch(`${daemonUrl}/api/l3/${encodeURIComponent(context)}`);
|
|
3238
|
+
if (!resp.ok) return `L3 query failed: ${resp.status}`;
|
|
3239
|
+
const data = await resp.json();
|
|
3240
|
+
if (!data.entries?.length) return `No L3 knowledge in context "${context}".`;
|
|
3241
|
+
return data.entries.map((e, i) => `${i + 1}. ${e.content} (from ${e.source_episodes} episodes, ${e.created_at})`).join("\n");
|
|
3242
|
+
} catch (err) {
|
|
3243
|
+
return `Memory daemon unreachable: ${err.message}`;
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
);
|
|
3247
|
+
registry.register(
|
|
3248
|
+
"memory_cross_search",
|
|
3249
|
+
"Search across ALL memory contexts at once. Useful when you're not sure which context a memory belongs to.",
|
|
3250
|
+
{
|
|
3251
|
+
type: "object",
|
|
3252
|
+
properties: {
|
|
3253
|
+
query: {
|
|
3254
|
+
type: "string",
|
|
3255
|
+
description: "What to search for across all contexts"
|
|
3256
|
+
},
|
|
3257
|
+
top_k: {
|
|
3258
|
+
type: "number",
|
|
3259
|
+
description: "Number of results (default: 10)"
|
|
3260
|
+
}
|
|
3261
|
+
},
|
|
3262
|
+
required: ["query"]
|
|
3263
|
+
},
|
|
3264
|
+
async (params) => {
|
|
3265
|
+
const query = params.query;
|
|
3266
|
+
const topK = params.top_k || 10;
|
|
3267
|
+
try {
|
|
3268
|
+
const resp = await fetch(`${daemonUrl}/api/search/cross-context`, {
|
|
3269
|
+
method: "POST",
|
|
3270
|
+
headers: { "Content-Type": "application/json" },
|
|
3271
|
+
body: JSON.stringify({ query, top_k: topK })
|
|
3272
|
+
});
|
|
3273
|
+
if (!resp.ok) return `Cross-context search failed: ${resp.status}`;
|
|
3274
|
+
const data = await resp.json();
|
|
3275
|
+
if (!data.results?.length) return `No memories found across any context for "${query}".`;
|
|
3276
|
+
return data.results.map((r, i) => `${i + 1}. [${r.score.toFixed(3)}] [${r.context_name}] ${r.content.slice(0, 200)}`).join("\n");
|
|
3277
|
+
} catch (err) {
|
|
3278
|
+
return `Memory daemon unreachable: ${err.message}`;
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
);
|
|
3282
|
+
}
|
|
3283
|
+
|
|
2667
3284
|
// packages/runtime/src/tools/register.ts
|
|
2668
|
-
import { resolve as
|
|
2669
|
-
import { mkdirSync as
|
|
3285
|
+
import { resolve as resolve8 } from "path";
|
|
3286
|
+
import { mkdirSync as mkdirSync5, existsSync as existsSync7 } from "fs";
|
|
2670
3287
|
function registerAllTools(hivemindHome, config) {
|
|
2671
3288
|
const registry = new ToolRegistry();
|
|
2672
3289
|
if (config?.enabled === false) {
|
|
2673
3290
|
return registry;
|
|
2674
3291
|
}
|
|
2675
|
-
const workspaceDir =
|
|
2676
|
-
if (!
|
|
2677
|
-
|
|
3292
|
+
const workspaceDir = resolve8(hivemindHome, config?.workspace || "workspace");
|
|
3293
|
+
if (!existsSync7(workspaceDir)) {
|
|
3294
|
+
mkdirSync5(workspaceDir, { recursive: true });
|
|
2678
3295
|
}
|
|
2679
3296
|
registerShellTool(registry, workspaceDir);
|
|
2680
3297
|
registerFileTools(registry, workspaceDir);
|
|
2681
3298
|
registerWebTools(registry, { braveApiKey: config?.braveApiKey });
|
|
3299
|
+
registerMemoryTools(registry, config?.memoryDaemonUrl || "http://localhost:3434");
|
|
2682
3300
|
return registry;
|
|
2683
3301
|
}
|
|
2684
3302
|
|
|
2685
3303
|
// packages/runtime/src/pipeline.ts
|
|
2686
|
-
import { readFileSync as
|
|
2687
|
-
import { resolve as
|
|
3304
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2 } from "fs";
|
|
3305
|
+
import { resolve as resolve9, dirname as dirname7 } from "path";
|
|
2688
3306
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
2689
3307
|
var PACKAGE_VERSION = "unknown";
|
|
2690
3308
|
try {
|
|
2691
|
-
const __dirname2 =
|
|
2692
|
-
const pkg = JSON.parse(
|
|
3309
|
+
const __dirname2 = dirname7(fileURLToPath3(import.meta.url));
|
|
3310
|
+
const pkg = JSON.parse(readFileSync9(resolve9(__dirname2, "../package.json"), "utf-8"));
|
|
2693
3311
|
PACKAGE_VERSION = pkg.version ?? "unknown";
|
|
2694
3312
|
} catch {
|
|
2695
3313
|
}
|
|
@@ -2725,7 +3343,7 @@ function writePidFile(path) {
|
|
|
2725
3343
|
}
|
|
2726
3344
|
function cleanupPidFile(path) {
|
|
2727
3345
|
try {
|
|
2728
|
-
|
|
3346
|
+
unlinkSync2(path);
|
|
2729
3347
|
} catch {
|
|
2730
3348
|
}
|
|
2731
3349
|
}
|
|
@@ -2751,27 +3369,50 @@ async function startPipeline(configPath) {
|
|
|
2751
3369
|
memoryConnected = true;
|
|
2752
3370
|
console.log("[hivemind] Memory daemon connected");
|
|
2753
3371
|
}
|
|
2754
|
-
const requestLogger = new RequestLogger(
|
|
3372
|
+
const requestLogger = new RequestLogger(resolve9(dirname7(configPath), "data", "dashboard.db"));
|
|
2755
3373
|
startDashboardServer(requestLogger, config.memory);
|
|
2756
3374
|
const agent = new Agent(config);
|
|
2757
3375
|
agent.setRequestLogger(requestLogger);
|
|
2758
|
-
const hivemindHome = process.env.HIVEMIND_HOME ||
|
|
3376
|
+
const hivemindHome = process.env.HIVEMIND_HOME || resolve9(process.env.HOME || "/root", "hivemind");
|
|
2759
3377
|
const toolRegistry = registerAllTools(hivemindHome, {
|
|
2760
3378
|
enabled: true,
|
|
2761
3379
|
workspace: config.agent.workspace || "workspace",
|
|
2762
|
-
braveApiKey: process.env.BRAVE_API_KEY
|
|
3380
|
+
braveApiKey: process.env.BRAVE_API_KEY,
|
|
3381
|
+
memoryDaemonUrl: config.memory.daemon_url
|
|
2763
3382
|
});
|
|
2764
3383
|
agent.setToolRegistry(toolRegistry);
|
|
2765
3384
|
console.log(`[hivemind] Context manager initialized (active: ${agent.getActiveContext()})`);
|
|
3385
|
+
const dataDir = resolve9(dirname7(configPath), "data");
|
|
2766
3386
|
if (config.sesame.api_key) {
|
|
2767
|
-
await startSesameLoop(config, agent);
|
|
3387
|
+
await startSesameLoop(config, agent, dataDir);
|
|
2768
3388
|
} else {
|
|
2769
3389
|
console.log("[hivemind] No Sesame API key configured \u2014 running in stdin mode");
|
|
2770
3390
|
await startStdinLoop(agent);
|
|
2771
3391
|
}
|
|
2772
3392
|
}
|
|
2773
|
-
async function startSesameLoop(config, agent) {
|
|
3393
|
+
async function startSesameLoop(config, agent, dataDir) {
|
|
2774
3394
|
const sesame = new SesameClient2(config.sesame);
|
|
3395
|
+
let eventsWatcher = null;
|
|
3396
|
+
if (dataDir) {
|
|
3397
|
+
eventsWatcher = new EventsWatcher(dataDir, async (channelId, text, filename, eventType) => {
|
|
3398
|
+
console.log(`[events] Firing ${eventType} event from ${filename}`);
|
|
3399
|
+
const eventMessage = `[EVENT:${filename}:${eventType}] ${text}`;
|
|
3400
|
+
try {
|
|
3401
|
+
const response = await agent.processMessage(eventMessage);
|
|
3402
|
+
if (response.content.trim() === "[SILENT]" || response.content.trim().startsWith("[SILENT]")) {
|
|
3403
|
+
console.log(`[events] Silent response for ${filename}`);
|
|
3404
|
+
return;
|
|
3405
|
+
}
|
|
3406
|
+
if (response.content.trim() === "__SKIP__") return;
|
|
3407
|
+
if (channelId) {
|
|
3408
|
+
await sesame.sendMessage(channelId, response.content);
|
|
3409
|
+
}
|
|
3410
|
+
} catch (err) {
|
|
3411
|
+
console.error(`[events] Error processing event ${filename}:`, err.message);
|
|
3412
|
+
}
|
|
3413
|
+
});
|
|
3414
|
+
eventsWatcher.start();
|
|
3415
|
+
}
|
|
2775
3416
|
let shuttingDown = false;
|
|
2776
3417
|
const shutdown = (signal) => {
|
|
2777
3418
|
if (shuttingDown) return;
|
|
@@ -2836,6 +3477,11 @@ async function startSesameLoop(config, agent) {
|
|
|
2836
3477
|
sesame.updatePresence("online", { emoji: "\u{1F7E2}" });
|
|
2837
3478
|
return;
|
|
2838
3479
|
}
|
|
3480
|
+
if (response.content.trim() === "[SILENT]" || response.content.trim().startsWith("[SILENT]")) {
|
|
3481
|
+
console.log(`[sesame] ${config.agent.name}: silent response`);
|
|
3482
|
+
sesame.updatePresence("online", { emoji: "\u{1F7E2}" });
|
|
3483
|
+
return;
|
|
3484
|
+
}
|
|
2839
3485
|
const ctxPrefix = response.contextSwitched ? `[switched to ${response.context}] ` : "";
|
|
2840
3486
|
let content = response.content;
|
|
2841
3487
|
const prefixPatterns = [
|
|
@@ -2909,8 +3555,8 @@ ${response.content}
|
|
|
2909
3555
|
console.error("Error:", err.message);
|
|
2910
3556
|
}
|
|
2911
3557
|
});
|
|
2912
|
-
return new Promise((
|
|
2913
|
-
rl.on("close",
|
|
3558
|
+
return new Promise((resolve10) => {
|
|
3559
|
+
rl.on("close", resolve10);
|
|
2914
3560
|
});
|
|
2915
3561
|
}
|
|
2916
3562
|
|
|
@@ -2939,20 +3585,20 @@ var WorkerServer = class {
|
|
|
2939
3585
|
}
|
|
2940
3586
|
/** Start listening. */
|
|
2941
3587
|
async start() {
|
|
2942
|
-
return new Promise((
|
|
3588
|
+
return new Promise((resolve10, reject) => {
|
|
2943
3589
|
this.server = createServer3((req, res) => this.handleRequest(req, res));
|
|
2944
3590
|
this.server.on("error", reject);
|
|
2945
|
-
this.server.listen(this.port, () =>
|
|
3591
|
+
this.server.listen(this.port, () => resolve10());
|
|
2946
3592
|
});
|
|
2947
3593
|
}
|
|
2948
3594
|
/** Stop the server. */
|
|
2949
3595
|
async stop() {
|
|
2950
|
-
return new Promise((
|
|
3596
|
+
return new Promise((resolve10) => {
|
|
2951
3597
|
if (!this.server) {
|
|
2952
|
-
|
|
3598
|
+
resolve10();
|
|
2953
3599
|
return;
|
|
2954
3600
|
}
|
|
2955
|
-
this.server.close(() =>
|
|
3601
|
+
this.server.close(() => resolve10());
|
|
2956
3602
|
});
|
|
2957
3603
|
}
|
|
2958
3604
|
getPort() {
|
|
@@ -3075,10 +3721,10 @@ var WorkerServer = class {
|
|
|
3075
3721
|
}
|
|
3076
3722
|
};
|
|
3077
3723
|
function readBody(req) {
|
|
3078
|
-
return new Promise((
|
|
3724
|
+
return new Promise((resolve10, reject) => {
|
|
3079
3725
|
const chunks = [];
|
|
3080
3726
|
req.on("data", (chunk) => chunks.push(chunk));
|
|
3081
|
-
req.on("end", () =>
|
|
3727
|
+
req.on("end", () => resolve10(Buffer.concat(chunks).toString("utf-8")));
|
|
3082
3728
|
req.on("error", reject);
|
|
3083
3729
|
});
|
|
3084
3730
|
}
|
|
@@ -3351,7 +3997,13 @@ export {
|
|
|
3351
3997
|
TaskEngine,
|
|
3352
3998
|
buildSystemPrompt,
|
|
3353
3999
|
buildMessages,
|
|
4000
|
+
SessionStore,
|
|
4001
|
+
estimateTokens,
|
|
4002
|
+
estimateMessageTokens,
|
|
4003
|
+
getModelContextWindow,
|
|
4004
|
+
CompactionManager,
|
|
3354
4005
|
Agent,
|
|
4006
|
+
EventsWatcher,
|
|
3355
4007
|
defaultSentinelConfig,
|
|
3356
4008
|
loadConfig,
|
|
3357
4009
|
SesameClient2 as SesameClient,
|
|
@@ -3405,4 +4057,4 @@ smol-toml/dist/index.js:
|
|
|
3405
4057
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
3406
4058
|
*)
|
|
3407
4059
|
*/
|
|
3408
|
-
//# sourceMappingURL=chunk-
|
|
4060
|
+
//# sourceMappingURL=chunk-DXQ3GROV.js.map
|