groove-dev 0.27.197 → 0.27.199

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 (76) 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/axom-integration/Screenshot_2026-07-25_at_6.44.56_PM.png +0 -0
  5. package/axom-integration/Screenshot_2026-07-25_at_6.45.35_PM.png +0 -0
  6. package/node_modules/@groove-dev/cli/package.json +1 -1
  7. package/node_modules/@groove-dev/daemon/package.json +1 -1
  8. package/node_modules/@groove-dev/daemon/src/api.js +2 -0
  9. package/node_modules/@groove-dev/daemon/src/axom-connector.js +368 -0
  10. package/node_modules/@groove-dev/daemon/src/axom-install.js +162 -0
  11. package/node_modules/@groove-dev/daemon/src/axom-remote.js +230 -0
  12. package/node_modules/@groove-dev/daemon/src/axom-server.js +249 -0
  13. package/node_modules/@groove-dev/daemon/src/federation.js +6 -0
  14. package/node_modules/@groove-dev/daemon/src/index.js +14 -0
  15. package/node_modules/@groove-dev/daemon/src/innerchat-docs.js +8 -1
  16. package/node_modules/@groove-dev/daemon/src/innerchat-relay.js +89 -0
  17. package/node_modules/@groove-dev/daemon/src/innerchat.js +261 -1
  18. package/node_modules/@groove-dev/daemon/src/introducer.js +1 -1
  19. package/node_modules/@groove-dev/daemon/src/network-guard.js +56 -0
  20. package/node_modules/@groove-dev/daemon/src/process.js +1 -1
  21. package/node_modules/@groove-dev/daemon/src/providers/axom.js +66 -0
  22. package/node_modules/@groove-dev/daemon/src/providers/index.js +2 -0
  23. package/node_modules/@groove-dev/daemon/src/routes/axom.js +267 -0
  24. package/node_modules/@groove-dev/daemon/src/routes/innerchat.js +334 -20
  25. package/node_modules/@groove-dev/daemon/test/axom-connector.test.js +455 -0
  26. package/node_modules/@groove-dev/daemon/test/axom-remote.test.js +115 -0
  27. package/node_modules/@groove-dev/daemon/test/axom-server.test.js +228 -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-BbA-X4CE.js +1084 -0
  30. package/node_modules/@groove-dev/gui/dist/assets/index-BbE3qX83.css +1 -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 -0
  34. package/node_modules/@groove-dev/gui/src/app.css +53 -0
  35. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +3 -1
  36. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -1
  37. package/node_modules/@groove-dev/gui/src/stores/groove.js +22 -0
  38. package/node_modules/@groove-dev/gui/src/stores/slices/axom-slice.js +279 -0
  39. package/node_modules/@groove-dev/gui/src/views/axom.jsx +1897 -0
  40. package/node_modules/@groove-dev/gui/src/views/settings.jsx +232 -4
  41. package/package.json +2 -2
  42. package/packages/cli/package.json +1 -1
  43. package/packages/daemon/package.json +1 -1
  44. package/packages/daemon/src/api.js +2 -0
  45. package/packages/daemon/src/axom-connector.js +368 -0
  46. package/packages/daemon/src/axom-install.js +162 -0
  47. package/packages/daemon/src/axom-remote.js +230 -0
  48. package/packages/daemon/src/axom-server.js +249 -0
  49. package/packages/daemon/src/federation.js +6 -0
  50. package/packages/daemon/src/index.js +14 -0
  51. package/packages/daemon/src/innerchat-docs.js +8 -1
  52. package/packages/daemon/src/innerchat-relay.js +89 -0
  53. package/packages/daemon/src/innerchat.js +261 -1
  54. package/packages/daemon/src/introducer.js +1 -1
  55. package/packages/daemon/src/network-guard.js +56 -0
  56. package/packages/daemon/src/process.js +1 -1
  57. package/packages/daemon/src/providers/axom.js +66 -0
  58. package/packages/daemon/src/providers/index.js +2 -0
  59. package/packages/daemon/src/routes/axom.js +267 -0
  60. package/packages/daemon/src/routes/innerchat.js +334 -20
  61. package/packages/gui/dist/assets/index-BbA-X4CE.js +1084 -0
  62. package/packages/gui/dist/assets/index-BbE3qX83.css +1 -0
  63. package/packages/gui/dist/index.html +2 -2
  64. package/packages/gui/package.json +1 -1
  65. package/packages/gui/src/App.jsx +2 -0
  66. package/packages/gui/src/app.css +53 -0
  67. package/packages/gui/src/components/agents/agent-feed.jsx +3 -1
  68. package/packages/gui/src/components/layout/activity-bar.jsx +2 -1
  69. package/packages/gui/src/stores/groove.js +22 -0
  70. package/packages/gui/src/stores/slices/axom-slice.js +279 -0
  71. package/packages/gui/src/views/axom.jsx +1897 -0
  72. package/packages/gui/src/views/settings.jsx +232 -4
  73. package/node_modules/@groove-dev/gui/dist/assets/index-Da2nbd6M.js +0 -1043
  74. package/node_modules/@groove-dev/gui/dist/assets/index-zmrIbwNm.css +0 -1
  75. package/packages/gui/dist/assets/index-Da2nbd6M.js +0 -1043
  76. package/packages/gui/dist/assets/index-zmrIbwNm.css +0 -1
@@ -0,0 +1,279 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ //
3
+ // Axom provider state. Everything rendered from this slice is
4
+ // telemetry-grounded: interrupt/stop UI states flip on EVENTS from the
5
+ // runtime, never on our own optimism (contract §2/§7 — fail-deceptive is
6
+ // worse than fail-open).
7
+
8
+ import { api } from '../../lib/api';
9
+
10
+ const EVENT_BUFFER = 2000;
11
+ // How long a sent prompt stays eligible to claim the next turn. Past this,
12
+ // it is more likely orphaned than starting, and claiming would misattribute.
13
+ const PROMPT_CORRELATION_WINDOW_S = 120;
14
+
15
+ const sessionKey = (endpoint, session) => `${endpoint}/${session}`;
16
+
17
+ export const createAxomSlice = (set, get) => ({
18
+ // ── Axom Provider ─────────────────────────────────────────
19
+ axomStatus: { endpoints: [] },
20
+ axomSelected: null, // { endpoint, session }
21
+ axomEvents: {}, // sessionKey -> [envelope] (bounded)
22
+ // Local interrupt ledger: id -> {text, state}. state: 'sent' (POST accepted,
23
+ // not yet seen on the stream) | 'heard' (interrupt event arrived) |
24
+ // 'acked' (interrupt_ack arrived). Rollup states come from pipeline_done.
25
+ axomInterrupts: {}, // sessionKey -> { [id]: {text, state, truncated} }
26
+ // Stop states: null | 'pending' (POST sent, nothing effected yet) |
27
+ // 'effected' (stop_effected seen) | 'resolved-before-stop' (§7 edge:
28
+ // pipeline_done arrived with no stop_effected).
29
+ axomStops: {}, // sessionKey -> state
30
+ // Contract-violation log (§8): e.g. an interrupt_ack without interrupt_id.
31
+ // A fallback that fires must announce itself — never degrade silently.
32
+ axomAnomalies: {}, // sessionKey -> [{eventId, message}]
33
+ // Prompts WE sent, per session — GROOVE's own record of its own action,
34
+ // not telemetry (the runtime's pipeline_start carries no prompt text). Each
35
+ // entry attaches to the next pipeline_start envelope so a turn can show
36
+ // what was asked. Prompts sent by another client (the REPL, a second
37
+ // GROOVE) are not ours to know: those turns render without a user bubble
38
+ // rather than with a guessed one.
39
+ axomPrompts: {}, // sessionKey -> [{ text, ts, attachedTo: evId|null }]
40
+ axomInstances: [], // managed local instances (contract §11)
41
+ axomInstall: { phase: 'idle', file: null, receivedBytes: 0, totalBytes: 0, error: null },
42
+ axomHardware: null, // machine readiness report for local inference
43
+ axomMyEndpoint: null, // this GROOVE's own Axom endpoint (share/copy)
44
+
45
+ async fetchAxomStatus() {
46
+ try {
47
+ const data = await api.get('/axom/status');
48
+ set({ axomStatus: data || { endpoints: [] } });
49
+ const instances = await api.get('/axom/instances');
50
+ set({ axomInstances: instances || [] });
51
+ } catch { /* daemon predates the axom routes */ }
52
+ },
53
+
54
+ async fetchAxomHardware() {
55
+ try {
56
+ const data = await api.get('/axom/hardware');
57
+ set({ axomHardware: data });
58
+ } catch { /* daemon predates the route */ }
59
+ try {
60
+ const me = await api.get('/axom/my-endpoint');
61
+ set({ axomMyEndpoint: me });
62
+ } catch { /* daemon predates the route */ }
63
+ try {
64
+ // Availability ("Coming soon" on builds with no distribution) must be
65
+ // known before the welcome page offers an install action.
66
+ const install = await api.get('/axom/install');
67
+ set((s) => ({ axomInstall: { ...s.axomInstall, ...install } }));
68
+ } catch { /* daemon predates the route */ }
69
+ },
70
+
71
+ async startAxomInstall(manifestUrl) {
72
+ await api.post('/axom/install', manifestUrl ? { manifestUrl } : {});
73
+ },
74
+
75
+ // §14: end the RUNTIME (not a turn). Contract statuses are returned rather
76
+ // than thrown — 409 (turn in flight) and 501 (runtime predates the verb)
77
+ // are things the UI must SAY, not failures to swallow.
78
+ async shutdownAxomRuntime(endpoint, { force = false } = {}) {
79
+ try {
80
+ await api.post('/axom/shutdown', { endpoint, force });
81
+ await get().fetchAxomStatus();
82
+ return { stopping: true };
83
+ } catch (err) {
84
+ if (err.status === 409) return { turnInFlight: true };
85
+ if (err.status === 501) return { unsupported: true };
86
+ throw err;
87
+ }
88
+ },
89
+
90
+ async startAxomInstance(id = 'default') {
91
+ const instance = await api.post('/axom/instances', { id });
92
+ await get().fetchAxomStatus();
93
+ return instance;
94
+ },
95
+
96
+ async stopAxomInstance(id) {
97
+ await api.delete(`/axom/instances/${encodeURIComponent(id)}`);
98
+ await get().fetchAxomStatus();
99
+ },
100
+
101
+ async saveAxomEndpoints(endpoints) {
102
+ const data = await api.patch('/axom/config', { endpoints });
103
+ await get().fetchAxomStatus();
104
+ return data;
105
+ },
106
+
107
+ async selectAxomSession(endpoint, session) {
108
+ set({ axomSelected: { endpoint, session } });
109
+ // Backfill from the daemon's ring so a freshly opened tab has history.
110
+ try {
111
+ const data = await api.get(`/axom/sessions/${encodeURIComponent(session)}/events?endpoint=${encodeURIComponent(endpoint)}`);
112
+ const key = sessionKey(endpoint, session);
113
+ set((s) => {
114
+ const existing = s.axomEvents[key] || [];
115
+ const seen = new Set(existing.map((e) => e.id));
116
+ const merged = [...(data.events || []).filter((e) => !seen.has(e.id)), ...existing]
117
+ .sort((a, b) => (a.id < b.id ? -1 : 1))
118
+ .slice(-EVENT_BUFFER);
119
+ return { axomEvents: { ...s.axomEvents, [key]: merged } };
120
+ });
121
+ } catch { /* session may have no ring yet — live events still flow */ }
122
+ },
123
+
124
+ ingestAxomEvent(endpoint, session, envelope) {
125
+ const key = sessionKey(endpoint, session);
126
+ set((s) => {
127
+ const buf = s.axomEvents[key] || [];
128
+ if (buf.length && buf[buf.length - 1].id === envelope.id) return {}; // WS echo guard
129
+ const updates = {
130
+ axomEvents: { ...s.axomEvents, [key]: [...buf, envelope].slice(-EVENT_BUFFER) },
131
+ };
132
+
133
+ // Correlate our sent prompt with the turn it started.
134
+ //
135
+ // The runtime's pipeline_start carries no client reference (§12 has no
136
+ // echo field yet), so correlation is inference, and inference here can
137
+ // LIE: another client (the REPL, a second GROOVE) opening a turn on this
138
+ // session would otherwise adopt our text as its prompt — displaying our
139
+ // words above someone else's turn. Confidently wrong is worse than
140
+ // silent, so we only attach when the inference is unambiguous:
141
+ // - exactly one prompt of ours is pending (two in flight = we cannot
142
+ // tell which turn is which), and
143
+ // - it was sent recently (a stale pending prompt is more likely
144
+ // orphaned than finally starting).
145
+ // Anything else stays unattached and renders as "started elsewhere" —
146
+ // honest silence. A client_ref echo in pipeline_start would make this
147
+ // exact; requested from the Axom side.
148
+ if (envelope.kind === 'pipeline_start') {
149
+ const prompts = s.axomPrompts[key] || [];
150
+ const pending = prompts.filter((p) => p.attachedTo === null);
151
+ const ref = envelope.payload?.client_ref;
152
+ // §15: when the runtime echoes our ref, correlation is EXACT — match
153
+ // on it and never guess. A ref that is present but not ours means the
154
+ // turn belongs to another client: attach nothing.
155
+ let claim = null;
156
+ if (typeof ref === 'string' && ref) {
157
+ claim = pending.find((p) => p.ref === ref) || null;
158
+ } else if (ref === undefined) {
159
+ // Pre-§15 runtime: fall back to the bounded inference above.
160
+ claim = (pending.length === 1
161
+ && (Date.now() / 1000) - pending[0].ts < PROMPT_CORRELATION_WINDOW_S)
162
+ ? pending[0] : null;
163
+ }
164
+ if (claim) {
165
+ const idx = prompts.indexOf(claim);
166
+ const next = prompts.slice();
167
+ next[idx] = { ...next[idx], attachedTo: envelope.id };
168
+ updates.axomPrompts = { ...s.axomPrompts, [key]: next };
169
+ }
170
+ }
171
+
172
+ // Interrupt lifecycle — driven only by what the runtime says happened.
173
+ // §8 pins interrupt_ack.payload.interrupt_id as ALWAYS present; the
174
+ // oldest-pending fallback stays as defense but surfaces as an anomaly
175
+ // when it fires — an ack without an id is a runtime bug to expose.
176
+ if (envelope.kind === 'interrupt' || envelope.kind === 'interrupt_ack') {
177
+ const ledger = { ...(s.axomInterrupts[key] || {}) };
178
+ const evId = envelope.payload?.interrupt_id ?? envelope.payload?.id;
179
+ const nextState = envelope.kind === 'interrupt' ? 'heard' : 'acked';
180
+ if (evId && ledger[evId]) {
181
+ ledger[evId] = { ...ledger[evId], state: nextState };
182
+ } else if (evId) {
183
+ // Known id, not ours — an interrupt posted by another client (e.g.
184
+ // typed straight into the REPL). Normal; it lives in the ticker,
185
+ // our ledger stays untouched.
186
+ } else {
187
+ // No id at all — a shape violation per §8. Advance the oldest
188
+ // pending entry as defense, and announce that the fallback fired.
189
+ const pending = Object.entries(ledger).find(([, v]) => v.state !== nextState && v.state !== 'acked');
190
+ if (pending) ledger[pending[0]] = { ...pending[1], state: nextState };
191
+ updates.axomAnomalies = {
192
+ ...s.axomAnomalies,
193
+ [key]: [
194
+ ...(s.axomAnomalies[key] || []),
195
+ { eventId: envelope.id, message: `${envelope.kind} arrived without an interrupt id (§8 violation)${pending ? ` — fallback advanced "${pending[1].text?.slice(0, 40)}"` : ''}` },
196
+ ].slice(-50),
197
+ };
198
+ }
199
+ updates.axomInterrupts = { ...s.axomInterrupts, [key]: ledger };
200
+ }
201
+
202
+ if (envelope.kind === 'stop_effected' && s.axomStops[key] === 'pending') {
203
+ updates.axomStops = { ...s.axomStops, [key]: 'effected' };
204
+ }
205
+ // §7 stop edge: the run can resolve before stop lands. Release the
206
+ // sticky pressed state on pipeline_done and say what actually happened.
207
+ if (envelope.kind === 'pipeline_done' && s.axomStops[key] === 'pending') {
208
+ updates.axomStops = { ...s.axomStops, [key]: 'resolved-before-stop' };
209
+ }
210
+ return updates;
211
+ });
212
+ },
213
+
214
+ // §12: message starts a turn on a caller-chosen session id (first message
215
+ // creates the session). 409 means a turn is in flight — steer instead.
216
+ async sendAxomMessage(text) {
217
+ const sel = get().axomSelected;
218
+ if (!sel) throw new Error('No Axom session selected');
219
+ // §15: an opaque per-message ref the runtime echoes in pipeline_start,
220
+ // making prompt→turn correlation exact instead of inferred.
221
+ const ref = `g-${Math.random().toString(36).slice(2, 10)}${Date.now().toString(36).slice(-4)}`;
222
+ try {
223
+ await api.post(`/axom/sessions/${encodeURIComponent(sel.session)}/message`, {
224
+ endpoint: sel.endpoint, text, clientRef: ref,
225
+ });
226
+ // Record only after the runtime ACCEPTED it (202) — a rejected prompt
227
+ // never ran, so it must not appear in the transcript.
228
+ const key = sessionKey(sel.endpoint, sel.session);
229
+ set((s) => ({
230
+ axomPrompts: {
231
+ ...s.axomPrompts,
232
+ [key]: [...(s.axomPrompts[key] || []), { text, ref, ts: Date.now() / 1000, attachedTo: null }].slice(-200),
233
+ },
234
+ }));
235
+ await get().fetchAxomStatus();
236
+ return { ok: true };
237
+ } catch (err) {
238
+ if (err.status === 409) return { busy: true };
239
+ if (err.status === 413) return { tooLong: true, max: err.body?.max };
240
+ throw err;
241
+ }
242
+ },
243
+
244
+ async sendAxomInterrupt(text) {
245
+ const sel = get().axomSelected;
246
+ if (!sel) throw new Error('No Axom session selected');
247
+ const result = await api.post(`/axom/sessions/${encodeURIComponent(sel.session)}/interrupt`, {
248
+ endpoint: sel.endpoint, text,
249
+ });
250
+ const key = sessionKey(sel.endpoint, sel.session);
251
+ set((s) => ({
252
+ axomInterrupts: {
253
+ ...s.axomInterrupts,
254
+ [key]: {
255
+ ...(s.axomInterrupts[key] || {}),
256
+ [result.id]: { text, state: 'sent', truncated: !!result.truncated },
257
+ },
258
+ },
259
+ }));
260
+ return result;
261
+ },
262
+
263
+ async sendAxomStop() {
264
+ const sel = get().axomSelected;
265
+ if (!sel) throw new Error('No Axom session selected');
266
+ const key = sessionKey(sel.endpoint, sel.session);
267
+ set((s) => ({ axomStops: { ...s.axomStops, [key]: 'pending' } }));
268
+ try {
269
+ return await api.post(`/axom/sessions/${encodeURIComponent(sel.session)}/stop`, { endpoint: sel.endpoint });
270
+ } catch (err) {
271
+ // The POST failed — nothing was requested, so the button must not stay
272
+ // pressed pretending it was.
273
+ set((s) => ({ axomStops: { ...s.axomStops, [key]: null } }));
274
+ throw err;
275
+ }
276
+ },
277
+ });
278
+
279
+ export { sessionKey as axomSessionKey };