mrmd-monitor 0.1.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/README.md +517 -0
- package/bin/cli.js +178 -0
- package/package.json +32 -0
- package/src/coordination.js +358 -0
- package/src/document.js +187 -0
- package/src/execution.js +261 -0
- package/src/index.js +14 -0
- package/src/monitor.js +420 -0
- package/src/terminal.js +294 -0
package/README.md
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
# mrmd-monitor
|
|
2
|
+
|
|
3
|
+
A headless Yjs peer that monitors and executes code in mrmd notebooks, ensuring long-running executions survive browser disconnects.
|
|
4
|
+
|
|
5
|
+
## Why This Exists
|
|
6
|
+
|
|
7
|
+
### The Problem
|
|
8
|
+
|
|
9
|
+
In the original mrmd architecture:
|
|
10
|
+
- Browsers connect directly to MRP runtimes (mrmd-python, etc.)
|
|
11
|
+
- Browser writes execution output to Y.Text (the document)
|
|
12
|
+
- mrmd-sync handles Yjs synchronization and file persistence
|
|
13
|
+
|
|
14
|
+
This works great, but has one issue: **if a browser disconnects during a long-running execution, the connection to the runtime is lost and output stops flowing to the document.**
|
|
15
|
+
|
|
16
|
+
### Failed Approach: Hub Architecture
|
|
17
|
+
|
|
18
|
+
We tried making mrmd-sync a "hub" that:
|
|
19
|
+
- Routes all execution through the server
|
|
20
|
+
- Writes output directly to Y.Text from server-side
|
|
21
|
+
- Manages runtime connections centrally
|
|
22
|
+
|
|
23
|
+
This failed because:
|
|
24
|
+
1. **y-codemirror binding conflicts** - The binding expects changes to come from Yjs peers, not server-side manipulation
|
|
25
|
+
2. **Position finding is fragile** - Searching for `\`\`\`output:exec-123` markers breaks with concurrent edits
|
|
26
|
+
3. **Tight coupling** - Mixing sync logic with execution logic creates complexity
|
|
27
|
+
4. **Single point of failure** - If hub crashes, both sync AND execution die
|
|
28
|
+
|
|
29
|
+
### New Approach: Monitor as Yjs Peer
|
|
30
|
+
|
|
31
|
+
Instead of making the server special, we create a **headless Yjs client** that:
|
|
32
|
+
- Connects to mrmd-sync as a regular peer (just like browsers)
|
|
33
|
+
- Monitors execution requests
|
|
34
|
+
- Connects to MRP runtimes
|
|
35
|
+
- Writes output to Y.Text (through the normal Yjs sync flow)
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐
|
|
39
|
+
│ Browser A │ │ Browser B │ │ mrmd-monitor │
|
|
40
|
+
│ (editor) │ │ (editor) │ │ (headless peer) │
|
|
41
|
+
└──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘
|
|
42
|
+
│ │ │
|
|
43
|
+
│ Yjs sync (all equal peers) │
|
|
44
|
+
└─────────────────┬─────────────────────────┘
|
|
45
|
+
│
|
|
46
|
+
▼
|
|
47
|
+
┌────────────────┐
|
|
48
|
+
│ mrmd-sync │
|
|
49
|
+
│ │
|
|
50
|
+
│ • Yjs provider│
|
|
51
|
+
│ • File sync │
|
|
52
|
+
└────────────────┘
|
|
53
|
+
│
|
|
54
|
+
▼
|
|
55
|
+
notebook.md
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Key insight:** The monitor writes to Y.Text exactly like a browser would. mrmd-sync doesn't know or care that it's special.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Architecture
|
|
63
|
+
|
|
64
|
+
### Components
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
mrmd-monitor/
|
|
68
|
+
├── src/
|
|
69
|
+
│ ├── index.js # Main entry, exports
|
|
70
|
+
│ ├── monitor.js # RuntimeMonitor class
|
|
71
|
+
│ ├── execution.js # Execution handling, MRP client
|
|
72
|
+
│ ├── document.js # Y.Text manipulation (output blocks)
|
|
73
|
+
│ └── coordination.js # Y.Map protocol for browser/monitor coordination
|
|
74
|
+
├── bin/
|
|
75
|
+
│ └── cli.js # CLI entry point
|
|
76
|
+
├── package.json
|
|
77
|
+
└── README.md
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Data Flow
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
1. Browser wants to execute code
|
|
84
|
+
│
|
|
85
|
+
▼
|
|
86
|
+
2. Browser writes to Y.Map('executions'):
|
|
87
|
+
{
|
|
88
|
+
"exec-123": {
|
|
89
|
+
status: "requested",
|
|
90
|
+
code: "print('hello')",
|
|
91
|
+
language: "python",
|
|
92
|
+
runtimeUrl: "http://localhost:8000/mrp/v1",
|
|
93
|
+
requestedBy: <clientId>,
|
|
94
|
+
requestedAt: <timestamp>
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
│
|
|
98
|
+
▼
|
|
99
|
+
3. Monitor observes Y.Map change
|
|
100
|
+
│
|
|
101
|
+
▼
|
|
102
|
+
4. Monitor claims execution:
|
|
103
|
+
Y.Map.set("exec-123", { ...existing, status: "claimed", claimedBy: <monitorId> })
|
|
104
|
+
│
|
|
105
|
+
▼
|
|
106
|
+
5. Browser sees claim, creates output block in Y.Text:
|
|
107
|
+
```python
|
|
108
|
+
print('hello')
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```output:exec-123
|
|
112
|
+
```
|
|
113
|
+
│
|
|
114
|
+
▼
|
|
115
|
+
6. Browser confirms output block ready:
|
|
116
|
+
Y.Map.set("exec-123", { ...existing, status: "ready", outputBlockReady: true })
|
|
117
|
+
│
|
|
118
|
+
▼
|
|
119
|
+
7. Monitor connects to MRP runtime (SSE streaming)
|
|
120
|
+
│
|
|
121
|
+
▼
|
|
122
|
+
8. Monitor writes output to Y.Text (finds output block, appends)
|
|
123
|
+
Y.Map.set("exec-123", { ...existing, status: "running" })
|
|
124
|
+
│
|
|
125
|
+
▼
|
|
126
|
+
9. Output syncs to all browsers via Yjs
|
|
127
|
+
│
|
|
128
|
+
▼
|
|
129
|
+
10. Execution completes:
|
|
130
|
+
Y.Map.set("exec-123", { ...existing, status: "completed", result: {...} })
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Coordination Protocol
|
|
136
|
+
|
|
137
|
+
### Y.Map('executions') Schema
|
|
138
|
+
|
|
139
|
+
```javascript
|
|
140
|
+
{
|
|
141
|
+
"exec-<id>": {
|
|
142
|
+
// Identity
|
|
143
|
+
id: "exec-<id>",
|
|
144
|
+
cellId: "cell-<id>", // Optional: which cell this is for
|
|
145
|
+
|
|
146
|
+
// Request (set by browser)
|
|
147
|
+
code: "print('hello')",
|
|
148
|
+
language: "python",
|
|
149
|
+
runtimeUrl: "http://localhost:8000/mrp/v1",
|
|
150
|
+
session: "default", // MRP session ID
|
|
151
|
+
|
|
152
|
+
// Coordination
|
|
153
|
+
status: "requested" | "claimed" | "ready" | "running" | "completed" | "error" | "cancelled",
|
|
154
|
+
requestedBy: <clientId>, // Browser that requested
|
|
155
|
+
requestedAt: <timestamp>,
|
|
156
|
+
claimedBy: <clientId>, // Monitor that claimed (or null)
|
|
157
|
+
claimedAt: <timestamp>,
|
|
158
|
+
|
|
159
|
+
// Output block coordination
|
|
160
|
+
outputBlockReady: false, // Browser sets true when output block exists
|
|
161
|
+
outputPosition: { // Yjs RelativePosition for output insertion
|
|
162
|
+
type: <typeId>,
|
|
163
|
+
item: <itemId>,
|
|
164
|
+
assoc: 0
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
// Runtime state
|
|
168
|
+
startedAt: <timestamp>,
|
|
169
|
+
completedAt: <timestamp>,
|
|
170
|
+
|
|
171
|
+
// Stdin coordination
|
|
172
|
+
stdinRequest: null | {
|
|
173
|
+
prompt: "Enter name: ",
|
|
174
|
+
password: false,
|
|
175
|
+
requestedAt: <timestamp>
|
|
176
|
+
},
|
|
177
|
+
stdinResponse: null | {
|
|
178
|
+
text: "Alice\n",
|
|
179
|
+
respondedAt: <timestamp>
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
// Result
|
|
183
|
+
result: null | <any>, // Final execution result
|
|
184
|
+
error: null | {
|
|
185
|
+
type: "NameError",
|
|
186
|
+
message: "name 'foo' is not defined",
|
|
187
|
+
traceback: [...]
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
// Rich outputs (stored here, rendered by browser)
|
|
191
|
+
displayData: [
|
|
192
|
+
{
|
|
193
|
+
mimeType: "image/png",
|
|
194
|
+
data: "base64...", // Small outputs inline
|
|
195
|
+
assetId: null // Or reference to asset
|
|
196
|
+
}
|
|
197
|
+
]
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Status State Machine
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
┌─────────────┐
|
|
206
|
+
│ requested │ ← Browser creates
|
|
207
|
+
└──────┬──────┘
|
|
208
|
+
│
|
|
209
|
+
Monitor claims│
|
|
210
|
+
▼
|
|
211
|
+
┌─────────────┐
|
|
212
|
+
│ claimed │
|
|
213
|
+
└──────┬──────┘
|
|
214
|
+
│
|
|
215
|
+
Browser creates │
|
|
216
|
+
output block │
|
|
217
|
+
▼
|
|
218
|
+
┌─────────────┐
|
|
219
|
+
│ ready │ ← Browser sets outputBlockReady=true
|
|
220
|
+
└──────┬──────┘
|
|
221
|
+
│
|
|
222
|
+
Monitor starts │
|
|
223
|
+
MRP execution │
|
|
224
|
+
▼
|
|
225
|
+
┌─────────────┐
|
|
226
|
+
│ running │ ← Monitor streaming output
|
|
227
|
+
└──────┬──────┘
|
|
228
|
+
│
|
|
229
|
+
┌────────────┼────────────┐
|
|
230
|
+
│ │ │
|
|
231
|
+
▼ ▼ ▼
|
|
232
|
+
┌───────────┐ ┌───────────┐ ┌───────────┐
|
|
233
|
+
│ completed │ │ error │ │ cancelled │
|
|
234
|
+
└───────────┘ └───────────┘ └───────────┘
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Browser Responsibilities
|
|
238
|
+
|
|
239
|
+
1. **Request execution:**
|
|
240
|
+
- Generate unique execId
|
|
241
|
+
- Write to Y.Map with status="requested"
|
|
242
|
+
- Include code, language, runtimeUrl
|
|
243
|
+
|
|
244
|
+
2. **Create output block:**
|
|
245
|
+
- Wait for status="claimed"
|
|
246
|
+
- Insert `\`\`\`output:<execId>\n\`\`\`` in Y.Text after code cell
|
|
247
|
+
- Store relative position in Y.Map
|
|
248
|
+
- Set status="ready", outputBlockReady=true
|
|
249
|
+
|
|
250
|
+
3. **Handle stdin:**
|
|
251
|
+
- Watch for stdinRequest in Y.Map
|
|
252
|
+
- Show input UI to user
|
|
253
|
+
- Write stdinResponse to Y.Map
|
|
254
|
+
|
|
255
|
+
4. **Render output:**
|
|
256
|
+
- Output appears in Y.Text via Yjs sync (monitor writes it)
|
|
257
|
+
- Rich outputs (images) in displayData, render via widgets
|
|
258
|
+
|
|
259
|
+
### Monitor Responsibilities
|
|
260
|
+
|
|
261
|
+
1. **Watch for requests:**
|
|
262
|
+
- Observe Y.Map('executions')
|
|
263
|
+
- Look for status="requested"
|
|
264
|
+
|
|
265
|
+
2. **Claim execution:**
|
|
266
|
+
- Set status="claimed", claimedBy=<myClientId>
|
|
267
|
+
- Only one monitor should claim (first wins via Yjs)
|
|
268
|
+
|
|
269
|
+
3. **Wait for output block:**
|
|
270
|
+
- Watch for status="ready", outputBlockReady=true
|
|
271
|
+
- Get outputPosition from Y.Map
|
|
272
|
+
|
|
273
|
+
4. **Execute:**
|
|
274
|
+
- Connect to MRP runtime via SSE
|
|
275
|
+
- Set status="running"
|
|
276
|
+
- Stream output to Y.Text at outputPosition
|
|
277
|
+
- Handle stdin requests (set stdinRequest, wait for stdinResponse)
|
|
278
|
+
|
|
279
|
+
5. **Complete:**
|
|
280
|
+
- Set status="completed" or "error"
|
|
281
|
+
- Store result/error in Y.Map
|
|
282
|
+
- Store displayData for rich outputs
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Y.Text Output Writing
|
|
287
|
+
|
|
288
|
+
### Finding the Output Block
|
|
289
|
+
|
|
290
|
+
```javascript
|
|
291
|
+
function findOutputBlock(ytext, execId) {
|
|
292
|
+
const text = ytext.toString();
|
|
293
|
+
const marker = '```output:' + execId;
|
|
294
|
+
const markerStart = text.indexOf(marker);
|
|
295
|
+
|
|
296
|
+
if (markerStart === -1) return null;
|
|
297
|
+
|
|
298
|
+
// Find the newline after marker
|
|
299
|
+
const contentStart = text.indexOf('\n', markerStart) + 1;
|
|
300
|
+
|
|
301
|
+
// Find the closing ```
|
|
302
|
+
const closingBackticks = text.indexOf('\n```', contentStart);
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
markerStart,
|
|
306
|
+
contentStart,
|
|
307
|
+
contentEnd: closingBackticks === -1 ? text.length : closingBackticks,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Appending Output
|
|
313
|
+
|
|
314
|
+
```javascript
|
|
315
|
+
function appendOutput(ytext, execId, content) {
|
|
316
|
+
const block = findOutputBlock(ytext, execId);
|
|
317
|
+
if (!block) {
|
|
318
|
+
console.warn('Output block not found for', execId);
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Insert just before the closing ```
|
|
323
|
+
ytext.insert(block.contentEnd, content);
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Using Relative Positions (Better)
|
|
329
|
+
|
|
330
|
+
Instead of searching by text, use Yjs RelativePosition:
|
|
331
|
+
|
|
332
|
+
```javascript
|
|
333
|
+
// Browser stores position when creating output block
|
|
334
|
+
const outputStart = /* position after ```output:exec-123\n */;
|
|
335
|
+
const relPos = Y.createRelativePositionFromTypeIndex(ytext, outputStart);
|
|
336
|
+
|
|
337
|
+
// Store in Y.Map
|
|
338
|
+
execMap.set(execId, {
|
|
339
|
+
...existing,
|
|
340
|
+
outputPosition: Y.relativePositionToJSON(relPos)
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// Monitor uses position
|
|
344
|
+
const relPos = Y.createRelativePositionFromJSON(exec.outputPosition);
|
|
345
|
+
const absPos = Y.createAbsolutePositionFromRelativePosition(relPos, ydoc);
|
|
346
|
+
if (absPos) {
|
|
347
|
+
ytext.insert(absPos.index, content);
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## MRP Client
|
|
354
|
+
|
|
355
|
+
The monitor connects to MRP runtimes using the same protocol as browsers:
|
|
356
|
+
|
|
357
|
+
```javascript
|
|
358
|
+
async function executeStreaming(runtimeUrl, code, language, options = {}) {
|
|
359
|
+
const response = await fetch(`${runtimeUrl}/execute/stream`, {
|
|
360
|
+
method: 'POST',
|
|
361
|
+
headers: { 'Content-Type': 'application/json' },
|
|
362
|
+
body: JSON.stringify({
|
|
363
|
+
code,
|
|
364
|
+
session: options.session || 'default',
|
|
365
|
+
storeHistory: true,
|
|
366
|
+
}),
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// Parse SSE stream
|
|
370
|
+
const reader = response.body.getReader();
|
|
371
|
+
const decoder = new TextDecoder();
|
|
372
|
+
|
|
373
|
+
let currentEvent = null;
|
|
374
|
+
|
|
375
|
+
while (true) {
|
|
376
|
+
const { done, value } = await reader.read();
|
|
377
|
+
if (done) break;
|
|
378
|
+
|
|
379
|
+
const text = decoder.decode(value);
|
|
380
|
+
for (const line of text.split('\n')) {
|
|
381
|
+
if (line.startsWith('event: ')) {
|
|
382
|
+
currentEvent = line.slice(7);
|
|
383
|
+
} else if (line.startsWith('data: ')) {
|
|
384
|
+
const data = JSON.parse(line.slice(6));
|
|
385
|
+
|
|
386
|
+
switch (currentEvent) {
|
|
387
|
+
case 'stdout':
|
|
388
|
+
options.onStdout?.(data.content, data.accumulated);
|
|
389
|
+
break;
|
|
390
|
+
case 'stderr':
|
|
391
|
+
options.onStderr?.(data.content, data.accumulated);
|
|
392
|
+
break;
|
|
393
|
+
case 'stdin_request':
|
|
394
|
+
options.onStdinRequest?.(data);
|
|
395
|
+
break;
|
|
396
|
+
case 'display':
|
|
397
|
+
options.onDisplay?.(data);
|
|
398
|
+
break;
|
|
399
|
+
case 'result':
|
|
400
|
+
options.onResult?.(data);
|
|
401
|
+
break;
|
|
402
|
+
case 'error':
|
|
403
|
+
options.onError?.(data);
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## CLI Usage
|
|
415
|
+
|
|
416
|
+
```bash
|
|
417
|
+
# Basic usage - connect to mrmd-sync and monitor executions
|
|
418
|
+
mrmd-monitor ws://localhost:4444
|
|
419
|
+
|
|
420
|
+
# With options
|
|
421
|
+
mrmd-monitor --doc notebook.md ws://localhost:4444
|
|
422
|
+
|
|
423
|
+
# Monitor specific document
|
|
424
|
+
mrmd-monitor --doc "projects/analysis.md" ws://localhost:4444
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Options
|
|
428
|
+
|
|
429
|
+
| Option | Description |
|
|
430
|
+
|--------|-------------|
|
|
431
|
+
| `--doc <path>` | Document to monitor (default: all documents) |
|
|
432
|
+
| `--log-level <level>` | Log level: debug, info, warn, error |
|
|
433
|
+
| `--name <name>` | Monitor name for Awareness |
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
## Implementation Plan
|
|
438
|
+
|
|
439
|
+
### Phase 1: Basic Execution
|
|
440
|
+
|
|
441
|
+
1. [ ] Connect to mrmd-sync as Yjs peer
|
|
442
|
+
2. [ ] Observe Y.Map('executions')
|
|
443
|
+
3. [ ] Claim executions with status="requested"
|
|
444
|
+
4. [ ] Wait for outputBlockReady
|
|
445
|
+
5. [ ] Connect to MRP runtime, stream output to Y.Text
|
|
446
|
+
6. [ ] Set completion status
|
|
447
|
+
|
|
448
|
+
### Phase 2: Stdin Support
|
|
449
|
+
|
|
450
|
+
1. [ ] Detect stdin_request from MRP
|
|
451
|
+
2. [ ] Set stdinRequest in Y.Map
|
|
452
|
+
3. [ ] Watch for stdinResponse
|
|
453
|
+
4. [ ] Send input to MRP runtime
|
|
454
|
+
|
|
455
|
+
### Phase 3: Rich Outputs
|
|
456
|
+
|
|
457
|
+
1. [ ] Handle displayData from MRP
|
|
458
|
+
2. [ ] Store small outputs inline in Y.Map
|
|
459
|
+
3. [ ] Large outputs: store as assets (future)
|
|
460
|
+
|
|
461
|
+
### Phase 4: Robustness
|
|
462
|
+
|
|
463
|
+
1. [ ] Handle monitor disconnect/reconnect
|
|
464
|
+
2. [ ] Handle runtime disconnect
|
|
465
|
+
3. [ ] Timeout for stuck executions
|
|
466
|
+
4. [ ] Multiple monitors (coordination)
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
## Comparison with Hub Approach
|
|
471
|
+
|
|
472
|
+
| Aspect | Hub (mrmd-sync) | Monitor (peer) |
|
|
473
|
+
|--------|-----------------|----------------|
|
|
474
|
+
| Complexity | High (all-in-one) | Low (separated) |
|
|
475
|
+
| Y.Text writes | Server-side (tricky) | Peer-side (natural) |
|
|
476
|
+
| Failure isolation | Poor | Good |
|
|
477
|
+
| Scaling | Hard | Easy (multiple monitors) |
|
|
478
|
+
| mrmd-sync changes | Major | None |
|
|
479
|
+
| y-codemirror compat | Issues | Works naturally |
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## FAQ
|
|
484
|
+
|
|
485
|
+
### Why not just keep execution in the browser?
|
|
486
|
+
|
|
487
|
+
That works fine for short executions. But:
|
|
488
|
+
- Long-running ML training (hours)
|
|
489
|
+
- User closes laptop
|
|
490
|
+
- Browser crashes/refreshes
|
|
491
|
+
|
|
492
|
+
Output stops flowing. Monitor ensures it continues.
|
|
493
|
+
|
|
494
|
+
### Why a separate process instead of in mrmd-sync?
|
|
495
|
+
|
|
496
|
+
1. **Separation of concerns** - Sync is sync, execution is execution
|
|
497
|
+
2. **Failure isolation** - Monitor crash doesn't affect sync
|
|
498
|
+
3. **Scaling** - Can run multiple monitors
|
|
499
|
+
4. **Simplicity** - mrmd-sync stays simple
|
|
500
|
+
|
|
501
|
+
### What if monitor crashes during execution?
|
|
502
|
+
|
|
503
|
+
Execution keeps running on MRP runtime. When monitor restarts:
|
|
504
|
+
- It sees status="running" in Y.Map
|
|
505
|
+
- It can reconnect to MRP (if runtime supports resume)
|
|
506
|
+
- Or mark as error and let user re-run
|
|
507
|
+
|
|
508
|
+
### What about multiple monitors?
|
|
509
|
+
|
|
510
|
+
First one to set status="claimed" wins (Yjs handles concurrency).
|
|
511
|
+
Other monitors see it's claimed and skip.
|
|
512
|
+
|
|
513
|
+
### Can browser execute directly without monitor?
|
|
514
|
+
|
|
515
|
+
Yes! The original architecture still works. Monitor is additive.
|
|
516
|
+
Browser can execute directly for quick things.
|
|
517
|
+
Monitor is for "fire and forget" long-running executions.
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* mrmd-monitor CLI
|
|
5
|
+
*
|
|
6
|
+
* Headless Yjs peer for monitoring and executing code in mrmd notebooks.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* mrmd-monitor ws://localhost:4444
|
|
10
|
+
* mrmd-monitor --doc notebook.md ws://localhost:4444
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { RuntimeMonitor } from '../src/monitor.js';
|
|
14
|
+
|
|
15
|
+
// Parse arguments
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
|
|
18
|
+
const options = {
|
|
19
|
+
doc: null,
|
|
20
|
+
logLevel: 'info',
|
|
21
|
+
name: 'mrmd-monitor',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
let syncUrl = null;
|
|
25
|
+
|
|
26
|
+
function printHelp() {
|
|
27
|
+
console.log(`
|
|
28
|
+
mrmd-monitor - Headless Yjs peer for execution monitoring
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
mrmd-monitor [options] <sync-url>
|
|
32
|
+
|
|
33
|
+
Arguments:
|
|
34
|
+
sync-url WebSocket URL for mrmd-sync (e.g., ws://localhost:4444)
|
|
35
|
+
|
|
36
|
+
Options:
|
|
37
|
+
--doc <path> Document to monitor (default: first document)
|
|
38
|
+
--name <name> Monitor name for Awareness (default: mrmd-monitor)
|
|
39
|
+
--log-level <level> Log level: debug, info, warn, error (default: info)
|
|
40
|
+
--help, -h Show this help
|
|
41
|
+
|
|
42
|
+
Examples:
|
|
43
|
+
mrmd-monitor ws://localhost:4444
|
|
44
|
+
mrmd-monitor --doc notebook.md ws://localhost:4444
|
|
45
|
+
mrmd-monitor --log-level debug ws://localhost:4444
|
|
46
|
+
|
|
47
|
+
The monitor connects to mrmd-sync as a Yjs peer and:
|
|
48
|
+
- Watches for execution requests in Y.Map('executions')
|
|
49
|
+
- Claims and executes requests via MRP runtimes
|
|
50
|
+
- Writes output to Y.Text (the document)
|
|
51
|
+
- Handles stdin requests from runtimes
|
|
52
|
+
`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Parse arguments
|
|
56
|
+
for (let i = 0; i < args.length; i++) {
|
|
57
|
+
const arg = args[i];
|
|
58
|
+
|
|
59
|
+
if (arg === '--help' || arg === '-h') {
|
|
60
|
+
printHelp();
|
|
61
|
+
process.exit(0);
|
|
62
|
+
} else if (arg === '--doc') {
|
|
63
|
+
options.doc = args[++i];
|
|
64
|
+
if (!options.doc) {
|
|
65
|
+
console.error('Error: --doc requires a path');
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
} else if (arg === '--name') {
|
|
69
|
+
options.name = args[++i];
|
|
70
|
+
if (!options.name) {
|
|
71
|
+
console.error('Error: --name requires a value');
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
} else if (arg === '--log-level') {
|
|
75
|
+
options.logLevel = args[++i];
|
|
76
|
+
if (!['debug', 'info', 'warn', 'error'].includes(options.logLevel)) {
|
|
77
|
+
console.error('Error: --log-level must be debug, info, warn, or error');
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
} else if (!arg.startsWith('-')) {
|
|
81
|
+
syncUrl = arg;
|
|
82
|
+
} else {
|
|
83
|
+
console.error(`Unknown option: ${arg}`);
|
|
84
|
+
console.error('Run with --help for usage');
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Validate
|
|
90
|
+
if (!syncUrl) {
|
|
91
|
+
console.error('Error: sync-url is required');
|
|
92
|
+
console.error('Run with --help for usage');
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Ensure URL has protocol
|
|
97
|
+
if (!syncUrl.startsWith('ws://') && !syncUrl.startsWith('wss://')) {
|
|
98
|
+
syncUrl = 'ws://' + syncUrl;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Document path - if not specified, use a default
|
|
102
|
+
const docPath = options.doc || 'default';
|
|
103
|
+
|
|
104
|
+
// Logging
|
|
105
|
+
const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
106
|
+
const minLevel = LOG_LEVELS[options.logLevel] ?? 1;
|
|
107
|
+
|
|
108
|
+
function log(entry) {
|
|
109
|
+
try {
|
|
110
|
+
const parsed = JSON.parse(entry);
|
|
111
|
+
if (LOG_LEVELS[parsed.level] >= minLevel) {
|
|
112
|
+
const time = parsed.timestamp.split('T')[1].split('.')[0];
|
|
113
|
+
const level = parsed.level.toUpperCase().padEnd(5);
|
|
114
|
+
const msg = parsed.message;
|
|
115
|
+
const extra = Object.keys(parsed)
|
|
116
|
+
.filter(k => !['timestamp', 'level', 'component', 'message'].includes(k))
|
|
117
|
+
.map(k => `${k}=${JSON.stringify(parsed[k])}`)
|
|
118
|
+
.join(' ');
|
|
119
|
+
|
|
120
|
+
console.log(`[${time}] ${level} ${msg}${extra ? ' ' + extra : ''}`);
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
console.log(entry);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Start monitor
|
|
128
|
+
console.log('');
|
|
129
|
+
console.log('\x1b[36m%s\x1b[0m', ' mrmd-monitor');
|
|
130
|
+
console.log(' ────────────');
|
|
131
|
+
console.log(` Sync: ${syncUrl}`);
|
|
132
|
+
console.log(` Document: ${docPath}`);
|
|
133
|
+
console.log(` Name: ${options.name}`);
|
|
134
|
+
console.log('');
|
|
135
|
+
|
|
136
|
+
const monitor = new RuntimeMonitor(syncUrl, docPath, {
|
|
137
|
+
name: options.name,
|
|
138
|
+
log,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Handle shutdown
|
|
142
|
+
let shuttingDown = false;
|
|
143
|
+
|
|
144
|
+
async function shutdown(signal) {
|
|
145
|
+
if (shuttingDown) return;
|
|
146
|
+
shuttingDown = true;
|
|
147
|
+
|
|
148
|
+
console.log('');
|
|
149
|
+
log(JSON.stringify({
|
|
150
|
+
timestamp: new Date().toISOString(),
|
|
151
|
+
level: 'info',
|
|
152
|
+
message: `Received ${signal}, shutting down...`,
|
|
153
|
+
}));
|
|
154
|
+
|
|
155
|
+
monitor.disconnect();
|
|
156
|
+
|
|
157
|
+
// Give time for cleanup
|
|
158
|
+
setTimeout(() => {
|
|
159
|
+
process.exit(0);
|
|
160
|
+
}, 500);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
164
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
165
|
+
|
|
166
|
+
// Connect
|
|
167
|
+
monitor.connect()
|
|
168
|
+
.then(() => {
|
|
169
|
+
log(JSON.stringify({
|
|
170
|
+
timestamp: new Date().toISOString(),
|
|
171
|
+
level: 'info',
|
|
172
|
+
message: 'Monitor ready, waiting for execution requests...',
|
|
173
|
+
}));
|
|
174
|
+
})
|
|
175
|
+
.catch((err) => {
|
|
176
|
+
console.error('Failed to connect:', err.message);
|
|
177
|
+
process.exit(1);
|
|
178
|
+
});
|