@henols/vice-mcp 0.2.2 → 0.2.4
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 +2 -2
- package/THIRD-PARTY-NOTICES.md +422 -1
- package/anno-bank.ts +171 -0
- package/anno-cli.ts +1736 -163
- package/anno-confidence.ts +2 -2
- package/anno-derive.ts +6 -6
- package/anno-details.ts +4 -4
- package/anno-enum-gen.ts +416 -30
- package/anno-export-asm.ts +1211 -126
- package/anno-graphics.ts +338 -0
- package/anno-hazard-report.ts +1367 -0
- package/anno-import.ts +495 -0
- package/anno-index.ts +8 -8
- package/anno-join.ts +480 -0
- package/anno-memmap-render.ts +22 -21
- package/anno-provenance-ledger.ts +472 -0
- package/anno-regbits-gen.ts +13 -13
- package/anno-register.ts +159 -0
- package/anno-store-export.ts +661 -0
- package/anno-store.ts +635 -124
- package/anno-symbols.ts +7 -7
- package/anno-tools.ts +1169 -16
- package/anno-types.ts +313 -40
- package/backend-detect.mts +124 -312
- package/build.ts +3 -1
- package/capture-predicate.ts +597 -0
- package/channel-lock.ts +349 -0
- package/evid-ingest.ts +217 -0
- package/evid-reconcile.ts +316 -0
- package/host-tool-client.ts +430 -0
- package/incident-record.ts +23 -12
- package/install-resources.ts +29 -13
- package/memmap-lookup.ts +285 -0
- package/package.json +27 -8
- package/prg-image.ts +1 -2
- package/repo-root.ts +87 -3
- package/resources/backend-detect.mjs +98 -236
- package/resources/broker-control.mjs +220 -54
- package/resources/broker-epoch.mjs +7 -8
- package/resources/broker-kill.mjs +36 -31
- package/resources/broker-launch.mjs +511 -374
- package/resources/broker-state.mjs +69 -24
- package/resources/container-guard.mjs +1 -1
- package/resources/ghidra-project.mjs +790 -0
- package/resources/host-tool.mjs +2533 -0
- package/resources/vice-broker.mjs +434 -290
- package/resources/vice-launcher.sh +127 -9
- package/stock-address.ts +1 -1
- package/stock-condition.ts +1 -1
- package/stock-connect.ts +9 -5
- package/stock-derived.ts +29 -37
- package/stock-diagnose.ts +200 -36
- package/stock-dispatch.ts +179 -77
- package/stock-handler.ts +1 -1
- package/stock-paths.ts +18 -14
- package/stock-petscii.ts +1 -1
- package/stock-protocol.ts +1 -1
- package/stock-recycle.ts +83 -2
- package/stock-reproducible-run.ts +811 -0
- package/stock-run-until.ts +100 -1
- package/stock-symbols.ts +4 -4
- package/stock-timing.ts +1 -1
- package/stop-oracle.ts +167 -0
- package/text-capability-probe.ts +660 -0
- package/text-connect.ts +157 -0
- package/text-protocol.ts +810 -0
- package/text-tools.ts +778 -0
- package/textmon-backtrace.ts +385 -0
- package/textmon-cpuhistory.ts +335 -0
- package/textmon-memmap.ts +494 -0
- package/textmon-profile.ts +458 -0
- package/textmon-registers.ts +748 -0
- package/tools-manifest.stock.json +864 -3
- package/vice-broker-client.ts +253 -108
- package/vice-errors.ts +268 -0
- package/vice-proxy.ts +339 -2144
- package/vsf-slice.ts +640 -0
- package/anno-d64.ts +0 -310
- package/capability-registry.ts +0 -390
- package/refresh-manifest.ts +0 -124
- package/tools-manifest.json +0 -1223
- package/vice-probe.ts +0 -278
- package/vice-sync.ts +0 -336
- package/vice.ts +0 -772
package/vice-broker-client.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Container-side half of the on-demand broker protocol.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// Container-side half of the on-demand broker protocol. This module used to
|
|
3
|
+
// write the request/lease files resources/vice-broker.sh read and read the
|
|
4
|
+
// grant/denial/broker files that script wrote, all on the SAME
|
|
5
5
|
// .vice-supervisor/ bind mount tools/vice-supervisor.sh's epoch.json already
|
|
6
|
-
// used.
|
|
7
|
-
//
|
|
6
|
+
// used. That whole file protocol is now deleted wholesale (six mechanisms
|
|
7
|
+
// retiring together -- startHeartbeat()/the mtime-as-heartbeat
|
|
8
8
|
// convention/touchLease()/pollGrant()/pollRecycleAck()/the request-grant-
|
|
9
9
|
// denial-lease-recycle-ack directory tree) now that vice-proxy.ts's
|
|
10
10
|
// acquisition, release AND recycle all run over the TCP control plane
|
|
@@ -17,20 +17,30 @@
|
|
|
17
17
|
//
|
|
18
18
|
// Every read of broker.json is still untrusted input: parse in try/catch, a
|
|
19
19
|
// malformed or half-written file is "not there yet" or "absent", never a
|
|
20
|
-
// thrown exception
|
|
21
|
-
//
|
|
20
|
+
// thrown exception -- the never-throw, never-cache-a-negative-result
|
|
21
|
+
// posture this module holds throughout.
|
|
22
22
|
//
|
|
23
23
|
// MUST NOT import hostpath.ts: the host-path consumer set is closed to
|
|
24
|
-
// exactly
|
|
25
|
-
// stock-paths.ts, vice-proxy.ts,
|
|
26
|
-
//
|
|
27
|
-
//
|
|
24
|
+
// exactly four production modules (containerpath.ts, install-resources.ts,
|
|
25
|
+
// stock-paths.ts, vice-proxy.ts), pinned by hostpath-consumers.test.ts, and
|
|
26
|
+
// host-path message text stays in vice-proxy.ts, which is already on that
|
|
27
|
+
// list.
|
|
28
28
|
import { readFileSync } from "node:fs";
|
|
29
29
|
import { randomUUID } from "node:crypto";
|
|
30
30
|
import { join, resolve } from "node:path";
|
|
31
31
|
import { connect, type Socket } from "node:net";
|
|
32
32
|
|
|
33
33
|
import { supervisorDir } from "./repo-root.ts";
|
|
34
|
+
// TYPE-ONLY, and IMPORTED rather than
|
|
35
|
+
// redeclared. broker-launch.mts is the one definition of the profile shape and
|
|
36
|
+
// the module that turns a profile into argv; a second local shape here is how
|
|
37
|
+
// a client would start requesting a knob the host cannot honour. Type-only, so
|
|
38
|
+
// the container-side bundle never resolves the host-bound module at runtime.
|
|
39
|
+
import type { LaunchProfile } from "./broker-launch.mts";
|
|
40
|
+
// backend-detect.mts is ViceBackend's one home (narrowed to a single literal
|
|
41
|
+
// now that the fork backend has been removed entirely). Type-only, same
|
|
42
|
+
// discipline as the import above.
|
|
43
|
+
import type { ViceBackend } from "./backend-detect.mts";
|
|
34
44
|
// The module tree's ONE definition of the container-visible host alias
|
|
35
45
|
// (vice.ts:49), carrying its own VICE_MCP_HOST override -- consumed below by
|
|
36
46
|
// resolveControlTarget() rather than a fourth `host.docker.internal` literal
|
|
@@ -44,12 +54,12 @@ import { supervisorDir } from "./repo-root.ts";
|
|
|
44
54
|
// and importing it would pull `hostpath.ts` into this module, which this
|
|
45
55
|
// file's own header (lines 23-26) forbids and which the host-path
|
|
46
56
|
// consumer-set assertion polices.
|
|
47
|
-
import { mcpHost, ViceError } from "./vice.ts";
|
|
57
|
+
import { mcpHost, ViceError } from "./vice-errors.ts";
|
|
48
58
|
|
|
49
59
|
// -------------------------------------------------------------- request ids
|
|
50
60
|
//
|
|
51
|
-
// Primary noun of this protocol
|
|
52
|
-
//
|
|
61
|
+
// Primary noun of this protocol: a request/grant/lease is identified by
|
|
62
|
+
// this id, never by port -- ports are
|
|
53
63
|
// recycled across sessions under on-demand launch, so a port is an attribute
|
|
54
64
|
// OF a grant, not identity. Matched byte-for-byte against the same shape
|
|
55
65
|
// resources/vice-broker.sh's own request-id pattern validates (T-01.2-01);
|
|
@@ -57,12 +67,10 @@ import { mcpHost, ViceError } from "./vice.ts";
|
|
|
57
67
|
// shared corpus through both validators so neither side can silently accept
|
|
58
68
|
// an id shape the other rejects.
|
|
59
69
|
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
// re-stating the pattern a third time; the bash copy
|
|
65
|
-
// (resources/vice-broker.sh) does not retire until that phase deletes it.
|
|
70
|
+
// This is a real, typed, NAMED export whose VALUE is unchanged from
|
|
71
|
+
// the pre-conversion .mjs (verified live). The in-process broker imports
|
|
72
|
+
// this exact binding rather than re-stating the pattern a third time; the
|
|
73
|
+
// bash copy (resources/vice-broker.sh) does not retire until it is deleted.
|
|
66
74
|
export const REQUEST_ID_PATTERN: RegExp = /^req-[0-9]+-[0-9]+-[0-9a-f]{8}$/;
|
|
67
75
|
|
|
68
76
|
export function newRequestId(): string {
|
|
@@ -82,8 +90,9 @@ export function isValidRequestId(id: unknown): id is string {
|
|
|
82
90
|
// The five sibling directory helpers this function used to anchor
|
|
83
91
|
// (requestsDir/grantsDir/denialsDir/brokerLeasesDir/recycleAcksDir) and the
|
|
84
92
|
// lease path helper (leasePathFor) are GONE, not merely unused -- their
|
|
85
|
-
// directories cease to exist
|
|
86
|
-
// survives, since broker.json itself is not part of
|
|
93
|
+
// directories cease to exist now that the file protocol is retired; only
|
|
94
|
+
// brokerJsonPath() below survives, since broker.json itself is not part of
|
|
95
|
+
// the retiring protocol.
|
|
87
96
|
export function brokerRootDir(): string {
|
|
88
97
|
return process.env.VICE_POOL_DIR ? resolve(process.env.VICE_POOL_DIR) : supervisorDir();
|
|
89
98
|
}
|
|
@@ -125,7 +134,7 @@ function readJsonMaybe(path: string): Record<string, unknown> | null {
|
|
|
125
134
|
// writeRequest/createLease/touchLease/releaseLease/pollGrant/pollRecycleAck
|
|
126
135
|
// and their record interfaces (RequestRecord, RecycleRequestRecord,
|
|
127
136
|
// LeaseRecord, PollOptions, PollGrantResult, PollRecycleAckResult) are GONE:
|
|
128
|
-
// the whole file-messaging protocol they implemented
|
|
137
|
+
// the whole file-messaging protocol they implemented is replaced
|
|
129
138
|
// wholesale by the TCP control plane below. GRANT_POLL_TIMEOUT_MS/
|
|
130
139
|
// GRANT_POLL_INTERVAL_MS/RECYCLE_ACK_TIMEOUT_MS/RECYCLE_ACK_POLL_INTERVAL_MS
|
|
131
140
|
// (the retiring polls' own timeout/interval constants) and sleepMs() (their
|
|
@@ -174,8 +183,8 @@ export function readBrokerLiveness(path: string = brokerJsonPath()): BrokerLiven
|
|
|
174
183
|
}
|
|
175
184
|
|
|
176
185
|
// StartHeartbeatOptions/HEARTBEAT_MS/startHeartbeat() are GONE -- the
|
|
177
|
-
// lease-heartbeat interval (one of
|
|
178
|
-
// successor. Nothing needs touching to prove a TCP connection is alive; it
|
|
186
|
+
// lease-heartbeat interval (one of the six retiring mechanisms named above)
|
|
187
|
+
// has no successor. Nothing needs touching to prove a TCP connection is alive; it
|
|
179
188
|
// either is, or the broker's own "close" handler has already reclaimed the
|
|
180
189
|
// instance.
|
|
181
190
|
|
|
@@ -291,9 +300,8 @@ export function resolveControlTarget(record: Record<string, unknown>, port: numb
|
|
|
291
300
|
// ---------------------------------------------------- TCP control plane
|
|
292
301
|
//
|
|
293
302
|
// The container-side half of the TCP control plane (broker-control.mts is
|
|
294
|
-
// the host-side half). Wire format confirmed at
|
|
295
|
-
//
|
|
296
|
-
// .planning/RE-FINDINGS.md for the full record): newline-delimited JSON,
|
|
303
|
+
// the host-side half). Wire format confirmed at an early blocking
|
|
304
|
+
// decision checkpoint (2026-08-03, `as-specified`): newline-delimited JSON,
|
|
297
305
|
// per-boot capability token, connection open = claim / close = release.
|
|
298
306
|
export interface AcquireGrant {
|
|
299
307
|
id: string;
|
|
@@ -301,6 +309,25 @@ export interface AcquireGrant {
|
|
|
301
309
|
url: string;
|
|
302
310
|
epoch_file: string;
|
|
303
311
|
supervisor_dir: string;
|
|
312
|
+
/** The broker-allocated port stock's `-remotemonitor` text monitor binds,
|
|
313
|
+
* mandatory in fact once a stock acquire without one was made to fail
|
|
314
|
+
* outright rather than degrade.
|
|
315
|
+
* Absent on a fork grant only -- a stock grant that could not bind a
|
|
316
|
+
* text-monitor port no longer reaches the wire at all: the acquire fails
|
|
317
|
+
* outright (`no_free_text_port`) before any grant is produced. */
|
|
318
|
+
remote_monitor_port?: number;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Parses the wire's `remote_monitor_port` into a validated
|
|
322
|
+
* integer in 1..65535, or `undefined` when the key is absent OR the observed
|
|
323
|
+
* value is not a valid port -- never a fabricated 0/null standing in for
|
|
324
|
+
* "no port", and never an unvalidated number handed downstream to a dial.
|
|
325
|
+
* Shared by both AcquireGrant construction sites below so the same
|
|
326
|
+
* validation cannot drift between them. */
|
|
327
|
+
function parseOptionalRemoteMonitorPort(value: unknown): number | undefined {
|
|
328
|
+
if (value === undefined) return undefined;
|
|
329
|
+
const n = Number(value);
|
|
330
|
+
return Number.isInteger(n) && n >= 1 && n <= 65535 ? n : undefined;
|
|
304
331
|
}
|
|
305
332
|
|
|
306
333
|
export interface AcquireOverControlPlaneHandle {
|
|
@@ -311,7 +338,7 @@ export interface AcquireOverControlPlaneHandle {
|
|
|
311
338
|
release: () => void;
|
|
312
339
|
}
|
|
313
340
|
|
|
314
|
-
/**
|
|
341
|
+
/** Default raised from 25000 to 120000. The
|
|
315
342
|
* knob (VICE_BROKER_ACQUIRE_TIMEOUT_MS) is unchanged -- an explicitly
|
|
316
343
|
* configured value keeps working exactly as before.
|
|
317
344
|
*
|
|
@@ -327,13 +354,62 @@ export interface AcquireOverControlPlaneHandle {
|
|
|
327
354
|
* client's generic timeout. */
|
|
328
355
|
export const CONTROL_ACQUIRE_TIMEOUT_MS: number = Number(process.env.VICE_BROKER_ACQUIRE_TIMEOUT_MS || 120000);
|
|
329
356
|
|
|
357
|
+
/** The request-side launch profile.
|
|
358
|
+
* Optional and absent by default at BOTH acquire write sites in this file.
|
|
359
|
+
*
|
|
360
|
+
* ONE RULE, and it is the whole reason this shape is named rather than
|
|
361
|
+
* inlined: the `profile` key must be OMITTED ENTIRELY when no profile was
|
|
362
|
+
* requested, not written as `profile: undefined`. `JSON.stringify` drops an
|
|
363
|
+
* `undefined` value, so both spellings happen to produce the same bytes today
|
|
364
|
+
* -- but an explicit `null` or `{}` would not, and the property this phase
|
|
365
|
+
* needs is that a profile-less acquire's wire line is BYTE-IDENTICAL to the
|
|
366
|
+
* one this client has always written. The spread idiom below is what makes
|
|
367
|
+
* that structural rather than incidental.
|
|
368
|
+
*
|
|
369
|
+
* The SECOND rule, which cost this plan its own dedicated must-have: the
|
|
370
|
+
* profile has to be written at BOTH sites. This file has two independent
|
|
371
|
+
* acquire writers -- acquireOverControlPlane()'s raw `socket.write` below and
|
|
372
|
+
* openBrokerControl()'s `sendAndAwaitLine` further down. A field added to only
|
|
373
|
+
* one of them silently never arrives for callers on the other path, which is
|
|
374
|
+
* the same defect class as a tool argument that is accepted and dropped.
|
|
375
|
+
*
|
|
376
|
+
* CONSUMER STATUS: SUBSTRATE, NOT YET WIRED. The profile
|
|
377
|
+
* threads client -> wire -> narrowing -> eligibility -> argv -> record with
|
|
378
|
+
* tests at every hop, but NO production call site passes one yet:
|
|
379
|
+
* `acquireOverControlPlane()` and `BrokerControlSession.acquire()` are only
|
|
380
|
+
* ever invoked without `opts.profile`, so `-warp` and `-console` are
|
|
381
|
+
* unreachable in production. That is deliberate -- the chain was built
|
|
382
|
+
* ahead of the callers that will use it -- and is recorded here rather than
|
|
383
|
+
* left for a reader to discover, because a fully-tested chain reads as a
|
|
384
|
+
* live one.
|
|
385
|
+
*
|
|
386
|
+
* Do NOT close this by inventing a call site. Note also that the profile is
|
|
387
|
+
* refused outright on the fork backend (broker-control.mts):
|
|
388
|
+
* it maps to stock-only launch flags, so the first real consumer has to be on
|
|
389
|
+
* stock. */
|
|
390
|
+
export interface AcquireProfileOptions {
|
|
391
|
+
profile?: LaunchProfile;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Builds the `profile` fragment of an acquire request line -- the ONE place
|
|
395
|
+
* this client decides whether the key appears at all, so the two write sites
|
|
396
|
+
* cannot drift apart on that decision. Returns an empty object (no key) when
|
|
397
|
+
* no profile was requested. */
|
|
398
|
+
function acquireProfileFragment(profile?: LaunchProfile): { profile?: LaunchProfile } {
|
|
399
|
+
return profile === undefined ? {} : { profile };
|
|
400
|
+
}
|
|
401
|
+
|
|
330
402
|
/** Reads broker.json ONCE for control_host/control_port/control_token,
|
|
331
403
|
* opens ONE TCP connection, sends a single `acquire` request framed as one
|
|
332
404
|
* JSON line, and awaits the grant line against
|
|
333
405
|
* CONTROL_ACQUIRE_TIMEOUT_MS. Rejects (never throws synchronously) on any
|
|
334
406
|
* failure: broker.json absent/unreadable/missing the control fields, a
|
|
335
|
-
* connection error, an `error` response, or a timeout.
|
|
336
|
-
|
|
407
|
+
* connection error, an `error` response, or a timeout.
|
|
408
|
+
*
|
|
409
|
+
* Additionally takes an optional `profile` (see
|
|
410
|
+
* AcquireProfileOptions above). Omitting it writes the exact wire line this
|
|
411
|
+
* function has always written. */
|
|
412
|
+
export function acquireOverControlPlane(dir: string = brokerRootDir(), opts: AcquireProfileOptions = {}): Promise<AcquireOverControlPlaneHandle> {
|
|
337
413
|
return new Promise((resolvePromise, reject) => {
|
|
338
414
|
const broker = readJsonMaybe(brokerJsonPath(dir));
|
|
339
415
|
if (broker === null) {
|
|
@@ -369,7 +445,11 @@ export function acquireOverControlPlane(dir: string = brokerRootDir()): Promise<
|
|
|
369
445
|
|
|
370
446
|
socket.on("connect", () => {
|
|
371
447
|
const requestId = newRequestId();
|
|
372
|
-
|
|
448
|
+
// Write site ONE of two (see
|
|
449
|
+
// acquireProfileFragment()'s own comment) -- the key is absent entirely
|
|
450
|
+
// when no profile was requested, so this line stays byte-identical to
|
|
451
|
+
// what it always was for a profile-less acquire.
|
|
452
|
+
socket.write(`${JSON.stringify({ op: "acquire", id: requestId, token, ...acquireProfileFragment(opts.profile) })}\n`);
|
|
373
453
|
});
|
|
374
454
|
|
|
375
455
|
socket.on("data", (chunk: Buffer) => {
|
|
@@ -400,12 +480,14 @@ export function acquireOverControlPlane(dir: string = brokerRootDir()): Promise<
|
|
|
400
480
|
if (resp.kind === "grant") {
|
|
401
481
|
settled = true;
|
|
402
482
|
clearTimeout(timer);
|
|
483
|
+
const remoteMonitorPort = parseOptionalRemoteMonitorPort(resp.remote_monitor_port);
|
|
403
484
|
const grant: AcquireGrant = {
|
|
404
485
|
id: String(resp.id),
|
|
405
486
|
port: Number(resp.port),
|
|
406
487
|
url: String(resp.url),
|
|
407
488
|
epoch_file: String(resp.epoch_file),
|
|
408
489
|
supervisor_dir: String(resp.supervisor_dir),
|
|
490
|
+
...(remoteMonitorPort === undefined ? {} : { remote_monitor_port: remoteMonitorPort }),
|
|
409
491
|
};
|
|
410
492
|
resolvePromise({
|
|
411
493
|
grant,
|
|
@@ -433,16 +515,16 @@ export function acquireOverControlPlane(dir: string = brokerRootDir()): Promise<
|
|
|
433
515
|
}
|
|
434
516
|
|
|
435
517
|
// ---------------------------------------------------------------------------
|
|
436
|
-
// BROKER-CONTROL-CLIENT REGION START
|
|
518
|
+
// BROKER-CONTROL-CLIENT REGION START
|
|
437
519
|
//
|
|
438
|
-
//
|
|
439
|
-
// openBrokerControl() is the container-side half of
|
|
440
|
-
// all five request kinds, one discovery-record read, real
|
|
441
|
-
// deadlines, and a distinct broker-gone outcome. Lives alongside
|
|
442
|
-
// acquireOverControlPlane() above (
|
|
520
|
+
// The file protocol beside it is now gone.
|
|
521
|
+
// openBrokerControl() is the container-side half of the TCP control plane:
|
|
522
|
+
// session shape, all five request kinds, one discovery-record read, real
|
|
523
|
+
// per-request deadlines, and a distinct broker-gone outcome. Lives alongside
|
|
524
|
+
// acquireOverControlPlane() above (an early tracer, kept unchanged and
|
|
443
525
|
// still used by broker-e2e.test.ts/broker-kill.test.ts as their own one-shot
|
|
444
526
|
// acquire helper for exercising the SERVER side) -- the file protocol this
|
|
445
|
-
// region's own predecessor sat beside is gone
|
|
527
|
+
// region's own predecessor sat beside is gone entirely.
|
|
446
528
|
//
|
|
447
529
|
// Deliberately never REJECTS a promise: every failure -- deadline, a
|
|
448
530
|
// refused connection, a malformed line, the broker going away mid-request --
|
|
@@ -460,21 +542,19 @@ export function acquireOverControlPlane(dir: string = brokerRootDir()): Promise<
|
|
|
460
542
|
|
|
461
543
|
/** Same value as the tracer's own CONTROL_ACQUIRE_TIMEOUT_MS above --
|
|
462
544
|
* referenced directly (not re-computed from the env var a second time) so
|
|
463
|
-
* the two can never drift apart. This is
|
|
464
|
-
* retiring grant-poll timeout
|
|
465
|
-
*
|
|
466
|
-
* from 25000 per P-08 / 01.6.2.1-04-PLAN.md, against the measured tool-call
|
|
545
|
+
* the two can never drift apart. This is the relocated value of the
|
|
546
|
+
* retiring grant-poll timeout (VICE_BROKER_ACQUIRE_TIMEOUT_MS, default now
|
|
547
|
+
* 120000, raised from 25000 against the measured tool-call
|
|
467
548
|
* budget spike-003 established -- see the counter-evidence comment at
|
|
468
549
|
* CONTROL_ACQUIRE_TIMEOUT_MS's own declaration above). */
|
|
469
550
|
export const ACQUIRE_TIMEOUT_MS: number = CONTROL_ACQUIRE_TIMEOUT_MS;
|
|
470
551
|
|
|
471
|
-
/** The recycle bound.
|
|
472
|
-
* pollRecycleAck()'s own RECYCLE_ACK_TIMEOUT_MS directly, so the
|
|
473
|
-
* never drift apart while both existed; that predecessor is gone
|
|
474
|
-
*
|
|
475
|
-
* itself is unchanged (VICE_BROKER_RECYCLE_TIMEOUT_MS, default
|
|
476
|
-
*
|
|
477
|
-
* Phase 01.6.2.1's item. */
|
|
552
|
+
/** The recycle bound. An earlier revision referenced the (now-deleted)
|
|
553
|
+
* retiring pollRecycleAck()'s own RECYCLE_ACK_TIMEOUT_MS directly, so the
|
|
554
|
+
* two could never drift apart while both existed; that predecessor is gone
|
|
555
|
+
* entirely, so this reads the SAME environment variable directly -- the
|
|
556
|
+
* value itself is unchanged (VICE_BROKER_RECYCLE_TIMEOUT_MS, default
|
|
557
|
+
* 30000). Final tuning remains a future item. */
|
|
478
558
|
export const RECYCLE_TIMEOUT_MS: number = Number(process.env.VICE_BROKER_RECYCLE_TIMEOUT_MS || 30000);
|
|
479
559
|
|
|
480
560
|
/** Genuinely NEW: the file protocol never "connected" anywhere, so there is
|
|
@@ -482,9 +562,9 @@ export const RECYCLE_TIMEOUT_MS: number = Number(process.env.VICE_BROKER_RECYCLE
|
|
|
482
562
|
* a TCP connect over the docker bridge to a broker broker.json has already
|
|
483
563
|
* classified alive (never_started/stale are refused before a connection is
|
|
484
564
|
* ever attempted) -- deliberately not read from an environment variable,
|
|
485
|
-
* since
|
|
486
|
-
*
|
|
487
|
-
*
|
|
565
|
+
* since no new environment variable was wanted beyond the two deadline
|
|
566
|
+
* variables named above. Final tuning remains a future item, same as the
|
|
567
|
+
* other two. */
|
|
488
568
|
export const CONTROL_CONNECT_TIMEOUT_MS = 5000;
|
|
489
569
|
|
|
490
570
|
/** Every way a session-level request can fail to produce its expected
|
|
@@ -511,7 +591,7 @@ export type ControlFailureKind =
|
|
|
511
591
|
| "no_free_port"
|
|
512
592
|
| "at_capacity"
|
|
513
593
|
| "internal"
|
|
514
|
-
//
|
|
594
|
+
// The broker's own ControlErrorCode gained
|
|
515
595
|
// this member for the ownership-conflict outcome; duplicated here for the
|
|
516
596
|
// same reason every other member already is (this client and the broker
|
|
517
597
|
// run in separate processes -- the shared surface is the wire format, not
|
|
@@ -550,49 +630,67 @@ interface ControlHostStateFields {
|
|
|
550
630
|
warm_floor: number;
|
|
551
631
|
max_instances: number;
|
|
552
632
|
base_port: number;
|
|
553
|
-
/**
|
|
554
|
-
*
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
-
backend:
|
|
633
|
+
/** Narrowed from `"fork" | "stock" | null` to
|
|
634
|
+
* `ViceBackend | null` -- `null` when the broker predates this field or
|
|
635
|
+
* sent something unrecognised: absent evidence, kept distinct from a
|
|
636
|
+
* definite value. text-tools.ts's own broker-identity cross-check (out of
|
|
637
|
+
* this plan's scope) still reads this field. */
|
|
638
|
+
backend: ViceBackend | null;
|
|
559
639
|
}
|
|
560
640
|
|
|
561
641
|
export type ControlHostStateResult =
|
|
562
642
|
| { ok: true; hostState: ControlHostStateFields }
|
|
563
643
|
| { ok: false; kind: ControlFailureKind; message: string };
|
|
564
644
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
645
|
+
// ---------------------------------------------------------------------------
|
|
646
|
+
// MonitorClaimChannel: the two-value channel contract,
|
|
647
|
+
// declared HERE as a local literal union rather than imported from
|
|
648
|
+
// broker-state.mts -- that module is host-bound and compiled into
|
|
649
|
+
// resources/*.mjs, and this file is the container-side half. The shared
|
|
650
|
+
// thing between the declarations is the CONTRACT ("binary" | "text"), not
|
|
651
|
+
// the declaration itself -- channel-lock.ts's own MonitorChannel and
|
|
652
|
+
// broker-state.mts's own MonitorChannel each declare it separately for the
|
|
653
|
+
// same reason.
|
|
654
|
+
// ---------------------------------------------------------------------------
|
|
655
|
+
export type MonitorClaimChannel = "binary" | "text";
|
|
656
|
+
|
|
657
|
+
/** The current monitor-socket holder's own identity, named in a
|
|
658
|
+
* `monitor_owned` refusal -- field-for-field the same shape the broker's own MonitorHolder
|
|
659
|
+
* carries (broker-control.mts), minus nothing (pid included, matching
|
|
660
|
+
* GrantRecord's own convention this whole mechanism mirrors). */
|
|
570
661
|
export interface MonitorClaimHolder {
|
|
571
662
|
grantId: string;
|
|
572
663
|
claimedAt: number;
|
|
573
664
|
pid: number | null;
|
|
665
|
+
channel: MonitorClaimChannel;
|
|
574
666
|
}
|
|
575
667
|
|
|
576
668
|
export interface ClaimMonitorOptions {
|
|
577
669
|
targetId: string;
|
|
578
670
|
timeoutMs?: number;
|
|
671
|
+
/** Which monitor socket to claim. Omitted is
|
|
672
|
+
* byte-identical to `"binary"` -- every pre-existing call site (and every
|
|
673
|
+
* broker that predates this field) keeps working unchanged. */
|
|
674
|
+
channel?: MonitorClaimChannel;
|
|
579
675
|
}
|
|
580
676
|
|
|
581
677
|
export interface ReleaseMonitorOptions {
|
|
582
678
|
targetId: string;
|
|
583
679
|
timeoutMs?: number;
|
|
680
|
+
/** Same default-to-binary posture as ClaimMonitorOptions.channel. */
|
|
681
|
+
channel?: MonitorClaimChannel;
|
|
584
682
|
}
|
|
585
683
|
|
|
586
|
-
/** Discriminated claim outcome
|
|
684
|
+
/** Discriminated claim outcome: `monitor_owned` is kept STRICTLY
|
|
587
685
|
* separate from `timeout` -- conflating "someone else holds it" with "the
|
|
588
|
-
* broker did not answer" would reintroduce exactly the ambiguity
|
|
589
|
-
* exists to remove. Never thrown; a caller that wants to raise instead
|
|
686
|
+
* broker did not answer" would reintroduce exactly the ambiguity this
|
|
687
|
+
* distinction exists to remove. Never thrown; a caller that wants to raise instead
|
|
590
688
|
* should construct a MonitorOwnershipError from this outcome's own fields
|
|
591
689
|
* (see that class's own header comment). */
|
|
592
690
|
export type ClaimMonitorOutcome =
|
|
593
691
|
| { ok: true }
|
|
594
692
|
| { ok: false; reason: "monitor_owned"; holder: MonitorClaimHolder }
|
|
595
|
-
// "denied"
|
|
693
|
+
// "denied": the broker's control plane refused because the grant
|
|
596
694
|
// named is not the one THIS connection holds. In a correct client that is
|
|
597
695
|
// unreachable -- stockConnect() always claims the grant its own session
|
|
598
696
|
// acquired -- so it is carried as its own reason rather than collapsed into
|
|
@@ -607,12 +705,15 @@ export interface MonitorOwnershipErrorOptions {
|
|
|
607
705
|
holderGrantId?: string;
|
|
608
706
|
holderClaimedAt?: number;
|
|
609
707
|
port?: number;
|
|
708
|
+
/** Which socket is contended -- so a handshake failure
|
|
709
|
+
* can say which channel was refused without re-parsing the message. */
|
|
710
|
+
channel?: MonitorClaimChannel;
|
|
610
711
|
}
|
|
611
712
|
|
|
612
713
|
/** Thrown (by a caller that prefers to raise rather than branch on
|
|
613
714
|
* ClaimMonitorOutcome) when `monitor_claim` is refused because a DIFFERENT
|
|
614
|
-
* grant already holds this instance's monitor socket
|
|
615
|
-
*
|
|
715
|
+
* grant already holds this instance's monitor socket. Names the holding
|
|
716
|
+
* grant and the port plainly, as an ownership
|
|
616
717
|
* conflict -- a state the broker itself enforced, distinct from an emulator
|
|
617
718
|
* that has stopped answering, and NOT a state the vice-wedge-triage skill's
|
|
618
719
|
* opening move should ever be misdirected by.
|
|
@@ -621,20 +722,22 @@ export interface MonitorOwnershipErrorOptions {
|
|
|
621
722
|
* connect() is ever attempted: stock VICE services exactly one binmon
|
|
622
723
|
* client, and a second connect() produces no reply and no EOF, so a refusal
|
|
623
724
|
* arriving only after dialling would be byte-for-byte indistinguishable
|
|
624
|
-
* from a wedge
|
|
725
|
+
* from a wedge. Claiming first means this refusal is a JSON
|
|
625
726
|
* response on a control-plane socket that already works, and the second
|
|
626
|
-
* client never dials the binmon port at all
|
|
727
|
+
* client never dials the binmon port at all. */
|
|
627
728
|
export class MonitorOwnershipError extends ViceError {
|
|
628
729
|
holderGrantId?: string;
|
|
629
730
|
holderClaimedAt?: number;
|
|
630
731
|
port?: number;
|
|
732
|
+
channel?: MonitorClaimChannel;
|
|
631
733
|
|
|
632
|
-
constructor(message: string, { holderGrantId, holderClaimedAt, port }: MonitorOwnershipErrorOptions = {}) {
|
|
734
|
+
constructor(message: string, { holderGrantId, holderClaimedAt, port, channel }: MonitorOwnershipErrorOptions = {}) {
|
|
633
735
|
super(message);
|
|
634
736
|
this.name = "MonitorOwnershipError";
|
|
635
737
|
this.holderGrantId = holderGrantId;
|
|
636
738
|
this.holderClaimedAt = holderClaimedAt;
|
|
637
739
|
this.port = port;
|
|
740
|
+
this.channel = channel;
|
|
638
741
|
}
|
|
639
742
|
}
|
|
640
743
|
|
|
@@ -654,13 +757,16 @@ export interface ControlDeadlineOptions {
|
|
|
654
757
|
* exactly one request line and resolves against its own deadline; none of
|
|
655
758
|
* them ever reject. */
|
|
656
759
|
export interface BrokerControlSession {
|
|
657
|
-
|
|
760
|
+
/** Additionally takes an optional
|
|
761
|
+
* `profile` -- see AcquireProfileOptions. Omitting it is byte-identical to
|
|
762
|
+
* every pre-existing call. */
|
|
763
|
+
acquire(opts?: ControlDeadlineOptions & AcquireProfileOptions): Promise<ControlAcquireResult>;
|
|
658
764
|
release(): Promise<ControlReleaseResult>;
|
|
659
765
|
recycle(targetId: string, opts?: ControlDeadlineOptions): Promise<ControlRecycleResult>;
|
|
660
766
|
status(opts?: ControlDeadlineOptions): Promise<ControlStatusResult>;
|
|
661
767
|
hostState(opts?: ControlDeadlineOptions): Promise<ControlHostStateResult>;
|
|
662
768
|
/** Claims exclusive ownership of an instance's monitor socket BEFORE any
|
|
663
|
-
* binmon connect() is attempted
|
|
769
|
+
* binmon connect() is attempted -- see
|
|
664
770
|
* MonitorOwnershipError's own header comment for why claiming first is
|
|
665
771
|
* the only way this refusal can ever be distinguishable from a wedge. */
|
|
666
772
|
claimMonitor(opts: ClaimMonitorOptions): Promise<ClaimMonitorOutcome>;
|
|
@@ -680,7 +786,7 @@ export type OpenBrokerControlOutcome =
|
|
|
680
786
|
|
|
681
787
|
/** The backend-agnostic coordinate set a session which ALREADY holds a
|
|
682
788
|
* broker grant hands to anything that needs to dial the instance that grant
|
|
683
|
-
* names
|
|
789
|
+
* names. Declared here, beside
|
|
684
790
|
* BrokerControlSession and openBrokerControl(), because it is
|
|
685
791
|
* backend-agnostic -- the fork path does not consume it only because
|
|
686
792
|
* forwardToVice() reads activeInstance() from the same module (vice.ts)
|
|
@@ -698,7 +804,7 @@ export interface HeldLease {
|
|
|
698
804
|
port: number;
|
|
699
805
|
targetId: string;
|
|
700
806
|
brokerControl: BrokerControlSession;
|
|
701
|
-
/**
|
|
807
|
+
/** THIS instance's own epoch.json, in the CONSUMER's view of the
|
|
702
808
|
* filesystem (i.e. already containerized -- vice-proxy.ts fills it from
|
|
703
809
|
* activeInstance().epochFile, which adoptGrant() set from the containerized
|
|
704
810
|
* grant). This is the reconnect-identity baseline stock-connect.ts's
|
|
@@ -709,7 +815,7 @@ export interface HeldLease {
|
|
|
709
815
|
* Empty string means genuinely no epoch evidence exists, which is that same
|
|
710
816
|
* unprovable case stated explicitly rather than by omission. */
|
|
711
817
|
epochFile: string;
|
|
712
|
-
/**
|
|
818
|
+
/** The TOP-LEVEL supervisor directory -- the one holding
|
|
713
819
|
* `backend.json`, i.e. the same directory `broker.json` is read from
|
|
714
820
|
* (brokerRootDir()). Deliberately NOT the grant's own per-instance
|
|
715
821
|
* `supervisor_dir` (`<stateDir>/<port>`), which holds epoch.json and would
|
|
@@ -719,6 +825,21 @@ export interface HeldLease {
|
|
|
719
825
|
* backend-detect.mts's own documented degradation for an omitted
|
|
720
826
|
* supervisorDir. */
|
|
721
827
|
supervisorDir: string;
|
|
828
|
+
/** THIS
|
|
829
|
+
* instance's own text-monitor port, read by text-connect.ts's
|
|
830
|
+
* textConnect() to dial the `-remotemonitor` channel. MANDATORY on a
|
|
831
|
+
* stock grant, ABSENT on a fork grant -- the fork never launches with
|
|
832
|
+
* `-remotemonitor` and advertises no text tools. Its absence on a stock
|
|
833
|
+
* lease is a real defect, not a tolerated state (mirrors epochFile's own
|
|
834
|
+
* "NOT optional" discipline above): the mechanism that makes this true is
|
|
835
|
+
* broker-launch.mts's acquirePortAndLaunch(), which now FAILS THE WHOLE
|
|
836
|
+
* ACQUIRE when the text-port allocation fails (`no_free_text_port`)
|
|
837
|
+
* rather than degrading to a portless launch -- there is no longer a code
|
|
838
|
+
* path that produces a stock grant, and therefore a HeldLease, without
|
|
839
|
+
* this field. Its optionality here is a transitional TypeScript
|
|
840
|
+
* convenience only (the fork case is real), never a semantic "sometimes
|
|
841
|
+
* missing on stock". */
|
|
842
|
+
remoteMonitorPort?: number;
|
|
722
843
|
}
|
|
723
844
|
|
|
724
845
|
/** One in-flight request's settlement callback -- pushed onto the session's
|
|
@@ -745,13 +866,18 @@ type RawLineOutcome = { ok: true; line: Record<string, unknown> } | { ok: false;
|
|
|
745
866
|
|
|
746
867
|
/** Never-throw extraction of a `holder` payload from untrusted wire input --
|
|
747
868
|
* absent or malformed input answers `undefined`, never a partially-filled
|
|
748
|
-
* object (
|
|
749
|
-
* this file's own header comment on broker.json reads).
|
|
750
|
-
|
|
869
|
+
* object (this module's own never-throw-on-untrusted-input posture, matching
|
|
870
|
+
* this file's own header comment on broker.json reads). `channel`
|
|
871
|
+
* defaults to `requestedChannel` -- THE channel this request
|
|
872
|
+
* itself named -- when the wire omits it or sends something unrecognised;
|
|
873
|
+
* never fabricated as a plausible value, in the same register the
|
|
874
|
+
* `grantId: "unknown"` fallback one layer up (claimMonitor()'s own) uses. */
|
|
875
|
+
function extractHolder(raw: unknown, requestedChannel: MonitorClaimChannel): MonitorClaimHolder | undefined {
|
|
751
876
|
if (typeof raw !== "object" || raw === null) return undefined;
|
|
752
877
|
const h = raw as Record<string, unknown>;
|
|
753
878
|
if (typeof h.grantId !== "string" || typeof h.claimedAt !== "number") return undefined;
|
|
754
|
-
|
|
879
|
+
const channel = h.channel === "text" || h.channel === "binary" ? h.channel : requestedChannel;
|
|
880
|
+
return { grantId: h.grantId, claimedAt: h.claimedAt, pid: typeof h.pid === "number" ? h.pid : null, channel };
|
|
755
881
|
}
|
|
756
882
|
|
|
757
883
|
/** Builds the session object wrapping an already-CONNECTED socket. Wires the
|
|
@@ -832,11 +958,16 @@ function createSession(socket: Socket, token: string): BrokerControlSession {
|
|
|
832
958
|
}
|
|
833
959
|
if (line.kind === "error") {
|
|
834
960
|
const code = typeof line.code === "string" ? (line.code as ControlFailureKind) : "internal";
|
|
835
|
-
//
|
|
961
|
+
// Forward `holder` verbatim ONLY for monitor_owned --
|
|
836
962
|
// every other error code carries no such field on the wire, and
|
|
837
963
|
// extractHolder() itself never invents one from absent/malformed
|
|
838
|
-
// input.
|
|
839
|
-
|
|
964
|
+
// input. The requested channel comes from
|
|
965
|
+
// THIS payload (the request this response answers), read from
|
|
966
|
+
// the same closure `payload` sendAndAwaitLine() was called
|
|
967
|
+
// with -- an absent/malformed wire `channel` is never fabricated,
|
|
968
|
+
// it defaults to the channel this specific request itself named.
|
|
969
|
+
const requestedChannel: MonitorClaimChannel = payload.channel === "text" ? "text" : "binary";
|
|
970
|
+
const holder = code === "monitor_owned" ? extractHolder(line.holder, requestedChannel) : undefined;
|
|
840
971
|
resolvePromise({
|
|
841
972
|
ok: false,
|
|
842
973
|
kind: code,
|
|
@@ -862,20 +993,29 @@ function createSession(socket: Socket, token: string): BrokerControlSession {
|
|
|
862
993
|
});
|
|
863
994
|
}
|
|
864
995
|
|
|
865
|
-
async function acquire(opts: ControlDeadlineOptions = {}): Promise<ControlAcquireResult> {
|
|
996
|
+
async function acquire(opts: ControlDeadlineOptions & AcquireProfileOptions = {}): Promise<ControlAcquireResult> {
|
|
866
997
|
const requestId = newRequestId();
|
|
867
|
-
|
|
998
|
+
// Write site TWO of two (see
|
|
999
|
+
// acquireProfileFragment()'s own comment for why both matter) -- same
|
|
1000
|
+
// key-omitted-when-absent discipline as acquireOverControlPlane()'s raw
|
|
1001
|
+
// socket.write above.
|
|
1002
|
+
const raw = await sendAndAwaitLine(
|
|
1003
|
+
{ op: "acquire", id: requestId, token, ...acquireProfileFragment(opts.profile) },
|
|
1004
|
+
opts.timeoutMs ?? ACQUIRE_TIMEOUT_MS,
|
|
1005
|
+
);
|
|
868
1006
|
if (!raw.ok) return raw;
|
|
869
1007
|
const line = raw.line;
|
|
870
1008
|
if (line.kind !== "grant") {
|
|
871
1009
|
return { ok: false, kind: "protocol", message: `openBrokerControl: acquire got unexpected response kind ${String(line.kind)}` };
|
|
872
1010
|
}
|
|
1011
|
+
const remoteMonitorPort = parseOptionalRemoteMonitorPort(line.remote_monitor_port);
|
|
873
1012
|
const grant: AcquireGrant = {
|
|
874
1013
|
id: String(line.id),
|
|
875
1014
|
port: Number(line.port),
|
|
876
1015
|
url: String(line.url),
|
|
877
1016
|
epoch_file: String(line.epoch_file),
|
|
878
1017
|
supervisor_dir: String(line.supervisor_dir),
|
|
1018
|
+
...(remoteMonitorPort === undefined ? {} : { remote_monitor_port: remoteMonitorPort }),
|
|
879
1019
|
};
|
|
880
1020
|
return { ok: true, grant };
|
|
881
1021
|
}
|
|
@@ -918,7 +1058,7 @@ function createSession(socket: Socket, token: string): BrokerControlSession {
|
|
|
918
1058
|
// in-memory read on the broker side (no launch, no kill involved), so it
|
|
919
1059
|
// needs no timeout of its own scale; introducing a distinct constant (or
|
|
920
1060
|
// environment variable) for it would be exactly the kind of new knob
|
|
921
|
-
//
|
|
1061
|
+
// this module deliberately declines to add.
|
|
922
1062
|
const raw = await sendAndAwaitLine({ op: "status", token }, opts.timeoutMs ?? ACQUIRE_TIMEOUT_MS);
|
|
923
1063
|
if (!raw.ok) return raw;
|
|
924
1064
|
const line = raw.line;
|
|
@@ -957,30 +1097,33 @@ function createSession(socket: Socket, token: string): BrokerControlSession {
|
|
|
957
1097
|
warm_floor: Number(line.warm_floor),
|
|
958
1098
|
max_instances: Number(line.max_instances),
|
|
959
1099
|
base_port: Number(line.base_port),
|
|
960
|
-
//
|
|
961
|
-
//
|
|
962
|
-
// which callers must treat as absent
|
|
963
|
-
|
|
1100
|
+
// Narrowed at the boundary, never cast --
|
|
1101
|
+
// anything other than the one known verdict reads as `null` ("this
|
|
1102
|
+
// broker did not tell us"), which callers must treat as absent
|
|
1103
|
+
// evidence rather than agreement.
|
|
1104
|
+
backend: line.backend === "stock" ? line.backend : null,
|
|
964
1105
|
},
|
|
965
1106
|
};
|
|
966
1107
|
}
|
|
967
1108
|
|
|
968
1109
|
/** Claims exclusive ownership of `opts.targetId`'s monitor socket, sending
|
|
969
|
-
* `{ op: "monitor_claim", id, target_id, token }` through the SAME
|
|
1110
|
+
* `{ op: "monitor_claim", id, target_id, channel, token }` through the SAME
|
|
970
1111
|
* `sendAndAwaitLine()` path -- the same session, the same token, the same
|
|
971
1112
|
* newline-delimited JSON discipline every other op uses; no second
|
|
972
1113
|
* control connection is ever opened, and this function never dials the
|
|
973
|
-
* binmon port itself, on success OR on failure
|
|
974
|
-
*
|
|
975
|
-
* made BEFORE any binmon connect()
|
|
1114
|
+
* binmon port itself, on success OR on failure -- see
|
|
1115
|
+
* MonitorOwnershipError's own header comment for why the claim is
|
|
1116
|
+
* made BEFORE any binmon connect(). `timeout` is reported distinctly
|
|
976
1117
|
* from `monitor_owned`: a timeout means the broker did not answer, never
|
|
977
|
-
* that someone else owns the socket.
|
|
1118
|
+
* that someone else owns the socket. `channel` defaults
|
|
1119
|
+
* to `"binary"` when omitted -- byte-identical to every pre-existing call. */
|
|
978
1120
|
async function claimMonitor(opts: ClaimMonitorOptions): Promise<ClaimMonitorOutcome> {
|
|
979
1121
|
const requestId = newRequestId();
|
|
980
|
-
const
|
|
1122
|
+
const channel: MonitorClaimChannel = opts.channel ?? "binary";
|
|
1123
|
+
const raw = await sendAndAwaitLine({ op: "monitor_claim", id: requestId, target_id: opts.targetId, channel, token }, opts.timeoutMs ?? ACQUIRE_TIMEOUT_MS);
|
|
981
1124
|
if (!raw.ok) {
|
|
982
1125
|
if (raw.kind === "deadline") return { ok: false, reason: "timeout" };
|
|
983
|
-
//
|
|
1126
|
+
// The `monitor_owned` REASON survives even when the wire's own
|
|
984
1127
|
// `holder` payload is absent or malformed. This used to be
|
|
985
1128
|
// `raw.kind === "monitor_owned" && raw.holder`, so a partially-malformed
|
|
986
1129
|
// refusal collapsed to `{ ok: false, reason: "internal" }` -- stockConnect()
|
|
@@ -989,11 +1132,11 @@ function createSession(socket: Socket, token: string): BrokerControlSession {
|
|
|
989
1132
|
// requires (and MonitorOwnershipError exists to preserve) was lost. The
|
|
990
1133
|
// broker has told us WHICH state this is; not being able to name the
|
|
991
1134
|
// holder does not make it a different state. Holder fields default to
|
|
992
|
-
// "unknown"/0/null so the wording still reads as an ownership
|
|
993
|
-
// rather than an emulator fault -- never fabricated as a
|
|
994
|
-
// id, which would be worse than admitting it is unknown.
|
|
1135
|
+
// "unknown"/0/null/`channel` so the wording still reads as an ownership
|
|
1136
|
+
// conflict rather than an emulator fault -- never fabricated as a
|
|
1137
|
+
// plausible grant id, which would be worse than admitting it is unknown.
|
|
995
1138
|
if (raw.kind === "monitor_owned") {
|
|
996
|
-
return { ok: false, reason: "monitor_owned", holder: raw.holder ?? { grantId: "unknown", claimedAt: 0, pid: null } };
|
|
1139
|
+
return { ok: false, reason: "monitor_owned", holder: raw.holder ?? { grantId: "unknown", claimedAt: 0, pid: null, channel } };
|
|
997
1140
|
}
|
|
998
1141
|
if (raw.kind === "unauthorized" || raw.kind === "bad_request" || raw.kind === "denied") return { ok: false, reason: raw.kind };
|
|
999
1142
|
return { ok: false, reason: "internal" };
|
|
@@ -1005,14 +1148,16 @@ function createSession(socket: Socket, token: string): BrokerControlSession {
|
|
|
1005
1148
|
}
|
|
1006
1149
|
|
|
1007
1150
|
/** Releases a previously claimed monitor socket, sending
|
|
1008
|
-
* `{ op: "monitor_release", id, target_id, token }` over the SAME
|
|
1151
|
+
* `{ op: "monitor_release", id, target_id, channel, token }` over the SAME
|
|
1009
1152
|
* session. Tolerates a broker that has already cleared the record (the
|
|
1010
1153
|
* broker's own onMonitorRelease answers `ok: true` for an already-cleared
|
|
1011
1154
|
* target) -- this function never retries and never opens a second
|
|
1012
|
-
* connection.
|
|
1155
|
+
* connection. `channel` defaults to `"binary"` when
|
|
1156
|
+
* omitted. */
|
|
1013
1157
|
async function releaseMonitor(opts: ReleaseMonitorOptions): Promise<ReleaseMonitorOutcome> {
|
|
1014
1158
|
const requestId = newRequestId();
|
|
1015
|
-
const
|
|
1159
|
+
const channel: MonitorClaimChannel = opts.channel ?? "binary";
|
|
1160
|
+
const raw = await sendAndAwaitLine({ op: "monitor_release", id: requestId, target_id: opts.targetId, channel, token }, opts.timeoutMs ?? ACQUIRE_TIMEOUT_MS);
|
|
1016
1161
|
if (!raw.ok) {
|
|
1017
1162
|
if (raw.kind === "deadline") return { ok: false, reason: "timeout" };
|
|
1018
1163
|
if (raw.kind === "unauthorized" || raw.kind === "bad_request" || raw.kind === "denied") return { ok: false, reason: raw.kind };
|
|
@@ -1121,5 +1266,5 @@ export function openBrokerControl(dir: string = brokerRootDir(), opts: OpenBroke
|
|
|
1121
1266
|
}
|
|
1122
1267
|
|
|
1123
1268
|
// ---------------------------------------------------------------------------
|
|
1124
|
-
// BROKER-CONTROL-CLIENT REGION END
|
|
1269
|
+
// BROKER-CONTROL-CLIENT REGION END
|
|
1125
1270
|
// ---------------------------------------------------------------------------
|