@mediagato/modelreins-channel 4.11.1 → 4.11.3

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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * @module modelreins-channel
4
- * @version 4.6.0
4
+ * @version 4.11.3
5
5
  *
6
6
  * ModelReins MCP Channel
7
7
  * ======================
@@ -134,6 +134,12 @@
134
134
  * you can do locally, delegate the rest to a headless
135
135
  * worker with more model capacity.
136
136
  *
137
+ * modelreins_result — Read back a job's actual output: full metadata plus
138
+ * the recorded output stream. This is the other half
139
+ * of dispatch — without it, dispatched work is a one
140
+ * way street. Works on running jobs too (partial
141
+ * output so far).
142
+ *
137
143
  * modelreins_status — Snapshot of the entire fleet: workers, recent jobs,
138
144
  * active schedules. Useful for situational awareness
139
145
  * before dispatching ("is worker-01 free right now?").
@@ -161,6 +167,10 @@
161
167
  * Channel polls → pulls job → notification → Claude executes →
162
168
  * Claude calls modelreins_complete → POST /jobs/{id}/output + PUT /jobs/{id}
163
169
  * → dashboard updates in real time.
170
+ *
171
+ * Readback flow (the return trip):
172
+ * Claude calls modelreins_result → GET /jobs/{id} + GET /jobs/{id}/output
173
+ * → job metadata + recorded output rendered back into the session.
164
174
  */
165
175
 
166
176
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
@@ -273,7 +283,7 @@ const STUCK_JOB_MS = 30 * 60 * 1000; // 30 min — generous Claude completion w
273
283
  // without user interaction.
274
284
 
275
285
  const mcp = new Server(
276
- { name: 'modelreins', version: '4.6.0' },
286
+ { name: 'modelreins', version: '4.11.3' },
277
287
  {
278
288
  capabilities: {
279
289
  // Enable the Claude-specific channel notification method.
@@ -306,6 +316,11 @@ const mcp = new Server(
306
316
  'DISPATCHING TO OTHER WORKERS:',
307
317
  'Use modelreins_dispatch to hand off subtasks. This is powerful: you can split a large job,',
308
318
  'keep the local/interactive parts for yourself, and delegate batch work to headless workers.',
319
+ '',
320
+ 'READING RESULTS BACK:',
321
+ 'Use modelreins_result to read a job\'s actual output — dispatch alone is a one way street.',
322
+ 'After a dispatched job completes (check with modelreins_status), fetch its real result',
323
+ 'before reporting on it or building on it. Works mid-run too for partial output.',
309
324
  ].join('\n'),
310
325
  }
311
326
  );
@@ -397,6 +412,37 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
397
412
  },
398
413
  },
399
414
 
415
+ // ── modelreins_result ─────────────────────────────────────────────────
416
+ {
417
+ name: 'modelreins_result',
418
+ description: [
419
+ 'Read back a job\'s actual result: full metadata (status, worker, timing, exit code)',
420
+ 'plus the recorded output stream. This is the other half of modelreins_dispatch —',
421
+ 'without it, dispatched work is a one way street.',
422
+ '',
423
+ 'Use after modelreins_status shows a dispatched job as done/failed, to get the real',
424
+ 'output instead of just a status flag. Also works on running jobs — returns whatever',
425
+ 'partial output the worker has streamed so far.',
426
+ '',
427
+ 'Output is tail-truncated at max_chars (the end of a job\'s output carries the',
428
+ 'summary/conclusion). Truncation is always reported explicitly, never silent.',
429
+ ].join('\n'),
430
+ inputSchema: {
431
+ type: 'object',
432
+ properties: {
433
+ job_id: {
434
+ type: 'string',
435
+ description: 'The job ID to read back (from modelreins_dispatch or modelreins_status).',
436
+ },
437
+ max_chars: {
438
+ type: 'number',
439
+ description: 'Max characters of output to return (tail-biased). Default 30000, max 200000.',
440
+ },
441
+ },
442
+ required: ['job_id'],
443
+ },
444
+ },
445
+
400
446
  // ── modelreins_status ─────────────────────────────────────────────────
401
447
  {
402
448
  name: 'modelreins_status',
@@ -628,6 +674,74 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
628
674
  return { content: [{ type: 'text', text: lines.join('\n') }] };
629
675
  }
630
676
 
677
+ // ── modelreins_result ─────────────────────────────────────────────────────
678
+
679
+ if (name === 'modelreins_result') {
680
+ const jobId = String(args.job_id).trim();
681
+ const maxChars = Math.max(1000, Math.min(args.max_chars ?? 30000, 200000));
682
+
683
+ const [jobRes, outRes] = await Promise.all([
684
+ api('GET', `/jobs/${jobId}`),
685
+ api('GET', `/jobs/${jobId}/output?limit=500`),
686
+ ]);
687
+
688
+ // A missing job and an unreachable server are NOT the same finding —
689
+ // same discipline as the workers block in modelreins_status.
690
+ if (jobRes.status === 404) {
691
+ return { content: [{ type: 'text', text: `Job #${jobId} not found (404). Check the ID — jobs are tenant-scoped, so a job dispatched under another token is invisible here.` }] };
692
+ }
693
+ if (jobRes.status < 200 || jobRes.status >= 300) {
694
+ return { content: [{ type: 'text', text: `COULD NOT READ job #${jobId} — HTTP ${jobRes.status}${jobRes.data?.error ? ` (${jobRes.data.error})` : ''}. This is a failed check, not a missing job.` }] };
695
+ }
696
+
697
+ const job = jobRes.data.job || {};
698
+ const lines = [`## Job #${jobId} — ${job.status || 'unknown'}`];
699
+ const meta = [];
700
+ if (job.assigned_to) meta.push(`worker: ${job.assigned_to}`);
701
+ if (job.model_preference) meta.push(`model: ${job.model_preference}`);
702
+ if (job.exit_code !== null && job.exit_code !== undefined) meta.push(`exit: ${job.exit_code}`);
703
+ if (job.created_at) meta.push(`created: ${job.created_at}`);
704
+ if (job.completed_at) meta.push(`completed: ${job.completed_at}`);
705
+ if (meta.length) lines.push(meta.join(' · '));
706
+ if (job.error) lines.push(`error: ${job.error}`);
707
+ if (job.prompt) {
708
+ const p = String(job.prompt);
709
+ lines.push('', `### Prompt${p.length > 300 ? ' (first 300 chars)' : ''}`, p.slice(0, 300));
710
+ }
711
+
712
+ if (outRes.status < 200 || outRes.status >= 300) {
713
+ lines.push('', `### Output — COULD NOT READ (HTTP ${outRes.status}${outRes.data?.error ? `: ${outRes.data.error}` : ''})`);
714
+ lines.push('This is a failed read, not an empty result.');
715
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
716
+ }
717
+
718
+ const rows = outRes.data?.output || [];
719
+ const systemRows = rows.filter(r => r.stream === 'system');
720
+ const bodyRows = rows.filter(r => r.stream !== 'system');
721
+ // Join with '' — workers stream token/text DELTAS that carry their own
722
+ // whitespace (verified against job output from both the Node worker
723
+ // daemon and LlamaStash). Joining with '\n' renders one token per line.
724
+ const body = bodyRows.map(r => r.content).join('');
725
+
726
+ if (rows.length === 0) {
727
+ lines.push('', '### Output', 'No output rows recorded for this job. The worker never posted output — for a done job that usually means it completed via a bare status update without a summary.');
728
+ } else {
729
+ let shown = body;
730
+ let truncNote = '';
731
+ if (body.length > maxChars) {
732
+ shown = body.slice(-maxChars);
733
+ truncNote = ` — TRUNCATED, showing last ${maxChars} of ${body.length} chars (raise max_chars for more)`;
734
+ }
735
+ lines.push('', `### Output (${bodyRows.length} chunk${bodyRows.length === 1 ? '' : 's'}, ${body.length} chars${truncNote})`, shown);
736
+ if (systemRows.length) {
737
+ lines.push('', `### System notes (${systemRows.length})`);
738
+ for (const r of systemRows.slice(-10)) lines.push(` ${r.content}`);
739
+ }
740
+ }
741
+
742
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
743
+ }
744
+
631
745
  // ── modelreins_status ─────────────────────────────────────────────────────
632
746
 
633
747
  if (name === 'modelreins_status') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mediagato/modelreins-channel",
3
- "version": "4.11.1",
3
+ "version": "4.11.3",
4
4
  "description": "ModelReins MCP Channel — turn any Claude Code session into a ModelReins worker",
5
5
  "type": "module",
6
6
  "main": "modelreins-channel.mjs",
@@ -10,13 +10,7 @@
10
10
  "dependencies": {
11
11
  "@modelcontextprotocol/sdk": "^1.12.0"
12
12
  },
13
- "keywords": [
14
- "modelreins",
15
- "mcp",
16
- "claude-code",
17
- "channel",
18
- "ai-orchestration"
19
- ],
13
+ "keywords": ["modelreins", "mcp", "claude-code", "channel", "ai-orchestration"],
20
14
  "license": "MIT",
21
15
  "homepage": "https://modelreins.com"
22
16
  }