@sesamespace/hivemind 0.6.1 → 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-HCA2NYWH.js → chunk-2SYP4OUR.js} +3 -3
- package/dist/{chunk-7YHRRM5B.js → chunk-DXQ3GROV.js} +785 -95
- package/dist/chunk-DXQ3GROV.js.map +1 -0
- package/dist/{chunk-EL4FSJBK.js → chunk-FIPJ6P5W.js} +2 -2
- package/dist/{chunk-F4C7TIEX.js → chunk-LW7KOSXM.js} +2 -2
- package/dist/{chunk-D3P3TJX4.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-7YHRRM5B.js.map +0 -1
- /package/dist/{chunk-HCA2NYWH.js.map → chunk-2SYP4OUR.js.map} +0 -0
- /package/dist/{chunk-EL4FSJBK.js.map → chunk-FIPJ6P5W.js.map} +0 -0
- /package/dist/{chunk-F4C7TIEX.js.map → chunk-LW7KOSXM.js.map} +0 -0
- /package/dist/{chunk-D3P3TJX4.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);
|
|
@@ -902,9 +1349,219 @@ var Agent = class {
|
|
|
902
1349
|
}
|
|
903
1350
|
};
|
|
904
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
|
+
|
|
905
1562
|
// packages/runtime/src/config.ts
|
|
906
|
-
import { readFileSync as
|
|
907
|
-
import { resolve as
|
|
1563
|
+
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
|
|
1564
|
+
import { resolve as resolve3, dirname as dirname2 } from "path";
|
|
908
1565
|
|
|
909
1566
|
// node_modules/.pnpm/smol-toml@1.6.0/node_modules/smol-toml/dist/error.js
|
|
910
1567
|
function getLineColFromPtr(string, ptr) {
|
|
@@ -1628,12 +2285,12 @@ function deepMerge(target, source) {
|
|
|
1628
2285
|
return result;
|
|
1629
2286
|
}
|
|
1630
2287
|
function loadConfig(path) {
|
|
1631
|
-
const raw =
|
|
2288
|
+
const raw = readFileSync4(path, "utf-8");
|
|
1632
2289
|
let parsed = parse(raw);
|
|
1633
|
-
const configDir =
|
|
1634
|
-
const localPath =
|
|
1635
|
-
if (
|
|
1636
|
-
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");
|
|
1637
2294
|
const localParsed = parse(localRaw);
|
|
1638
2295
|
parsed = deepMerge(parsed, localParsed);
|
|
1639
2296
|
console.log(`[config] Merged overrides from ${localPath}`);
|
|
@@ -1682,20 +2339,20 @@ function loadConfig(path) {
|
|
|
1682
2339
|
parsed.sentinel.stop_flag_file = process.env.SENTINEL_STOP_FLAG_FILE;
|
|
1683
2340
|
}
|
|
1684
2341
|
if (parsed.agent.workspace && !parsed.agent.workspace.startsWith("/")) {
|
|
1685
|
-
const configDir2 =
|
|
1686
|
-
parsed.agent.workspace =
|
|
2342
|
+
const configDir2 = dirname2(path);
|
|
2343
|
+
parsed.agent.workspace = resolve3(configDir2, "..", parsed.agent.workspace);
|
|
1687
2344
|
}
|
|
1688
2345
|
return parsed;
|
|
1689
2346
|
}
|
|
1690
2347
|
|
|
1691
2348
|
// packages/runtime/src/sesame.ts
|
|
1692
|
-
import { readFileSync as
|
|
1693
|
-
import { resolve as
|
|
2349
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
2350
|
+
import { resolve as resolve4, dirname as dirname3 } from "path";
|
|
1694
2351
|
import { fileURLToPath } from "url";
|
|
1695
2352
|
var RUNTIME_VERSION = "unknown";
|
|
1696
2353
|
try {
|
|
1697
|
-
const __dirname2 =
|
|
1698
|
-
const pkg = JSON.parse(
|
|
2354
|
+
const __dirname2 = dirname3(fileURLToPath(import.meta.url));
|
|
2355
|
+
const pkg = JSON.parse(readFileSync5(resolve4(__dirname2, "../package.json"), "utf-8"));
|
|
1699
2356
|
RUNTIME_VERSION = pkg.version ?? "unknown";
|
|
1700
2357
|
} catch {
|
|
1701
2358
|
}
|
|
@@ -1864,24 +2521,24 @@ var HEALTH_PORT = 9484;
|
|
|
1864
2521
|
var HEALTH_PATH = "/health";
|
|
1865
2522
|
|
|
1866
2523
|
// packages/runtime/src/request-logger.ts
|
|
1867
|
-
import { randomUUID } from "crypto";
|
|
1868
|
-
import { mkdirSync, existsSync as
|
|
1869
|
-
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";
|
|
1870
2527
|
var RequestLogger = class {
|
|
1871
2528
|
logPath;
|
|
1872
2529
|
maxAgeDays = 7;
|
|
1873
2530
|
constructor(dbPath) {
|
|
1874
2531
|
this.logPath = dbPath.replace(/\.db$/, ".jsonl");
|
|
1875
2532
|
if (this.logPath === dbPath) this.logPath = dbPath + ".jsonl";
|
|
1876
|
-
const dir =
|
|
1877
|
-
if (!
|
|
2533
|
+
const dir = dirname4(this.logPath);
|
|
2534
|
+
if (!existsSync5(dir)) mkdirSync3(dir, { recursive: true });
|
|
1878
2535
|
this.prune();
|
|
1879
2536
|
}
|
|
1880
2537
|
prune() {
|
|
1881
|
-
if (!
|
|
2538
|
+
if (!existsSync5(this.logPath)) return;
|
|
1882
2539
|
const cutoff = new Date(Date.now() - this.maxAgeDays * 24 * 60 * 60 * 1e3).toISOString();
|
|
1883
2540
|
try {
|
|
1884
|
-
const lines =
|
|
2541
|
+
const lines = readFileSync6(this.logPath, "utf-8").split("\n").filter(Boolean);
|
|
1885
2542
|
const kept = [];
|
|
1886
2543
|
let pruned = 0;
|
|
1887
2544
|
for (const line of lines) {
|
|
@@ -1903,7 +2560,7 @@ var RequestLogger = class {
|
|
|
1903
2560
|
}
|
|
1904
2561
|
}
|
|
1905
2562
|
log(entry) {
|
|
1906
|
-
const id =
|
|
2563
|
+
const id = randomUUID2();
|
|
1907
2564
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1908
2565
|
const sysTokens = Math.ceil(entry.systemPromptComponents.fullText.length / 4);
|
|
1909
2566
|
const histTokens = Math.ceil(
|
|
@@ -1933,7 +2590,7 @@ var RequestLogger = class {
|
|
|
1933
2590
|
token_est_user: userTokens,
|
|
1934
2591
|
token_est_total: sysTokens + histTokens + userTokens
|
|
1935
2592
|
};
|
|
1936
|
-
|
|
2593
|
+
appendFileSync2(this.logPath, JSON.stringify(record) + "\n");
|
|
1937
2594
|
return id;
|
|
1938
2595
|
}
|
|
1939
2596
|
getRequests(opts = {}) {
|
|
@@ -1957,9 +2614,9 @@ var RequestLogger = class {
|
|
|
1957
2614
|
close() {
|
|
1958
2615
|
}
|
|
1959
2616
|
readAll() {
|
|
1960
|
-
if (!
|
|
2617
|
+
if (!existsSync5(this.logPath)) return [];
|
|
1961
2618
|
try {
|
|
1962
|
-
const lines =
|
|
2619
|
+
const lines = readFileSync6(this.logPath, "utf-8").split("\n").filter(Boolean);
|
|
1963
2620
|
const entries = [];
|
|
1964
2621
|
for (const line of lines) {
|
|
1965
2622
|
try {
|
|
@@ -1976,17 +2633,17 @@ var RequestLogger = class {
|
|
|
1976
2633
|
|
|
1977
2634
|
// packages/runtime/src/dashboard.ts
|
|
1978
2635
|
import { createServer } from "http";
|
|
1979
|
-
import { readFileSync as
|
|
1980
|
-
import { resolve as
|
|
2636
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
2637
|
+
import { resolve as resolve5, dirname as dirname5 } from "path";
|
|
1981
2638
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1982
|
-
var __dirname =
|
|
2639
|
+
var __dirname = dirname5(fileURLToPath2(import.meta.url));
|
|
1983
2640
|
var DASHBOARD_PORT = 9485;
|
|
1984
2641
|
var spaHtml = null;
|
|
1985
2642
|
function getSpaHtml() {
|
|
1986
2643
|
if (!spaHtml) {
|
|
1987
|
-
for (const dir of [__dirname,
|
|
2644
|
+
for (const dir of [__dirname, resolve5(__dirname, "../src")]) {
|
|
1988
2645
|
try {
|
|
1989
|
-
spaHtml =
|
|
2646
|
+
spaHtml = readFileSync7(resolve5(dir, "dashboard.html"), "utf-8");
|
|
1990
2647
|
break;
|
|
1991
2648
|
} catch {
|
|
1992
2649
|
}
|
|
@@ -2158,7 +2815,7 @@ var ToolRegistry = class {
|
|
|
2158
2815
|
|
|
2159
2816
|
// packages/runtime/src/tools/shell.ts
|
|
2160
2817
|
import { execSync } from "child_process";
|
|
2161
|
-
import { resolve as
|
|
2818
|
+
import { resolve as resolve6 } from "path";
|
|
2162
2819
|
var MAX_OUTPUT = 5e4;
|
|
2163
2820
|
function registerShellTool(registry, workspaceDir) {
|
|
2164
2821
|
registry.register(
|
|
@@ -2185,7 +2842,7 @@ function registerShellTool(registry, workspaceDir) {
|
|
|
2185
2842
|
async (params) => {
|
|
2186
2843
|
const command = params.command;
|
|
2187
2844
|
const timeout = (params.timeout || 30) * 1e3;
|
|
2188
|
-
const cwd = params.workdir ?
|
|
2845
|
+
const cwd = params.workdir ? resolve6(workspaceDir, params.workdir) : workspaceDir;
|
|
2189
2846
|
try {
|
|
2190
2847
|
const output = execSync(command, {
|
|
2191
2848
|
cwd,
|
|
@@ -2212,8 +2869,8 @@ ${output || err.message}`;
|
|
|
2212
2869
|
}
|
|
2213
2870
|
|
|
2214
2871
|
// packages/runtime/src/tools/files.ts
|
|
2215
|
-
import { readFileSync as
|
|
2216
|
-
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";
|
|
2217
2874
|
var MAX_READ = 1e5;
|
|
2218
2875
|
function registerFileTools(registry, workspaceDir) {
|
|
2219
2876
|
registry.register(
|
|
@@ -2239,11 +2896,11 @@ function registerFileTools(registry, workspaceDir) {
|
|
|
2239
2896
|
},
|
|
2240
2897
|
async (params) => {
|
|
2241
2898
|
const filePath = resolvePath(workspaceDir, params.path);
|
|
2242
|
-
if (!
|
|
2899
|
+
if (!existsSync6(filePath)) {
|
|
2243
2900
|
return `Error: File not found: ${filePath}`;
|
|
2244
2901
|
}
|
|
2245
2902
|
try {
|
|
2246
|
-
let content =
|
|
2903
|
+
let content = readFileSync8(filePath, "utf-8");
|
|
2247
2904
|
if (params.offset || params.limit) {
|
|
2248
2905
|
const lines = content.split("\n");
|
|
2249
2906
|
const start = (params.offset || 1) - 1;
|
|
@@ -2280,8 +2937,8 @@ function registerFileTools(registry, workspaceDir) {
|
|
|
2280
2937
|
async (params) => {
|
|
2281
2938
|
const filePath = resolvePath(workspaceDir, params.path);
|
|
2282
2939
|
try {
|
|
2283
|
-
const dir =
|
|
2284
|
-
if (!
|
|
2940
|
+
const dir = dirname6(filePath);
|
|
2941
|
+
if (!existsSync6(dir)) mkdirSync4(dir, { recursive: true });
|
|
2285
2942
|
writeFileSync2(filePath, params.content);
|
|
2286
2943
|
return `Written ${params.content.length} bytes to ${filePath}`;
|
|
2287
2944
|
} catch (err) {
|
|
@@ -2312,11 +2969,11 @@ function registerFileTools(registry, workspaceDir) {
|
|
|
2312
2969
|
},
|
|
2313
2970
|
async (params) => {
|
|
2314
2971
|
const filePath = resolvePath(workspaceDir, params.path);
|
|
2315
|
-
if (!
|
|
2972
|
+
if (!existsSync6(filePath)) {
|
|
2316
2973
|
return `Error: File not found: ${filePath}`;
|
|
2317
2974
|
}
|
|
2318
2975
|
try {
|
|
2319
|
-
const content =
|
|
2976
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
2320
2977
|
const oldText = params.old_text;
|
|
2321
2978
|
const newText = params.new_text;
|
|
2322
2979
|
if (!content.includes(oldText)) {
|
|
@@ -2344,18 +3001,18 @@ function registerFileTools(registry, workspaceDir) {
|
|
|
2344
3001
|
required: []
|
|
2345
3002
|
},
|
|
2346
3003
|
async (params) => {
|
|
2347
|
-
const { readdirSync:
|
|
3004
|
+
const { readdirSync: readdirSync3, statSync: statSync2 } = await import("fs");
|
|
2348
3005
|
const dirPath = params.path ? resolvePath(workspaceDir, params.path) : workspaceDir;
|
|
2349
|
-
if (!
|
|
3006
|
+
if (!existsSync6(dirPath)) {
|
|
2350
3007
|
return `Error: Directory not found: ${dirPath}`;
|
|
2351
3008
|
}
|
|
2352
3009
|
try {
|
|
2353
|
-
const entries =
|
|
3010
|
+
const entries = readdirSync3(dirPath);
|
|
2354
3011
|
const results = [];
|
|
2355
3012
|
for (const entry of entries) {
|
|
2356
3013
|
if (entry.startsWith(".")) continue;
|
|
2357
3014
|
try {
|
|
2358
|
-
const stat =
|
|
3015
|
+
const stat = statSync2(resolve7(dirPath, entry));
|
|
2359
3016
|
results.push(stat.isDirectory() ? `${entry}/` : entry);
|
|
2360
3017
|
} catch {
|
|
2361
3018
|
results.push(entry);
|
|
@@ -2372,7 +3029,7 @@ function resolvePath(workspace, path) {
|
|
|
2372
3029
|
if (path.startsWith("/") || path.startsWith("~")) {
|
|
2373
3030
|
return path.replace(/^~/, process.env.HOME || "/root");
|
|
2374
3031
|
}
|
|
2375
|
-
return
|
|
3032
|
+
return resolve7(workspace, path);
|
|
2376
3033
|
}
|
|
2377
3034
|
|
|
2378
3035
|
// packages/runtime/src/tools/web.ts
|
|
@@ -2625,16 +3282,16 @@ Contexts:
|
|
|
2625
3282
|
}
|
|
2626
3283
|
|
|
2627
3284
|
// packages/runtime/src/tools/register.ts
|
|
2628
|
-
import { resolve as
|
|
2629
|
-
import { mkdirSync as
|
|
3285
|
+
import { resolve as resolve8 } from "path";
|
|
3286
|
+
import { mkdirSync as mkdirSync5, existsSync as existsSync7 } from "fs";
|
|
2630
3287
|
function registerAllTools(hivemindHome, config) {
|
|
2631
3288
|
const registry = new ToolRegistry();
|
|
2632
3289
|
if (config?.enabled === false) {
|
|
2633
3290
|
return registry;
|
|
2634
3291
|
}
|
|
2635
|
-
const workspaceDir =
|
|
2636
|
-
if (!
|
|
2637
|
-
|
|
3292
|
+
const workspaceDir = resolve8(hivemindHome, config?.workspace || "workspace");
|
|
3293
|
+
if (!existsSync7(workspaceDir)) {
|
|
3294
|
+
mkdirSync5(workspaceDir, { recursive: true });
|
|
2638
3295
|
}
|
|
2639
3296
|
registerShellTool(registry, workspaceDir);
|
|
2640
3297
|
registerFileTools(registry, workspaceDir);
|
|
@@ -2644,13 +3301,13 @@ function registerAllTools(hivemindHome, config) {
|
|
|
2644
3301
|
}
|
|
2645
3302
|
|
|
2646
3303
|
// packages/runtime/src/pipeline.ts
|
|
2647
|
-
import { readFileSync as
|
|
2648
|
-
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";
|
|
2649
3306
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
2650
3307
|
var PACKAGE_VERSION = "unknown";
|
|
2651
3308
|
try {
|
|
2652
|
-
const __dirname2 =
|
|
2653
|
-
const pkg = JSON.parse(
|
|
3309
|
+
const __dirname2 = dirname7(fileURLToPath3(import.meta.url));
|
|
3310
|
+
const pkg = JSON.parse(readFileSync9(resolve9(__dirname2, "../package.json"), "utf-8"));
|
|
2654
3311
|
PACKAGE_VERSION = pkg.version ?? "unknown";
|
|
2655
3312
|
} catch {
|
|
2656
3313
|
}
|
|
@@ -2686,7 +3343,7 @@ function writePidFile(path) {
|
|
|
2686
3343
|
}
|
|
2687
3344
|
function cleanupPidFile(path) {
|
|
2688
3345
|
try {
|
|
2689
|
-
|
|
3346
|
+
unlinkSync2(path);
|
|
2690
3347
|
} catch {
|
|
2691
3348
|
}
|
|
2692
3349
|
}
|
|
@@ -2712,11 +3369,11 @@ async function startPipeline(configPath) {
|
|
|
2712
3369
|
memoryConnected = true;
|
|
2713
3370
|
console.log("[hivemind] Memory daemon connected");
|
|
2714
3371
|
}
|
|
2715
|
-
const requestLogger = new RequestLogger(
|
|
3372
|
+
const requestLogger = new RequestLogger(resolve9(dirname7(configPath), "data", "dashboard.db"));
|
|
2716
3373
|
startDashboardServer(requestLogger, config.memory);
|
|
2717
3374
|
const agent = new Agent(config);
|
|
2718
3375
|
agent.setRequestLogger(requestLogger);
|
|
2719
|
-
const hivemindHome = process.env.HIVEMIND_HOME ||
|
|
3376
|
+
const hivemindHome = process.env.HIVEMIND_HOME || resolve9(process.env.HOME || "/root", "hivemind");
|
|
2720
3377
|
const toolRegistry = registerAllTools(hivemindHome, {
|
|
2721
3378
|
enabled: true,
|
|
2722
3379
|
workspace: config.agent.workspace || "workspace",
|
|
@@ -2725,15 +3382,37 @@ async function startPipeline(configPath) {
|
|
|
2725
3382
|
});
|
|
2726
3383
|
agent.setToolRegistry(toolRegistry);
|
|
2727
3384
|
console.log(`[hivemind] Context manager initialized (active: ${agent.getActiveContext()})`);
|
|
3385
|
+
const dataDir = resolve9(dirname7(configPath), "data");
|
|
2728
3386
|
if (config.sesame.api_key) {
|
|
2729
|
-
await startSesameLoop(config, agent);
|
|
3387
|
+
await startSesameLoop(config, agent, dataDir);
|
|
2730
3388
|
} else {
|
|
2731
3389
|
console.log("[hivemind] No Sesame API key configured \u2014 running in stdin mode");
|
|
2732
3390
|
await startStdinLoop(agent);
|
|
2733
3391
|
}
|
|
2734
3392
|
}
|
|
2735
|
-
async function startSesameLoop(config, agent) {
|
|
3393
|
+
async function startSesameLoop(config, agent, dataDir) {
|
|
2736
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
|
+
}
|
|
2737
3416
|
let shuttingDown = false;
|
|
2738
3417
|
const shutdown = (signal) => {
|
|
2739
3418
|
if (shuttingDown) return;
|
|
@@ -2798,6 +3477,11 @@ async function startSesameLoop(config, agent) {
|
|
|
2798
3477
|
sesame.updatePresence("online", { emoji: "\u{1F7E2}" });
|
|
2799
3478
|
return;
|
|
2800
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
|
+
}
|
|
2801
3485
|
const ctxPrefix = response.contextSwitched ? `[switched to ${response.context}] ` : "";
|
|
2802
3486
|
let content = response.content;
|
|
2803
3487
|
const prefixPatterns = [
|
|
@@ -2871,8 +3555,8 @@ ${response.content}
|
|
|
2871
3555
|
console.error("Error:", err.message);
|
|
2872
3556
|
}
|
|
2873
3557
|
});
|
|
2874
|
-
return new Promise((
|
|
2875
|
-
rl.on("close",
|
|
3558
|
+
return new Promise((resolve10) => {
|
|
3559
|
+
rl.on("close", resolve10);
|
|
2876
3560
|
});
|
|
2877
3561
|
}
|
|
2878
3562
|
|
|
@@ -2901,20 +3585,20 @@ var WorkerServer = class {
|
|
|
2901
3585
|
}
|
|
2902
3586
|
/** Start listening. */
|
|
2903
3587
|
async start() {
|
|
2904
|
-
return new Promise((
|
|
3588
|
+
return new Promise((resolve10, reject) => {
|
|
2905
3589
|
this.server = createServer3((req, res) => this.handleRequest(req, res));
|
|
2906
3590
|
this.server.on("error", reject);
|
|
2907
|
-
this.server.listen(this.port, () =>
|
|
3591
|
+
this.server.listen(this.port, () => resolve10());
|
|
2908
3592
|
});
|
|
2909
3593
|
}
|
|
2910
3594
|
/** Stop the server. */
|
|
2911
3595
|
async stop() {
|
|
2912
|
-
return new Promise((
|
|
3596
|
+
return new Promise((resolve10) => {
|
|
2913
3597
|
if (!this.server) {
|
|
2914
|
-
|
|
3598
|
+
resolve10();
|
|
2915
3599
|
return;
|
|
2916
3600
|
}
|
|
2917
|
-
this.server.close(() =>
|
|
3601
|
+
this.server.close(() => resolve10());
|
|
2918
3602
|
});
|
|
2919
3603
|
}
|
|
2920
3604
|
getPort() {
|
|
@@ -3037,10 +3721,10 @@ var WorkerServer = class {
|
|
|
3037
3721
|
}
|
|
3038
3722
|
};
|
|
3039
3723
|
function readBody(req) {
|
|
3040
|
-
return new Promise((
|
|
3724
|
+
return new Promise((resolve10, reject) => {
|
|
3041
3725
|
const chunks = [];
|
|
3042
3726
|
req.on("data", (chunk) => chunks.push(chunk));
|
|
3043
|
-
req.on("end", () =>
|
|
3727
|
+
req.on("end", () => resolve10(Buffer.concat(chunks).toString("utf-8")));
|
|
3044
3728
|
req.on("error", reject);
|
|
3045
3729
|
});
|
|
3046
3730
|
}
|
|
@@ -3313,7 +3997,13 @@ export {
|
|
|
3313
3997
|
TaskEngine,
|
|
3314
3998
|
buildSystemPrompt,
|
|
3315
3999
|
buildMessages,
|
|
4000
|
+
SessionStore,
|
|
4001
|
+
estimateTokens,
|
|
4002
|
+
estimateMessageTokens,
|
|
4003
|
+
getModelContextWindow,
|
|
4004
|
+
CompactionManager,
|
|
3316
4005
|
Agent,
|
|
4006
|
+
EventsWatcher,
|
|
3317
4007
|
defaultSentinelConfig,
|
|
3318
4008
|
loadConfig,
|
|
3319
4009
|
SesameClient2 as SesameClient,
|
|
@@ -3367,4 +4057,4 @@ smol-toml/dist/index.js:
|
|
|
3367
4057
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
3368
4058
|
*)
|
|
3369
4059
|
*/
|
|
3370
|
-
//# sourceMappingURL=chunk-
|
|
4060
|
+
//# sourceMappingURL=chunk-DXQ3GROV.js.map
|