groove-dev 0.27.196 → 0.27.198

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.
Files changed (106) hide show
  1. package/axom-integration/Screenshot_2026-07-25_at_5.25.30_PM.png +0 -0
  2. package/axom-integration/Screenshot_2026-07-25_at_5.33.26_PM.png +0 -0
  3. package/axom-integration/Screenshot_2026-07-25_at_6.11.53_PM.png +0 -0
  4. package/node_modules/@groove-dev/cli/package.json +1 -1
  5. package/node_modules/@groove-dev/daemon/package.json +1 -1
  6. package/node_modules/@groove-dev/daemon/src/api.js +5 -5
  7. package/node_modules/@groove-dev/daemon/src/axom-connector.js +340 -0
  8. package/node_modules/@groove-dev/daemon/src/axom-install.js +140 -0
  9. package/node_modules/@groove-dev/daemon/src/axom-server.js +229 -0
  10. package/node_modules/@groove-dev/daemon/src/chatstore.js +104 -0
  11. package/node_modules/@groove-dev/daemon/src/federation.js +6 -0
  12. package/node_modules/@groove-dev/daemon/src/index.js +14 -2
  13. package/node_modules/@groove-dev/daemon/src/innerchat-docs.js +8 -1
  14. package/node_modules/@groove-dev/daemon/src/innerchat-relay.js +89 -0
  15. package/node_modules/@groove-dev/daemon/src/innerchat.js +261 -1
  16. package/node_modules/@groove-dev/daemon/src/introducer.js +1 -1
  17. package/node_modules/@groove-dev/daemon/src/network-guard.js +56 -0
  18. package/node_modules/@groove-dev/daemon/src/process.js +2 -2
  19. package/node_modules/@groove-dev/daemon/src/providers/axom.js +66 -0
  20. package/node_modules/@groove-dev/daemon/src/providers/index.js +2 -0
  21. package/node_modules/@groove-dev/daemon/src/routes/agents.js +1 -175
  22. package/node_modules/@groove-dev/daemon/src/routes/axom.js +196 -0
  23. package/node_modules/@groove-dev/daemon/src/routes/chat-history.js +34 -0
  24. package/node_modules/@groove-dev/daemon/src/routes/innerchat.js +152 -20
  25. package/node_modules/@groove-dev/daemon/test/axom-connector.test.js +412 -0
  26. package/node_modules/@groove-dev/daemon/test/axom-server.test.js +187 -0
  27. package/node_modules/@groove-dev/daemon/test/chatstore.test.js +86 -0
  28. package/node_modules/@groove-dev/daemon/test/innerchat-relay.test.js +251 -0
  29. package/node_modules/@groove-dev/gui/dist/assets/index-RbtaI6l7.css +1 -0
  30. package/node_modules/@groove-dev/gui/dist/assets/index-b9dKN6cq.js +1074 -0
  31. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  32. package/node_modules/@groove-dev/gui/package.json +1 -1
  33. package/node_modules/@groove-dev/gui/src/App.jsx +2 -2
  34. package/node_modules/@groove-dev/gui/src/app.css +53 -0
  35. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +2 -2
  36. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +3 -3
  37. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +3 -3
  38. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +1 -1
  39. package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +1 -1
  40. package/node_modules/@groove-dev/gui/src/stores/groove.js +24 -163
  41. package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +52 -2
  42. package/node_modules/@groove-dev/gui/src/stores/slices/axom-slice.js +197 -0
  43. package/node_modules/@groove-dev/gui/src/views/axom.jsx +1060 -0
  44. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +1 -1
  45. package/node_modules/@groove-dev/gui/src/views/settings.jsx +178 -64
  46. package/package.json +2 -2
  47. package/packages/cli/package.json +1 -1
  48. package/packages/daemon/package.json +1 -1
  49. package/packages/daemon/src/api.js +5 -5
  50. package/packages/daemon/src/axom-connector.js +340 -0
  51. package/packages/daemon/src/axom-install.js +140 -0
  52. package/packages/daemon/src/axom-server.js +229 -0
  53. package/packages/daemon/src/chatstore.js +104 -0
  54. package/packages/daemon/src/federation.js +6 -0
  55. package/packages/daemon/src/index.js +14 -2
  56. package/packages/daemon/src/innerchat-docs.js +8 -1
  57. package/packages/daemon/src/innerchat-relay.js +89 -0
  58. package/packages/daemon/src/innerchat.js +261 -1
  59. package/packages/daemon/src/introducer.js +1 -1
  60. package/packages/daemon/src/network-guard.js +56 -0
  61. package/packages/daemon/src/process.js +2 -2
  62. package/packages/daemon/src/providers/axom.js +66 -0
  63. package/packages/daemon/src/providers/index.js +2 -0
  64. package/packages/daemon/src/routes/agents.js +1 -175
  65. package/packages/daemon/src/routes/axom.js +196 -0
  66. package/packages/daemon/src/routes/chat-history.js +34 -0
  67. package/packages/daemon/src/routes/innerchat.js +152 -20
  68. package/packages/gui/dist/assets/index-RbtaI6l7.css +1 -0
  69. package/packages/gui/dist/assets/index-b9dKN6cq.js +1074 -0
  70. package/packages/gui/dist/index.html +2 -2
  71. package/packages/gui/package.json +1 -1
  72. package/packages/gui/src/App.jsx +2 -2
  73. package/packages/gui/src/app.css +53 -0
  74. package/packages/gui/src/components/agents/agent-config.jsx +2 -2
  75. package/packages/gui/src/components/agents/spawn-wizard.jsx +3 -3
  76. package/packages/gui/src/components/layout/activity-bar.jsx +3 -3
  77. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +1 -1
  78. package/packages/gui/src/components/layout/command-palette.jsx +1 -1
  79. package/packages/gui/src/stores/groove.js +24 -163
  80. package/packages/gui/src/stores/slices/agents-slice.js +52 -2
  81. package/packages/gui/src/stores/slices/axom-slice.js +197 -0
  82. package/packages/gui/src/views/axom.jsx +1060 -0
  83. package/packages/gui/src/views/marketplace.jsx +1 -1
  84. package/packages/gui/src/views/settings.jsx +178 -64
  85. package/node_modules/@groove-dev/daemon/src/conversations.js +0 -576
  86. package/node_modules/@groove-dev/gui/dist/assets/index-Cat5pJUx.css +0 -1
  87. package/node_modules/@groove-dev/gui/dist/assets/index-L8xSnvHj.js +0 -1070
  88. package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +0 -198
  89. package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +0 -367
  90. package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +0 -527
  91. package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +0 -212
  92. package/node_modules/@groove-dev/gui/src/components/chat/conversation-list.jsx +0 -163
  93. package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +0 -201
  94. package/node_modules/@groove-dev/gui/src/stores/slices/chat-slice.js +0 -227
  95. package/node_modules/@groove-dev/gui/src/views/chat.jsx +0 -6
  96. package/packages/daemon/src/conversations.js +0 -576
  97. package/packages/gui/dist/assets/index-Cat5pJUx.css +0 -1
  98. package/packages/gui/dist/assets/index-L8xSnvHj.js +0 -1070
  99. package/packages/gui/src/components/chat/chat-header.jsx +0 -198
  100. package/packages/gui/src/components/chat/chat-input.jsx +0 -367
  101. package/packages/gui/src/components/chat/chat-messages.jsx +0 -527
  102. package/packages/gui/src/components/chat/chat-view.jsx +0 -212
  103. package/packages/gui/src/components/chat/conversation-list.jsx +0 -163
  104. package/packages/gui/src/components/chat/model-picker.jsx +0 -201
  105. package/packages/gui/src/stores/slices/chat-slice.js +0 -227
  106. package/packages/gui/src/views/chat.jsx +0 -6
@@ -0,0 +1,229 @@
1
+ // GROOVE — Axom Server Manager (per-session local instances)
2
+ // FSL-1.1-Apache-2.0 — see LICENSE
3
+ //
4
+ // Manages local `axom serve` processes per contract §11: one instance per
5
+ // GROOVE session that wants its own Axom, each with its own port and its own
6
+ // SOVEREIGN --data-dir (ledgers never shared — that isolation is the product,
7
+ // not an implementation detail). Same lifecycle family as LlamaServerManager.
8
+ // The serve binary refuses a locked data-dir; we surface that error verbatim
9
+ // rather than retrying around it.
10
+
11
+ import { spawn } from 'child_process';
12
+ import { mkdirSync, statfsSync } from 'fs';
13
+ import { join } from 'path';
14
+ import os from 'os';
15
+ import { AXOM_DEFAULT_PORT } from './axom-connector.js';
16
+
17
+ const START_TIMEOUT_MS = 60000; // model load can be slow on first start
18
+
19
+ // Local-inference floor, learned 2026-07-25: an 8GB Mac loaded the 4GB Q8
20
+ // chassis fine, then llama_decode failed at the Metal working-set ceiling and
21
+ // memory pressure took the whole machine down. Refusing is a feature. The
22
+ // install manifest's min_ram_gb overrides these when present.
23
+ export const AXOM_REQUIREMENTS = {
24
+ minRamGb: 12,
25
+ recommendedRamGb: 16,
26
+ minDiskGb: 6,
27
+ downloadGb: 4.4,
28
+ };
29
+
30
+ export function hardwareReport(dir = os.homedir(), requirements = AXOM_REQUIREMENTS) {
31
+ const totalRamGb = os.totalmem() / 2 ** 30;
32
+ let freeDiskGb = null;
33
+ try {
34
+ const s = statfsSync(dir);
35
+ freeDiskGb = (s.bavail * s.bsize) / 2 ** 30;
36
+ } catch { /* stat unavailable — report null, never guess */ }
37
+ const appleSilicon = process.platform === 'darwin' && process.arch === 'arm64';
38
+ const verdict = totalRamGb < requirements.minRamGb ? 'insufficient'
39
+ : totalRamGb < requirements.recommendedRamGb ? 'marginal' : 'ready';
40
+ return {
41
+ totalRamGb: Math.round(totalRamGb * 10) / 10,
42
+ cpuCores: os.cpus().length,
43
+ arch: process.arch,
44
+ platform: process.platform,
45
+ appleSilicon,
46
+ gpu: appleSilicon ? 'Metal (unified memory)' : null, // CUDA detection: v1
47
+ freeDiskGb: freeDiskGb === null ? null : Math.round(freeDiskGb * 10) / 10,
48
+ diskOk: freeDiskGb === null ? null : freeDiskGb >= requirements.minDiskGb,
49
+ verdict,
50
+ requirements,
51
+ };
52
+ }
53
+
54
+ export class AxomServerManager {
55
+ constructor(daemon, opts = {}) {
56
+ this.daemon = daemon;
57
+ this.command = opts.command || null; // resolved lazily from config
58
+ this.totalRamGbOverride = opts.totalRamGb; // tests inject; prod reads os
59
+ this.instances = new Map(); // id -> {id, proc, port, dataDir, status, startedAt, error}
60
+ }
61
+
62
+ _command() {
63
+ return this.command || this.daemon.config?.axom?.command || 'axom';
64
+ }
65
+
66
+ _modelDir() {
67
+ return this.daemon.config?.axom?.modelDir
68
+ || join(this.daemon.grooveDir, 'axom', 'models');
69
+ }
70
+
71
+ _allocatePort() {
72
+ const used = new Set([...this.instances.values()].map((i) => i.port));
73
+ for (const ep of this.daemon.axom?.endpoints?.values?.() || []) {
74
+ try { used.add(Number(new URL(ep.url).port)); } catch { /* non-numeric */ }
75
+ }
76
+ let port = AXOM_DEFAULT_PORT;
77
+ while (used.has(port)) port += 1;
78
+ return port;
79
+ }
80
+
81
+ list() {
82
+ return [...this.instances.values()].map(({ proc, ...rest }) => rest);
83
+ }
84
+
85
+ // Start a local instance. `id` doubles as the data-dir name, so the same id
86
+ // across restarts resumes the same sovereign memory.
87
+ async start(id = 'default') {
88
+ if (!/^[a-zA-Z0-9_-]{1,40}$/.test(id)) throw new Error('invalid instance id');
89
+ const existing = this.instances.get(id);
90
+ if (existing && existing.status === 'running') return this.list().find((i) => i.id === id);
91
+
92
+ // Hardware floor — refuse under-spec local inference instead of letting
93
+ // it take the host down. Mock mode is weightless and exempt.
94
+ if (!this.daemon.config?.axom?.mock && !this.daemon.config?.axom?.allowUnderspec) {
95
+ const totalRamGb = this.totalRamGbOverride ?? os.totalmem() / 2 ** 30;
96
+ if (totalRamGb < AXOM_REQUIREMENTS.minRamGb) {
97
+ throw new Error(
98
+ `This machine has ${Math.round(totalRamGb)}GB RAM — below Axom's ${AXOM_REQUIREMENTS.minRamGb}GB floor for local inference. `
99
+ + 'Connect to a remote Axom endpoint instead (axom.allowUnderspec overrides at your own risk).',
100
+ );
101
+ }
102
+ }
103
+
104
+ const port = this._allocatePort();
105
+ const dataDir = join(this.daemon.grooveDir, 'axom', 'instances', id);
106
+ mkdirSync(dataDir, { recursive: true });
107
+
108
+ const args = [
109
+ 'serve',
110
+ '--host', '127.0.0.1',
111
+ '--port', String(port),
112
+ '--data-dir', dataDir,
113
+ '--model-dir', this._modelDir(),
114
+ ];
115
+ // config.axom.mock: run the scripted engine (§11 addendum) — the
116
+ // no-weights bring-up mode. /about reports family "mock" so the GUI can
117
+ // never mistake it for the real thing.
118
+ if (this.daemon.config?.axom?.mock) args.push('--mock');
119
+ const instance = {
120
+ id, port, dataDir, status: 'starting', startedAt: Date.now(), error: null, proc: null,
121
+ };
122
+ this.instances.set(id, instance);
123
+ this._broadcast();
124
+
125
+ let proc;
126
+ try {
127
+ proc = spawn(this._command(), args, { stdio: ['ignore', 'pipe', 'pipe'] });
128
+ } catch (err) {
129
+ instance.status = 'error';
130
+ instance.error = err.message;
131
+ this._broadcast();
132
+ throw err;
133
+ }
134
+ instance.proc = proc;
135
+ let stderrTail = '';
136
+ proc.stderr?.on('data', (d) => { stderrTail = (stderrTail + d.toString()).slice(-2000); });
137
+ proc.on('error', (err) => {
138
+ instance.status = 'error';
139
+ instance.error = err.code === 'ENOENT'
140
+ ? `"${this._command()}" not found — install the Axom runtime first`
141
+ : err.message;
142
+ this._broadcast();
143
+ });
144
+ proc.on('exit', (code, signal) => {
145
+ const wasRunning = instance.status === 'running';
146
+ instance.status = instance.status === 'stopping' ? 'stopped' : 'error';
147
+ if (instance.status === 'error') {
148
+ // The lockfile refusal (§11: two instances on one data-dir) lands
149
+ // here — surface the runtime's own words, don't paraphrase.
150
+ instance.error = instance.error || stderrTail.trim().split('\n').pop() || `exited (code ${code}, signal ${signal})`;
151
+ }
152
+ instance.proc = null;
153
+ if (wasRunning) this._deregisterEndpoint(id);
154
+ this._broadcast();
155
+ this.daemon.audit.log('axom.instance.exit', { id, code, signal });
156
+ });
157
+
158
+ await this._waitForAbout(port, instance);
159
+ if (instance.status !== 'error') {
160
+ instance.status = 'running';
161
+ this._registerEndpoint(id, port);
162
+ this.daemon.audit.log('axom.instance.start', { id, port, dataDir });
163
+ }
164
+ this._broadcast();
165
+ if (instance.status === 'error') throw new Error(instance.error || 'instance failed to start');
166
+ return this.list().find((i) => i.id === id);
167
+ }
168
+
169
+ async stop(id) {
170
+ const instance = this.instances.get(id);
171
+ if (!instance) throw new Error(`no instance "${id}"`);
172
+ instance.status = 'stopping';
173
+ this._broadcast();
174
+ this._deregisterEndpoint(id);
175
+ if (instance.proc) {
176
+ instance.proc.kill('SIGTERM');
177
+ await new Promise((resolve) => {
178
+ const t = setTimeout(() => { instance.proc?.kill('SIGKILL'); resolve(); }, 5000);
179
+ instance.proc.once('exit', () => { clearTimeout(t); resolve(); });
180
+ });
181
+ } else {
182
+ instance.status = 'stopped';
183
+ }
184
+ this._broadcast();
185
+ this.daemon.audit.log('axom.instance.stop', { id });
186
+ }
187
+
188
+ async _waitForAbout(port, instance) {
189
+ const deadline = Date.now() + START_TIMEOUT_MS;
190
+ while (Date.now() < deadline) {
191
+ if (instance.status === 'error') return;
192
+ try {
193
+ const res = await fetch(`http://127.0.0.1:${port}/about`, { signal: AbortSignal.timeout(2000) });
194
+ if (res.ok) return;
195
+ } catch { /* not up yet */ }
196
+ await new Promise((r) => setTimeout(r, 500));
197
+ }
198
+ instance.status = 'error';
199
+ instance.error = instance.error || `no /about on port ${port} within ${START_TIMEOUT_MS / 1000}s`;
200
+ instance.proc?.kill('SIGKILL');
201
+ }
202
+
203
+ // Managed instances surface through the same connector as any endpoint —
204
+ // one socket, every tier (contract §5).
205
+ _registerEndpoint(id, port) {
206
+ const name = `local-${id}`;
207
+ const entries = (this.daemon.config.axom?.endpoints || []).filter((e) => e.name !== name);
208
+ entries.push({ name, url: `http://127.0.0.1:${port}`, managed: true });
209
+ this.daemon.config.axom = { ...(this.daemon.config.axom || {}), endpoints: entries };
210
+ this.daemon.axom.configure(entries);
211
+ }
212
+
213
+ _deregisterEndpoint(id) {
214
+ const name = `local-${id}`;
215
+ const entries = (this.daemon.config.axom?.endpoints || []).filter((e) => e.name !== name);
216
+ this.daemon.config.axom = { ...(this.daemon.config.axom || {}), endpoints: entries };
217
+ this.daemon.axom.configure(entries);
218
+ }
219
+
220
+ _broadcast() {
221
+ this.daemon.broadcast({ type: 'axom:instances', data: this.list() });
222
+ }
223
+
224
+ async destroy() {
225
+ for (const id of [...this.instances.keys()]) {
226
+ try { await this.stop(id); } catch { /* shutting down */ }
227
+ }
228
+ }
229
+ }
@@ -0,0 +1,104 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ import { resolve } from 'path';
4
+ import { existsSync, readFileSync, writeFileSync, renameSync } from 'fs';
5
+
6
+ // Per-agent history cap and attachment handling mirror the GUI's own limits so
7
+ // the two stay in step.
8
+ const MAX_PER_AGENT = 200;
9
+ const SAVE_DEBOUNCE_MS = 1500;
10
+
11
+ /**
12
+ * Server-side chat history.
13
+ *
14
+ * The GUI used to keep chat history only in the browser's localStorage, which
15
+ * is scoped per origin (scheme://host:PORT). For a remote GUI reached over an
16
+ * SSH tunnel the local port changes across reconnects, so the origin — and thus
17
+ * the entire chat store — changed out from under the user, stranding history on
18
+ * old ports. Keeping it on the daemon makes it independent of port, origin, and
19
+ * even which machine connects: reconnect from anywhere and the chats are there.
20
+ */
21
+ export class ChatStore {
22
+ constructor(daemon) {
23
+ this.daemon = daemon;
24
+ this.path = resolve(daemon.grooveDir, 'chat-history.json');
25
+ this.history = this._load();
26
+ this._saveTimer = null;
27
+ }
28
+
29
+ _load() {
30
+ try {
31
+ if (existsSync(this.path)) {
32
+ const data = JSON.parse(readFileSync(this.path, 'utf8'));
33
+ if (data && typeof data === 'object') return data;
34
+ }
35
+ } catch { /* corrupt or missing — start empty rather than throw */ }
36
+ return {};
37
+ }
38
+
39
+ _scheduleSave() {
40
+ if (this._saveTimer) return;
41
+ this._saveTimer = setTimeout(() => {
42
+ this._saveTimer = null;
43
+ this._saveNow();
44
+ }, SAVE_DEBOUNCE_MS);
45
+ if (this._saveTimer.unref) this._saveTimer.unref();
46
+ }
47
+
48
+ _saveNow() {
49
+ try {
50
+ const tmp = `${this.path}.tmp`;
51
+ writeFileSync(tmp, JSON.stringify(this.history), { mode: 0o600 });
52
+ renameSync(tmp, this.path); // atomic — a reader never sees a half-written file
53
+ } catch { /* best effort */ }
54
+ }
55
+
56
+ // Strip base64 attachment payloads — metadata only. Keeping data URLs would
57
+ // bloat the file the same way it bloated localStorage.
58
+ _clean(message) {
59
+ if (!message || typeof message !== 'object') return null;
60
+ const out = { ...message };
61
+ if (Array.isArray(out.attachments) && out.attachments.length) {
62
+ out.attachments = out.attachments.map(({ dataUrl, ...rest }) => rest);
63
+ }
64
+ return out;
65
+ }
66
+
67
+ append(agentId, message) {
68
+ if (!agentId) return;
69
+ const clean = this._clean(message);
70
+ if (!clean) return;
71
+ const arr = this.history[agentId] || [];
72
+ arr.push(clean);
73
+ this.history[agentId] = arr.slice(-MAX_PER_AGENT);
74
+ this._scheduleSave();
75
+ }
76
+
77
+ // Replace an agent's whole history — used when the GUI syncs a batch (e.g.
78
+ // messages it recorded while briefly disconnected).
79
+ replace(agentId, messages) {
80
+ if (!agentId || !Array.isArray(messages)) return;
81
+ this.history[agentId] = messages.map((m) => this._clean(m)).filter(Boolean).slice(-MAX_PER_AGENT);
82
+ this._scheduleSave();
83
+ }
84
+
85
+ getAll() {
86
+ return this.history;
87
+ }
88
+
89
+ get(agentId) {
90
+ return this.history[agentId] || [];
91
+ }
92
+
93
+ remove(agentId) {
94
+ if (this.history[agentId]) {
95
+ delete this.history[agentId];
96
+ this._scheduleSave();
97
+ }
98
+ }
99
+
100
+ stop() {
101
+ if (this._saveTimer) { clearTimeout(this._saveTimer); this._saveTimer = null; }
102
+ this._saveNow();
103
+ }
104
+ }
@@ -355,6 +355,12 @@ export class Federation {
355
355
  };
356
356
  }
357
357
 
358
+ // Public accessor for this daemon's stable id (used by cross-daemon InnerChat
359
+ // relay to identify and verify the sender). Same value as _daemonId().
360
+ getDaemonId() {
361
+ return this._daemonId();
362
+ }
363
+
358
364
  _daemonId() {
359
365
  // Stable ID derived from keypair — fingerprint of public key
360
366
  const idPath = resolve(this.fedDir, 'daemon.id');
@@ -44,15 +44,19 @@ import { ModelLab } from './model-lab.js';
44
44
  import { LlamaServerManager } from './llama-server.js';
45
45
  import { MLXServerManager } from './mlx-server.js';
46
46
  import { RepoImporter } from './repo-import.js';
47
- import { ConversationManager } from './conversations.js';
48
47
  import { Toys } from './toys.js';
49
48
  import { InnerChat } from './innerchat.js';
49
+ import { ChatStore } from './chatstore.js';
50
50
  import { Watcher } from './watcher.js';
51
51
  import { AutoState } from './autostate.js';
52
52
  import { Orchestrator } from './orchestrator.js';
53
53
  import { TrajectoryCapture, ConsentManager } from '../../../moe-training/client/index.js';
54
54
  import { isFirstRun, runFirstTimeSetup, loadConfig, saveConfig, printWelcome } from './firstrun.js';
55
55
  import { bindDaemon as bindGrooveNetworkDaemon } from './providers/groove-network.js';
56
+ import { bindDaemon as bindAxomDaemon } from './providers/axom.js';
57
+ import { AxomConnector } from './axom-connector.js';
58
+ import { AxomServerManager } from './axom-server.js';
59
+ import { AxomInstaller } from './axom-install.js';
56
60
  import { setProviderPaths } from './providers/index.js';
57
61
 
58
62
  const DEFAULT_PORT = 31415;
@@ -139,7 +143,6 @@ export class Daemon {
139
143
  this.rotator = new Rotator(this);
140
144
  this.adaptive = new AdaptiveThresholds(this.grooveDir);
141
145
  this.teams = new Teams(this);
142
- this.conversations = new ConversationManager(this);
143
146
  this.credentials = new CredentialStore(this.grooveDir);
144
147
  this.classifier = new TaskClassifier();
145
148
  this.router = new ModelRouter(this);
@@ -163,9 +166,13 @@ export class Daemon {
163
166
  this.modelLab = new ModelLab(this);
164
167
  this.toys = new Toys(this);
165
168
  this.innerchat = new InnerChat(this);
169
+ this.chatStore = new ChatStore(this);
166
170
  this.watcher = new Watcher(this);
167
171
  this.autoState = new AutoState(this.grooveDir);
168
172
  this.orchestrator = new Orchestrator(this);
173
+ this.axom = new AxomConnector(this);
174
+ this.axomServer = new AxomServerManager(this);
175
+ this.axomInstaller = new AxomInstaller(this);
169
176
  this.trajectoryCapture = null;
170
177
 
171
178
  // Hook teams.delete to clean up agent-loop session files
@@ -202,6 +209,7 @@ export class Daemon {
202
209
  // Give the groove-network provider a handle to the daemon so it can read
203
210
  // networkBeta config at spawn time without a circular import.
204
211
  bindGrooveNetworkDaemon(this);
212
+ bindAxomDaemon(this);
205
213
 
206
214
  // HTTP + WebSocket server
207
215
  this.app = express();
@@ -626,6 +634,7 @@ export class Daemon {
626
634
  this.orchestrator.start();
627
635
  this.timeline.start();
628
636
  this.gateways.start();
637
+ this.axom.start();
629
638
  this.federation.initialize();
630
639
  this._startGarbageCollector();
631
640
 
@@ -874,8 +883,11 @@ export class Daemon {
874
883
  this.scheduler.stop();
875
884
  this.watcher.stop();
876
885
  this.innerchat.stop();
886
+ this.chatStore.stop();
877
887
  this.orchestrator.stop();
878
888
  this.timeline.stop();
889
+ await this.axomServer.destroy();
890
+ this.axom.destroy();
879
891
  if (this._gcInterval) clearInterval(this._gcInterval);
880
892
  if (this._stateSaveInterval) clearInterval(this._stateSaveInterval);
881
893
  if (this._classifierInterval) clearInterval(this._classifierInterval);
@@ -9,7 +9,13 @@
9
9
  //
10
10
  // No imports on purpose — process.js and introducer.js both pull from here.
11
11
 
12
- export function innerChatInstructions(port = 31415, agentName = 'YOUR_NAME') {
12
+ export function innerChatInstructions(port = 31415, agentName = 'YOUR_NAME', peers = []) {
13
+ const aliases = Array.isArray(peers) ? peers.map((p) => p.alias).filter(Boolean) : [];
14
+ const peerRule = aliases.length
15
+ ? `- Agents on peer machines are addressed as \`name@peer\` (configured peers: ${aliases.join(', ')}). `
16
+ + 'A plain name is always local; `name@peer` routes the same ask/tell to that daemon and relays the reply back.'
17
+ : '- To reach an agent on another machine, address it as `name@peer` (a peer must be configured first with '
18
+ + '`POST /api/innerchat/peers`); a plain name is always local.';
13
19
  return [
14
20
  '## Consulting Other Agents (InnerChat)',
15
21
  '',
@@ -56,6 +62,7 @@ export function innerChatInstructions(port = 31415, agentName = 'YOUR_NAME') {
56
62
  '- Only start a conversation when the user asks you to. Never consult other agents on your own initiative.',
57
63
  `- \`from\` must be your own agent name${agentName === 'YOUR_NAME' ? ' (it is in your $GROOVE_AGENT_NAME environment variable)' : ''}.`,
58
64
  '- Agent names are listed in `AGENTS_REGISTRY.md`, including agents on other teams.',
65
+ peerRule,
59
66
  '- Use `ask` when you need the reply now; use `tell` to hand off or notify without waiting.',
60
67
  '- One message per call. With `ask`, wait for the answer before deciding what to ask next.',
61
68
  '- Ground truth lives in files (specs, code, run output), not in chat — use InnerChat to',
@@ -0,0 +1,89 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ //
3
+ // Cross-daemon InnerChat relay transport (spec: plans/cross-daemon-innerchat-spec.md).
4
+ //
5
+ // An agent addresses a peer's agent as `name@alias`. A plain `name` stays purely
6
+ // local (zero behavior change); `name@alias` routes to the daemon configured
7
+ // under that alias. Transport is whatever the user can already reach — a
8
+ // tunnel-forwarded localhost port today, a Tailscale address tomorrow — so this
9
+ // module never assumes the topology: a peer is just a URL. The daemon stays
10
+ // bound to loopback; no new listener is opened.
11
+
12
+ const RELAY_FETCH_TIMEOUT_MS = 30000; // ask holds the peer's request open
13
+
14
+ // Peer config entry: { alias, url, daemonId }. Reuses the Axom connector's
15
+ // endpoint rules for the URL (http(s), no embedded credentials) and pins the
16
+ // peer's federation daemon id so replies/signatures resolve to one identity.
17
+ export function validatePeer(entry) {
18
+ if (!entry || typeof entry !== 'object') return 'peer must be an object';
19
+ const { alias, url, daemonId } = entry;
20
+ if (!alias || typeof alias !== 'string' || !/^[a-zA-Z0-9_-]{1,40}$/.test(alias)) {
21
+ return 'peer alias must be 1-40 chars (letters, digits, dash, underscore)';
22
+ }
23
+ if (!url || typeof url !== 'string') return 'peer url is required';
24
+ let parsed;
25
+ try { parsed = new URL(url); } catch { return `invalid url: ${url}`; }
26
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
27
+ return 'peer url must be http(s)';
28
+ }
29
+ if (parsed.username || parsed.password) return 'credentials in the url are not allowed';
30
+ if (!daemonId || typeof daemonId !== 'string' || !/^[a-f0-9]{6,64}$/.test(daemonId)) {
31
+ return 'peer daemonId must be lowercase hex (the peer daemon\'s federation id)';
32
+ }
33
+ return null;
34
+ }
35
+
36
+ // Split an InnerChat address. `name` → local (peer null). `name@alias` → remote.
37
+ // Agent names never contain '@', so it reliably marks a peer reference. Returns
38
+ // null on a malformed reference (empty name or empty/multi alias).
39
+ export function parsePeerRef(ref) {
40
+ if (!ref || typeof ref !== 'string' || !ref.includes('@')) return { name: ref, alias: null };
41
+ const parts = ref.split('@');
42
+ if (parts.length !== 2) return null; // transitive/garbled — one hop only
43
+ const [name, alias] = parts.map((s) => s.trim());
44
+ if (!name || !alias) return null;
45
+ return { name, alias };
46
+ }
47
+
48
+ // Thin HTTP client for the relay. Kept injectable so tests can drive the relay
49
+ // logic without binding two real servers.
50
+ export class RelayClient {
51
+ constructor({ fetchImpl = fetch, timeoutMs = RELAY_FETCH_TIMEOUT_MS } = {}) {
52
+ this.fetchImpl = fetchImpl;
53
+ this.timeoutMs = timeoutMs;
54
+ }
55
+
56
+ async _post(url, body, timeoutMs) {
57
+ const controller = new AbortController();
58
+ const timer = setTimeout(() => controller.abort(), timeoutMs || this.timeoutMs);
59
+ try {
60
+ const res = await this.fetchImpl(url, {
61
+ method: 'POST',
62
+ headers: { 'Content-Type': 'application/json' },
63
+ body: JSON.stringify(body),
64
+ signal: controller.signal,
65
+ });
66
+ const json = await res.json().catch(() => ({}));
67
+ return { ok: res.ok, status: res.status, json };
68
+ } finally {
69
+ clearTimeout(timer);
70
+ }
71
+ }
72
+
73
+ // POST a signed relay envelope to a peer. `{ payload, signature }`.
74
+ sendRelay(peerUrl, envelope, timeoutMs) {
75
+ return this._post(`${trimUrl(peerUrl)}/api/innerchat/relay`, envelope, timeoutMs);
76
+ }
77
+
78
+ // Drain replies queued on a peer for us (async `tell` answers). Signed so the
79
+ // peer can confirm which daemon is asking and hand back only its entries.
80
+ fetchOutbox(peerUrl, envelope, timeoutMs) {
81
+ return this._post(`${trimUrl(peerUrl)}/api/innerchat/relay/outbox`, envelope, timeoutMs);
82
+ }
83
+ }
84
+
85
+ function trimUrl(url) {
86
+ return url.replace(/\/+$/, '');
87
+ }
88
+
89
+ export { RELAY_FETCH_TIMEOUT_MS };