@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.
Files changed (84) hide show
  1. package/README.md +2 -2
  2. package/THIRD-PARTY-NOTICES.md +422 -1
  3. package/anno-bank.ts +171 -0
  4. package/anno-cli.ts +1736 -163
  5. package/anno-confidence.ts +2 -2
  6. package/anno-derive.ts +6 -6
  7. package/anno-details.ts +4 -4
  8. package/anno-enum-gen.ts +416 -30
  9. package/anno-export-asm.ts +1211 -126
  10. package/anno-graphics.ts +338 -0
  11. package/anno-hazard-report.ts +1367 -0
  12. package/anno-import.ts +495 -0
  13. package/anno-index.ts +8 -8
  14. package/anno-join.ts +480 -0
  15. package/anno-memmap-render.ts +22 -21
  16. package/anno-provenance-ledger.ts +472 -0
  17. package/anno-regbits-gen.ts +13 -13
  18. package/anno-register.ts +159 -0
  19. package/anno-store-export.ts +661 -0
  20. package/anno-store.ts +635 -124
  21. package/anno-symbols.ts +7 -7
  22. package/anno-tools.ts +1169 -16
  23. package/anno-types.ts +313 -40
  24. package/backend-detect.mts +124 -312
  25. package/build.ts +3 -1
  26. package/capture-predicate.ts +597 -0
  27. package/channel-lock.ts +349 -0
  28. package/evid-ingest.ts +217 -0
  29. package/evid-reconcile.ts +316 -0
  30. package/host-tool-client.ts +430 -0
  31. package/incident-record.ts +23 -12
  32. package/install-resources.ts +29 -13
  33. package/memmap-lookup.ts +285 -0
  34. package/package.json +27 -8
  35. package/prg-image.ts +1 -2
  36. package/repo-root.ts +87 -3
  37. package/resources/backend-detect.mjs +98 -236
  38. package/resources/broker-control.mjs +220 -54
  39. package/resources/broker-epoch.mjs +7 -8
  40. package/resources/broker-kill.mjs +36 -31
  41. package/resources/broker-launch.mjs +511 -374
  42. package/resources/broker-state.mjs +69 -24
  43. package/resources/container-guard.mjs +1 -1
  44. package/resources/ghidra-project.mjs +790 -0
  45. package/resources/host-tool.mjs +2533 -0
  46. package/resources/vice-broker.mjs +434 -290
  47. package/resources/vice-launcher.sh +127 -9
  48. package/stock-address.ts +1 -1
  49. package/stock-condition.ts +1 -1
  50. package/stock-connect.ts +9 -5
  51. package/stock-derived.ts +29 -37
  52. package/stock-diagnose.ts +200 -36
  53. package/stock-dispatch.ts +179 -77
  54. package/stock-handler.ts +1 -1
  55. package/stock-paths.ts +18 -14
  56. package/stock-petscii.ts +1 -1
  57. package/stock-protocol.ts +1 -1
  58. package/stock-recycle.ts +83 -2
  59. package/stock-reproducible-run.ts +811 -0
  60. package/stock-run-until.ts +100 -1
  61. package/stock-symbols.ts +4 -4
  62. package/stock-timing.ts +1 -1
  63. package/stop-oracle.ts +167 -0
  64. package/text-capability-probe.ts +660 -0
  65. package/text-connect.ts +157 -0
  66. package/text-protocol.ts +810 -0
  67. package/text-tools.ts +778 -0
  68. package/textmon-backtrace.ts +385 -0
  69. package/textmon-cpuhistory.ts +335 -0
  70. package/textmon-memmap.ts +494 -0
  71. package/textmon-profile.ts +458 -0
  72. package/textmon-registers.ts +748 -0
  73. package/tools-manifest.stock.json +864 -3
  74. package/vice-broker-client.ts +253 -108
  75. package/vice-errors.ts +268 -0
  76. package/vice-proxy.ts +339 -2144
  77. package/vsf-slice.ts +640 -0
  78. package/anno-d64.ts +0 -310
  79. package/capability-registry.ts +0 -390
  80. package/refresh-manifest.ts +0 -124
  81. package/tools-manifest.json +0 -1223
  82. package/vice-probe.ts +0 -278
  83. package/vice-sync.ts +0 -336
  84. package/vice.ts +0 -772
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- // Container-side half of the on-demand broker protocol. Through Phase 01.2
3
- // this module wrote the request/lease files resources/vice-broker.sh read
4
- // and read the grant/denial/broker files that script wrote, all on the SAME
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. Plan 01.6.2-07 deletes that file protocol wholesale (D-12: six
7
- // mechanisms retiring together -- startHeartbeat()/the mtime-as-heartbeat
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. See 01.2-PATTERNS.md's "Never-throw /
21
- // never-cache-a-negative-result" section.
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 five production modules (containerpath.ts, install-resources.ts,
25
- // stock-paths.ts, vice-proxy.ts, vice-sync.ts), pinned by
26
- // hostpath-consumers.test.ts, and host-path message text stays in
27
- // vice-proxy.ts, which is already on that list.
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 (assumption-delta decision, 01.2-01-PLAN.md):
52
- // a request/grant/lease is identified by this id, never by port -- ports are
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
- // C7 (Phase 01.6.1): this is the criterion's whole container-side
61
- // deliverable -- a real, typed, NAMED export whose VALUE is unchanged from
62
- // the pre-conversion .mjs (verified live, this plan's SUMMARY quotes both).
63
- // 01.6.2's in-process broker imports this exact binding rather than
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 under D-01/D-12; only brokerJsonPath() below
86
- // survives, since broker.json itself is not part of the retiring protocol.
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 (D-01/D-12) is replaced
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 D-12's six retiring mechanisms) has no
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 plan 01's blocking
295
- // checkpoint:decision (2026-08-03, `as-specified`; see
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
- /** P-08 (01.6.2.1-04-PLAN.md): default raised from 25000 to 120000. The
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
- export function acquireOverControlPlane(dir: string = brokerRootDir()): Promise<AcquireOverControlPlaneHandle> {
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
- socket.write(`${JSON.stringify({ op: "acquire", id: requestId, token })}\n`);
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 (plan 06, task 1)
518
+ // BROKER-CONTROL-CLIENT REGION START
437
519
  //
438
- // Completed by plan 07 (the file protocol beside it is now gone).
439
- // openBrokerControl() is the container-side half of D-01: session shape,
440
- // all five request kinds, one discovery-record read, real per-request
441
- // deadlines, and a distinct broker-gone outcome. Lives alongside
442
- // acquireOverControlPlane() above (plan 01's tracer, kept unchanged and
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 (plan 07, D-12).
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 "the relocated value of the
464
- * retiring grant-poll timeout" per 01.6.2-01-PLAN.md's own environment
465
- * variable table (VICE_BROKER_ACQUIRE_TIMEOUT_MS, default now 120000, raised
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. Plan 06 referenced the (now-deleted) retiring
472
- * pollRecycleAck()'s own RECYCLE_ACK_TIMEOUT_MS directly, so the two could
473
- * never drift apart while both existed; that predecessor is gone (plan 07,
474
- * D-12), so this reads the SAME environment variable directly -- the value
475
- * itself is unchanged (VICE_BROKER_RECYCLE_TIMEOUT_MS, default 30000, per
476
- * 01.6.2-01-PLAN.md's own environment variable table). Final tuning is
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 01.6.2-06-PLAN.md's own scope is "no new environment variables
486
- * beyond the two deadline variables named in 01.6.2-01-PLAN.md" (the two
487
- * above). Final tuning is Phase 01.6.2.1's item, same as the other two. */
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
- // Plan 05 (BROK-02/PROTO-08): the broker's own ControlErrorCode gained
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
- /** WR-04: the backend verdict the BROKER resolved -- the authoritative one,
554
- * since it is what decided the emulator's launch argv. `null` when the broker
555
- * predates this field or sent something unrecognised: absent evidence, kept
556
- * strictly distinct from a definite disagreement, so a mismatch check can
557
- * refuse only on the latter. */
558
- backend: "fork" | "stock" | null;
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
- /** Plan 05 (BROK-02/PROTO-08, D-13): the current monitor-socket holder's own
566
- * identity, named in a `monitor_owned` refusal -- field-for-field the same
567
- * shape the broker's own MonitorHolder carries (broker-control.mts), minus
568
- * nothing (pid included, matching GrantRecord's own convention this whole
569
- * mechanism mirrors). */
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 (plan 05): `monitor_owned` is kept STRICTLY
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 PROTO-08
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" (CR-03): the broker's control plane refused because the grant
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 (plan 05, PROTO-08,
615
- * D-13). Names the holding grant and the port plainly, as an ownership
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 (PROTO-08). Claiming first means this refusal is a JSON
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 (D-13). */
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
- acquire(opts?: ControlDeadlineOptions): Promise<ControlAcquireResult>;
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 (plan 05, PROTO-08, D-13) -- see
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 (plan 02-09, PROTO-08, D-13). Declared here, beside
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
- /** CR-06: THIS instance's own epoch.json, in the CONSUMER's view of the
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
- /** CR-06: the TOP-LEVEL supervisor directory -- the one holding
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 (plan 05's own never-throw-on-untrusted-input posture, matching
749
- * this file's own header comment on broker.json reads). */
750
- function extractHolder(raw: unknown): MonitorClaimHolder | undefined {
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
- return { grantId: h.grantId, claimedAt: h.claimedAt, pid: typeof h.pid === "number" ? h.pid : null };
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
- // Plan 05: forward `holder` verbatim ONLY for monitor_owned --
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
- const holder = code === "monitor_owned" ? extractHolder(line.holder) : undefined;
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
- const raw = await sendAndAwaitLine({ op: "acquire", id: requestId, token }, opts.timeoutMs ?? ACQUIRE_TIMEOUT_MS);
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
- // 01.6.2-06-PLAN.md's own scope excludes.
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
- // WR-04: narrowed at the boundary, never cast -- anything other than the
961
- // two known verdicts reads as `null` ("this broker did not tell us"),
962
- // which callers must treat as absent evidence rather than agreement.
963
- backend: line.backend === "fork" || line.backend === "stock" ? line.backend : null,
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 (plan 05, PROTO-08, D-13
974
- * -- see MonitorOwnershipError's own header comment for why the claim is
975
- * made BEFORE any binmon connect()). `timeout` is reported distinctly
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 raw = await sendAndAwaitLine({ op: "monitor_claim", id: requestId, target_id: opts.targetId, token }, opts.timeoutMs ?? ACQUIRE_TIMEOUT_MS);
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
- // WR-08: the `monitor_owned` REASON survives even when the wire's own
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 conflict
993
- // rather than an emulator fault -- never fabricated as a plausible grant
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 raw = await sendAndAwaitLine({ op: "monitor_release", id: requestId, target_id: opts.targetId, token }, opts.timeoutMs ?? ACQUIRE_TIMEOUT_MS);
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 (plan 06, task 1)
1269
+ // BROKER-CONTROL-CLIENT REGION END
1125
1270
  // ---------------------------------------------------------------------------