@remcp/runtime 0.2.188 → 0.2.190

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/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.190 — 2026-10-27
4
+
5
+ - Publish the runtime alongside the compatible ReMCP agent; the device HTTPS network probe is agent-owned and does not add runtime MCP tools.
6
+ - Preserve the existing 84-tool runtime contract while verifying public package source and release parity.
7
+
3
8
  ## 0.2.186 — 2026-10-26
4
9
 
5
10
  - Return from same-document CDP navigation promptly when Page.navigate provides no new loaderId instead of awaiting a full load event that never fires.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/runtime",
3
- "version": "0.2.188",
3
+ "version": "0.2.190",
4
4
  "description": "First-party ReMCP local device runtime: filesystem, terminal, computer-use, browser, diagnostics and document tools over MCP.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -228,8 +228,8 @@ export const coreToolDefinitionsPart2 = [
228
228
  properties: {
229
229
  pid: { type: 'number', description: 'Session pid returned by start_process.' },
230
230
  offset: { type: 'number', description: 'Zero-based line number to start from, counted across everything the session has produced; a negative value reads the last N lines. Omit for new output. Lines evicted by the buffer cap are no longer available.' },
231
- length: { type: 'number', description: 'Maximum number of lines to return.' },
232
- timeout_ms: { type: 'number', description: 'How long to wait for new output when no offset is given, in milliseconds.' },
231
+ length: { type: 'number', description: 'Maximum number of lines to return per call (default 200). Without offset, unreturned lines remain available; use hasMore to fetch the next page.' },
232
+ timeout_ms: { type: 'number', description: 'With no offset, wait up to this many milliseconds for output/exit; 0 returns immediately (default). Use 1000–2000 to avoid repeated remote polls when output is expected.' },
233
233
  },
234
234
  required: ['pid'],
235
235
  additionalProperties: false,
@@ -288,7 +288,7 @@ export const coreToolDefinitionsPart2 = [
288
288
  required: ['pid'],
289
289
  additionalProperties: false,
290
290
  },
291
- annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
291
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
292
292
  outputSchema: TERMINATE_PROCESS_OUTPUT_SCHEMA,
293
293
  handler: terminalToolHandlers.force_terminate,
294
294
  },
@@ -335,7 +335,7 @@ export const coreToolDefinitionsPart2 = [
335
335
  required: ['pid'],
336
336
  additionalProperties: false,
337
337
  },
338
- annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
338
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
339
339
  handler: systemToolHandlers.kill_process,
340
340
  },
341
341
  {
@@ -45,7 +45,8 @@ export const READ_PROCESS_OUTPUT_SCHEMA = terminalOutputSchema({
45
45
  range:{ type:'string' },
46
46
  output:{ type:'string' },
47
47
  explicitOffset:{ type:'boolean' },
48
- }, ['range','output','explicitOffset']);
48
+ hasMore:{ type:'boolean' },
49
+ }, ['range','output','explicitOffset','hasMore']);
49
50
  export const WAIT_PROCESS_OUTPUT_SCHEMA = terminalOutputSchema({
50
51
  pattern:{ type:'string' },
51
52
  matched:{ type:'boolean' },
package/src/sessions.mjs CHANGED
@@ -37,13 +37,15 @@ function notify(session) {
37
37
 
38
38
  // A waiter whose timer fires first must remove itself, otherwise finished sessions keep
39
39
  // holding closures until the next append (which may never come).
40
- function waiter(session, timeoutMs) {
40
+ function waiter(session, timeoutMs, signal = null) {
41
+ if (signal?.aborted) return Promise.resolve();
41
42
  return new Promise(resolve => {
42
43
  let settled = false;
43
44
  const finish = () => {
44
45
  if (settled) return;
45
46
  settled = true;
46
47
  clearTimeout(timer);
48
+ signal?.removeEventListener('abort', finish);
47
49
  const index = session.waiters.indexOf(finish);
48
50
  if (index !== -1) session.waiters.splice(index, 1);
49
51
  resolve();
@@ -51,6 +53,9 @@ function waiter(session, timeoutMs) {
51
53
  const timer = setTimeout(finish, Math.max(0, timeoutMs));
52
54
  timer.unref?.();
53
55
  session.waiters.push(finish);
56
+ signal?.addEventListener('abort', finish, { once:true });
57
+ // The signal can abort between the initial check and listener registration.
58
+ if (signal?.aborted) finish();
54
59
  });
55
60
  }
56
61
 
@@ -65,6 +70,8 @@ export function createProcessSession({ pid, child, command, shell }) {
65
70
  lines: [],
66
71
  partial: '',
67
72
  lastPartialRead: null,
73
+ lastPartialLineIndex: null,
74
+ cursorColumn: 0,
68
75
  droppedLines: 0,
69
76
  bufferedChars: 0,
70
77
  cursor: 0,
@@ -131,13 +138,64 @@ export function totalLines(session) {
131
138
  return session.droppedLines + session.lines.length + (session.partial ? 1 : 0);
132
139
  }
133
140
 
134
- export function readNewOutput(session) {
135
- const from = Math.max(0, session.cursor - session.droppedLines);
136
- const complete = session.lines.slice(from);
137
- session.cursor = session.droppedLines + session.lines.length;
138
- const parts = [...complete];
139
- if (session.partial && session.partial !== session.lastPartialRead) parts.push(session.partial);
140
- session.lastPartialRead = session.partial || null;
141
+ export function readNewOutput(session, maxLines = Infinity, maxChars = Infinity) {
142
+ const parts = [];
143
+ const lineLimit = Math.max(1, maxLines);
144
+ let remaining = Math.max(1, maxChars);
145
+ // If the buffer evicted a partially read line, its character offset does
146
+ // not belong to the first remaining line; never skip that fresh output.
147
+ if (session.cursor < session.droppedLines) {
148
+ session.cursor = session.droppedLines;
149
+ session.cursorColumn = 0;
150
+ session.lastPartialRead = null;
151
+ session.lastPartialLineIndex = null;
152
+ }
153
+ let from = session.cursor - session.droppedLines;
154
+ while (from < session.lines.length && parts.length < lineLimit && remaining > 0) {
155
+ const absolute = session.droppedLines + from;
156
+ const line = session.lines[from];
157
+ let skip = session.cursorColumn || 0;
158
+ if (session.lastPartialLineIndex === absolute && session.lastPartialRead
159
+ && line.startsWith(session.lastPartialRead)) {
160
+ skip = Math.max(skip, session.lastPartialRead.length);
161
+ }
162
+ const suffix = line.slice(skip);
163
+ const room = remaining - (parts.length ? 1 : 0);
164
+ if (room <= 0) break;
165
+ if (suffix.length > room) {
166
+ // Preserve the undispatched tail rather than consuming then truncating it.
167
+ parts.push(suffix.slice(0, room));
168
+ session.cursorColumn = skip + room;
169
+ break;
170
+ }
171
+ parts.push(suffix);
172
+ remaining -= suffix.length + (parts.length > 1 ? 1 : 0);
173
+ from++;
174
+ session.cursor = session.droppedLines + from;
175
+ session.cursorColumn = 0;
176
+ if (session.lastPartialLineIndex === absolute) {
177
+ session.lastPartialRead = null;
178
+ session.lastPartialLineIndex = null;
179
+ }
180
+ }
181
+ // Partial lines may be updated without a newline. Send only newly appended
182
+ // characters, and track even a partial page of that unfinished line.
183
+ if (session.cursor === session.droppedLines + session.lines.length
184
+ && parts.length < lineLimit && remaining > 0 && session.partial
185
+ && session.partial !== session.lastPartialRead) {
186
+ const sent = session.lastPartialLineIndex === session.cursor
187
+ && session.partial.startsWith(session.lastPartialRead || '')
188
+ ? session.lastPartialRead || '' : '';
189
+ const room = remaining - (parts.length ? 1 : 0);
190
+ if (room > 0) {
191
+ const delta = session.partial.slice(sent.length, sent.length + room);
192
+ if (delta) {
193
+ parts.push(delta);
194
+ session.lastPartialRead = sent + delta;
195
+ session.lastPartialLineIndex = session.cursor;
196
+ }
197
+ }
198
+ }
141
199
  return parts;
142
200
  }
143
201
 
@@ -164,9 +222,9 @@ export function readOutputRange(session, offset, length) {
164
222
  return { slice: snapshot.slice(start, end), start: session.droppedLines + start + 1, end: session.droppedLines + end, total };
165
223
  }
166
224
 
167
- export async function waitForProcessActivity(session, timeoutMs) {
168
- if (session.exited) return;
169
- await waiter(session, timeoutMs);
225
+ export async function waitForProcessActivity(session, timeoutMs, signal = null) {
226
+ if (session.exited || signal?.aborted) return;
227
+ await waiter(session, timeoutMs, signal);
170
228
  }
171
229
 
172
230
  export async function waitForProcessExit(session, timeoutMs) {
@@ -129,7 +129,9 @@ export async function startProcessTool(args, extra = {}) {
129
129
  const output = session.lines.slice(-200).join('\n');
130
130
  const partial = session.partial;
131
131
  session.cursor = session.droppedLines + session.lines.length;
132
+ session.cursorColumn = 0;
132
133
  session.lastPartialRead = session.partial || null;
134
+ session.lastPartialLineIndex = session.partial ? session.cursor : null;
133
135
  const warning = verdict.note ? String(verdict.note) : '';
134
136
  const rendered = [warning, headline, output, partial].filter(Boolean).join('\n');
135
137
  const status = describeSession(session);
@@ -147,6 +149,7 @@ export async function readProcessOutputTool(args, extra = {}) {
147
149
  const session = getProcessSession(pid);
148
150
  if (!session) fail(`No ReMCP session with pid ${pid}`);
149
151
  const timeoutMs = clampInteger(args.timeout_ms, 0, 0, 120000);
152
+ const maxLines = clampInteger(args.length, 200, 1, 5000);
150
153
  const hasOffset = args.offset !== undefined && args.offset !== null;
151
154
  let slice;
152
155
  let range;
@@ -154,22 +157,28 @@ export async function readProcessOutputTool(args, extra = {}) {
154
157
  // An explicit offset is a peek at a line range: it does not consume the new-output
155
158
  // cursor, so a caller can look at the tail and still read everything afterwards.
156
159
  // Zero-based from the first line the session produced; negative reads the last N.
157
- const page = readOutputRange(session, Number(args.offset), clampInteger(args.length, 200, 1, 5000));
160
+ const page = readOutputRange(session, Number(args.offset), maxLines);
158
161
  slice = page.slice;
159
162
  range = `${page.start}-${page.end} of ${page.total}`;
160
163
  } else {
161
- // Never sleep over output that is already buffered.
164
+ // Wait for a process event, exit, cancellation or deadline. Repeated
165
+ // 200ms wakeups multiplied unnecessarily across many concurrent sessions.
162
166
  if (timeoutMs && !hasNewOutput(session)) {
163
- const deadline = Date.now() + timeoutMs;
164
- while (!hasNewOutput(session) && !session.exited && Date.now() < deadline) {
167
+ const deadline = performance.now() + timeoutMs;
168
+ while (!hasNewOutput(session) && !session.exited && performance.now() < deadline) {
165
169
  throwIfAborted(extra.signal);
166
- await waitForProcessActivity(session, Math.min(200, Math.max(20, deadline - Date.now())));
170
+ await waitForProcessActivity(session, Math.max(0, deadline - performance.now()), extra.signal);
167
171
  }
168
172
  }
169
- slice = readNewOutput(session);
173
+ throwIfAborted(extra.signal);
174
+ const before = Math.max(session.cursor, session.droppedLines);
175
+ // 32K code units keep each structured + text mirror below the 128K
176
+ // truncation boundary, even for multi-byte Unicode lines.
177
+ slice = readNewOutput(session, maxLines, 32 * 1024);
170
178
  session.waitCursor = session.cursor;
171
- const first = Math.max(1, session.cursor - slice.length + 1);
172
- range = slice.length ? `${first}-${session.cursor} of ${totalLines(session)}` : `no new output (${totalLines(session)} lines total)`;
179
+ range = slice.length
180
+ ? `${before + 1}-${before + slice.length} of ${totalLines(session)}`
181
+ : `no new output (${totalLines(session)} lines total)`;
173
182
  }
174
183
  const status = describeSession(session);
175
184
  const header = `pid ${pid} ${status.status} · lines ${range}`;
@@ -179,6 +188,7 @@ export async function readProcessOutputTool(args, extra = {}) {
179
188
  range,
180
189
  output,
181
190
  explicitOffset:hasOffset,
191
+ hasMore:!hasOffset && hasNewOutput(session),
182
192
  }, `${header}\n${output}`);
183
193
  }
184
194
 
@@ -216,8 +226,10 @@ export async function waitForProcessOutputTool(args, extra = {}) {
216
226
  }
217
227
  const matched = waiterMatches(slice, matcher);
218
228
  session.cursor = session.droppedLines + session.lines.length;
229
+ session.cursorColumn = 0;
219
230
  session.waitCursor = session.cursor;
220
231
  session.lastPartialRead = session.partial || null;
232
+ session.lastPartialLineIndex = session.partial ? session.cursor : null;
221
233
  const status = describeSession(session);
222
234
  const headline = matched
223
235
  ? `pid ${pid} ${status.status} · pattern matched${bufferedMatch ? ' (already buffered)' : ''}`
@@ -243,8 +255,10 @@ export async function interactWithProcessTool(args, extra = {}) {
243
255
  const timeoutMs = clampInteger(args.timeout_ms, 1000, 0, 120000);
244
256
 
245
257
  session.cursor = session.droppedLines + session.lines.length;
258
+ session.cursorColumn = 0;
246
259
  session.waitCursor = session.cursor;
247
260
  session.lastPartialRead = null;
261
+ session.lastPartialLineIndex = null;
248
262
  throwIfAborted(extra.signal);
249
263
  const stdin = session.child?.stdin;
250
264
  if (!stdin || stdin.destroyed || session.stdinClosed) {