instar 1.3.92 → 1.3.94
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/dist/commands/machine.d.ts.map +1 -1
- package/dist/commands/machine.js +8 -0
- package/dist/commands/machine.js.map +1 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +146 -0
- package/dist/commands/server.js.map +1 -1
- package/dist/core/MeshUrlAdvertiser.d.ts +50 -0
- package/dist/core/MeshUrlAdvertiser.d.ts.map +1 -0
- package/dist/core/MeshUrlAdvertiser.js +64 -0
- package/dist/core/MeshUrlAdvertiser.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.92.md +14 -50
- package/upgrades/1.3.93.md +28 -0
- package/upgrades/1.3.94.md +38 -0
- package/upgrades/side-effects/mesh-url-advertisement.md +66 -0
- package/upgrades/side-effects/session-pool-activation-wiring.md +22 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MeshUrlAdvertiser — populate a machine's `lastKnownUrl` so cross-machine
|
|
3
|
+
* routing can actually reach it.
|
|
4
|
+
*
|
|
5
|
+
* THE BUG THIS FIXES (found via real-hardware dogfooding, 2026-05-29):
|
|
6
|
+
* the Multi-Machine Session Pool routes every cross-machine RPC (deliver /
|
|
7
|
+
* transfer / lease) to a peer by reading `registry.machines[id].lastKnownUrl`.
|
|
8
|
+
* But the only writer of that field — `MachineIdentityManager.updateMachineUrl`
|
|
9
|
+
* — had ZERO callers. So in production every peer's URL was null, the peer
|
|
10
|
+
* filters (`!!e.lastKnownUrl`) dropped every peer, and no session could ever be
|
|
11
|
+
* delivered or transferred across machines. The 3-tier tests missed it because
|
|
12
|
+
* they inject peers with mock URLs and never exercise the runtime "where does
|
|
13
|
+
* lastKnownUrl come from" path.
|
|
14
|
+
*
|
|
15
|
+
* A machine's reachable URL is its tunnel URL: a named tunnel's hostname is
|
|
16
|
+
* known from config; a quick tunnel's URL is only known once `tunnel.start()`
|
|
17
|
+
* resolves it. This module turns either into the value to advertise, and writes
|
|
18
|
+
* it into the registry entry — from where the existing git-backed registry sync
|
|
19
|
+
* (RegistrySyncDebouncer) propagates it to peers.
|
|
20
|
+
*/
|
|
21
|
+
export interface MeshTunnelConfig {
|
|
22
|
+
enabled?: boolean;
|
|
23
|
+
type?: string;
|
|
24
|
+
hostname?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the URL this machine should advertise to the mesh.
|
|
28
|
+
*
|
|
29
|
+
* Preference order:
|
|
30
|
+
* 1. A concretely-resolved tunnel URL (quick tunnels only know it at runtime;
|
|
31
|
+
* `tunnel.start()` returns it). This is authoritative when present.
|
|
32
|
+
* 2. A named tunnel's deterministic `https://<hostname>`.
|
|
33
|
+
* 3. null — no reachable URL (tunnel disabled / not configured). A machine
|
|
34
|
+
* with no tunnel is genuinely not reachable cross-machine; advertising
|
|
35
|
+
* nothing is correct (peers will skip it rather than route to a dead URL).
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveAdvertisedMeshUrl(tunnel: MeshTunnelConfig | undefined, resolvedTunnelUrl?: string | null): string | null;
|
|
38
|
+
/** Minimal slice of MachineIdentityManager this module needs (keeps it testable). */
|
|
39
|
+
export interface MeshUrlRecorder {
|
|
40
|
+
getMachineUrl(machineId: string): string | null;
|
|
41
|
+
updateMachineUrl(machineId: string, url: string): void;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Advertise this machine's reachable URL into its own registry entry so peers
|
|
45
|
+
* can route to it. Idempotent (no write when unchanged), and tolerant when the
|
|
46
|
+
* self entry isn't present yet (best-effort — boot ordering can race the
|
|
47
|
+
* registration). Returns true iff it wrote a new value.
|
|
48
|
+
*/
|
|
49
|
+
export declare function advertiseSelfMeshUrl(recorder: MeshUrlRecorder, selfMachineId: string, url: string | null, log?: (msg: string) => void): boolean;
|
|
50
|
+
//# sourceMappingURL=MeshUrlAdvertiser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MeshUrlAdvertiser.d.ts","sourceRoot":"","sources":["../../src/core/MeshUrlAdvertiser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,gBAAgB,GAAG,SAAS,EACpC,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,GAChC,MAAM,GAAG,IAAI,CASf;AAED,qFAAqF;AACrF,MAAM,WAAW,eAAe;IAC9B,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAChD,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACxD;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,eAAe,EACzB,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,MAAM,GAAG,IAAI,EAClB,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,GAC1B,OAAO,CAWT"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MeshUrlAdvertiser — populate a machine's `lastKnownUrl` so cross-machine
|
|
3
|
+
* routing can actually reach it.
|
|
4
|
+
*
|
|
5
|
+
* THE BUG THIS FIXES (found via real-hardware dogfooding, 2026-05-29):
|
|
6
|
+
* the Multi-Machine Session Pool routes every cross-machine RPC (deliver /
|
|
7
|
+
* transfer / lease) to a peer by reading `registry.machines[id].lastKnownUrl`.
|
|
8
|
+
* But the only writer of that field — `MachineIdentityManager.updateMachineUrl`
|
|
9
|
+
* — had ZERO callers. So in production every peer's URL was null, the peer
|
|
10
|
+
* filters (`!!e.lastKnownUrl`) dropped every peer, and no session could ever be
|
|
11
|
+
* delivered or transferred across machines. The 3-tier tests missed it because
|
|
12
|
+
* they inject peers with mock URLs and never exercise the runtime "where does
|
|
13
|
+
* lastKnownUrl come from" path.
|
|
14
|
+
*
|
|
15
|
+
* A machine's reachable URL is its tunnel URL: a named tunnel's hostname is
|
|
16
|
+
* known from config; a quick tunnel's URL is only known once `tunnel.start()`
|
|
17
|
+
* resolves it. This module turns either into the value to advertise, and writes
|
|
18
|
+
* it into the registry entry — from where the existing git-backed registry sync
|
|
19
|
+
* (RegistrySyncDebouncer) propagates it to peers.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the URL this machine should advertise to the mesh.
|
|
23
|
+
*
|
|
24
|
+
* Preference order:
|
|
25
|
+
* 1. A concretely-resolved tunnel URL (quick tunnels only know it at runtime;
|
|
26
|
+
* `tunnel.start()` returns it). This is authoritative when present.
|
|
27
|
+
* 2. A named tunnel's deterministic `https://<hostname>`.
|
|
28
|
+
* 3. null — no reachable URL (tunnel disabled / not configured). A machine
|
|
29
|
+
* with no tunnel is genuinely not reachable cross-machine; advertising
|
|
30
|
+
* nothing is correct (peers will skip it rather than route to a dead URL).
|
|
31
|
+
*/
|
|
32
|
+
export function resolveAdvertisedMeshUrl(tunnel, resolvedTunnelUrl) {
|
|
33
|
+
if (resolvedTunnelUrl && /^https?:\/\/\S+$/.test(resolvedTunnelUrl.trim())) {
|
|
34
|
+
return resolvedTunnelUrl.trim().replace(/\/+$/, '');
|
|
35
|
+
}
|
|
36
|
+
if (tunnel?.enabled !== false && tunnel?.hostname && tunnel.hostname.trim()) {
|
|
37
|
+
const host = tunnel.hostname.trim().replace(/^https?:\/\//, '').replace(/\/+$/, '');
|
|
38
|
+
if (host)
|
|
39
|
+
return `https://${host}`;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Advertise this machine's reachable URL into its own registry entry so peers
|
|
45
|
+
* can route to it. Idempotent (no write when unchanged), and tolerant when the
|
|
46
|
+
* self entry isn't present yet (best-effort — boot ordering can race the
|
|
47
|
+
* registration). Returns true iff it wrote a new value.
|
|
48
|
+
*/
|
|
49
|
+
export function advertiseSelfMeshUrl(recorder, selfMachineId, url, log) {
|
|
50
|
+
if (!url || !selfMachineId)
|
|
51
|
+
return false;
|
|
52
|
+
try {
|
|
53
|
+
if (recorder.getMachineUrl(selfMachineId) === url)
|
|
54
|
+
return false; // idempotent
|
|
55
|
+
recorder.updateMachineUrl(selfMachineId, url);
|
|
56
|
+
log?.(` Mesh: advertised self URL → ${url}`);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Self entry not present yet — caller may retry on a later lifecycle event.
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=MeshUrlAdvertiser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MeshUrlAdvertiser.js","sourceRoot":"","sources":["../../src/core/MeshUrlAdvertiser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAQH;;;;;;;;;;GAUG;AACH,MAAM,UAAU,wBAAwB,CACtC,MAAoC,EACpC,iBAAiC;IAEjC,IAAI,iBAAiB,IAAI,kBAAkB,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;QAC3E,OAAO,iBAAiB,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACtD,CAAC;IACD,IAAI,MAAM,EAAE,OAAO,KAAK,KAAK,IAAI,MAAM,EAAE,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;QAC5E,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACpF,IAAI,IAAI;YAAE,OAAO,WAAW,IAAI,EAAE,CAAC;IACrC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAQD;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAClC,QAAyB,EACzB,aAAqB,EACrB,GAAkB,EAClB,GAA2B;IAE3B,IAAI,CAAC,GAAG,IAAI,CAAC,aAAa;QAAE,OAAO,KAAK,CAAC;IACzC,IAAI,CAAC;QACH,IAAI,QAAQ,CAAC,aAAa,CAAC,aAAa,CAAC,KAAK,GAAG;YAAE,OAAO,KAAK,CAAC,CAAC,aAAa;QAC9E,QAAQ,CAAC,gBAAgB,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC;QAC9C,GAAG,EAAE,CAAC,iCAAiC,GAAG,EAAE,CAAC,CAAC;QAC9C,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,4EAA4E;QAC5E,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-05-
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-05-29T17:44:10.981Z",
|
|
5
|
+
"instarVersion": "1.3.94",
|
|
6
6
|
"entryCount": 193,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
package/upgrades/1.3.92.md
CHANGED
|
@@ -1,63 +1,27 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
> per-session placement + transfer). Built track-by-track behind a dark stage
|
|
5
|
-
> gate; this guide grows as tracks land and becomes the versioned guide at the
|
|
6
|
-
> release cut. Spec: docs/specs/MULTI-MACHINE-SESSION-POOL-SPEC.md (approved).
|
|
3
|
+
<!-- bump: patch -->
|
|
7
4
|
|
|
8
5
|
## What Changed
|
|
9
6
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
- **Clock-jump-proof router lease.** The machine that holds the "who's in charge"
|
|
17
|
-
lease now judges its own lease expiry on a **monotonic clock** instead of the
|
|
18
|
-
wall clock. A wall-clock jump — an NTP correction, a VM pause/resume, a
|
|
19
|
-
sleep/wake, or a CPU-starvation timer slip — can no longer fool a partitioned
|
|
20
|
-
machine into thinking it still holds the lease (or into giving it up early).
|
|
21
|
-
This is the lease-substrate-robustness hardening that the whole session pool
|
|
22
|
-
rests on, and it directly answers the SleepWakeDetector CPU-starvation lesson.
|
|
23
|
-
- **Router role.** The existing leader lease is now also addressable as the
|
|
24
|
-
"router" role (the machine that dispatches conversations to machines). On a
|
|
25
|
-
single machine this is a no-op — it is its own router.
|
|
26
|
-
- **Dark config block.** A new `multiMachine.sessionPool` config block ships
|
|
27
|
-
turned off (`enabled: false`, `stage: 'dark'`, `dryRun: true`). Existing
|
|
28
|
-
agents receive these dark defaults automatically on update.
|
|
29
|
-
|
|
30
|
-
**Track B (part 1: machine nicknames).** Every machine the agent is installed on
|
|
31
|
-
now gets a friendly, auto-assigned **nickname** (e.g. "Mac Mini", "Justins Macbook
|
|
32
|
-
Pro") — the handle you'll use to say "run this on the mini" / "move this to <name>".
|
|
33
|
-
Nicknames are unique within the pool, editable, and derived deterministically from
|
|
34
|
-
the machine's hostname. Still dark (the Machines dashboard tab + the placement
|
|
35
|
-
commands that consume nicknames land in the next part of Track B).
|
|
7
|
+
The Multi-Machine Session Pool gains its activation wiring (still DARK): the SessionRouter
|
|
8
|
+
is now constructed at boot and consulted on the inbound message path — but ONLY when the
|
|
9
|
+
rollout stage is advanced past 'dark'. A single-machine, un-activated agent behaves exactly
|
|
10
|
+
as before (the interception is gated off on three independent conditions and fails safe to
|
|
11
|
+
local dispatch).
|
|
36
12
|
|
|
37
13
|
## What to Tell Your User
|
|
38
14
|
|
|
39
|
-
Nothing changes
|
|
40
|
-
|
|
41
|
-
it's installed on (and you'll get a Machines tab in the dashboard to name them
|
|
42
|
-
and move conversations between them by nickname). For now, this update just makes
|
|
43
|
-
the under-the-hood "who's in charge" machinery immune to clock jumps, which makes
|
|
44
|
-
everything steadier. A one-machine agent behaves exactly as before.
|
|
15
|
+
Nothing changes yet — this is the plumbing that lets the session pool actually move
|
|
16
|
+
conversations between machines once it's staged on. It stays off until explicitly activated.
|
|
45
17
|
|
|
46
18
|
## Summary of New Capabilities
|
|
47
19
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
-
|
|
51
|
-
`holdsLease()`).
|
|
52
|
-
- `multiMachine.sessionPool` dark config block (`enabled`/`stage`/`dryRun`) with
|
|
53
|
-
migration parity for existing agents.
|
|
20
|
+
| Area | Capability |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| Multi-machine | The inbound dispatch can route a topic's message to the machine that owns the session (gated; dark by default). |
|
|
54
23
|
|
|
55
24
|
## Evidence
|
|
56
25
|
|
|
57
|
-
-
|
|
58
|
-
|
|
59
|
-
wiring integrity, `isRouter()`, and the dark config defaults + migration parity
|
|
60
|
-
(adds-into-existing-multiMachine, no-clobber, inert, idempotent).
|
|
61
|
-
- 155 tests green across the lease/config/coordinator/seamlessness cluster after
|
|
62
|
-
the change; tsc clean. Full suite runs in CI.
|
|
63
|
-
- Side-effects review: upgrades/side-effects/session-pool-track-a-router-lease.md
|
|
26
|
+
- `tests/unit/session-pool-activation-wiring.test.ts` pins the safety invariants: gated on a
|
|
27
|
+
non-dark stage, fail-safe to local dispatch, real-deps construction, module-ref sharing.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- bump: patch -->
|
|
4
|
+
|
|
5
|
+
## What Changed
|
|
6
|
+
|
|
7
|
+
The Multi-Machine Session Pool gains its activation wiring (still DARK): the SessionRouter
|
|
8
|
+
is constructed at boot and consulted on the inbound message path, and a forwarded message
|
|
9
|
+
now resumes the session on the owner machine — but ALL of this is gated on the rollout stage
|
|
10
|
+
being advanced past 'dark'. A single-machine, un-activated agent behaves exactly as before:
|
|
11
|
+
the interception is gated off on three independent conditions (no router without machine
|
|
12
|
+
identity, stage defaults dark, fail-safe to local dispatch).
|
|
13
|
+
|
|
14
|
+
## What to Tell Your User
|
|
15
|
+
|
|
16
|
+
Nothing changes yet — this is the plumbing that lets the session pool actually move
|
|
17
|
+
conversations between machines once it's staged on. It stays off until explicitly activated.
|
|
18
|
+
|
|
19
|
+
## Summary of New Capabilities
|
|
20
|
+
|
|
21
|
+
| Area | Capability |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| Multi-machine | Inbound dispatch can route a topic's message to the machine that owns the session, and the owner resumes it (gated; dark by default). |
|
|
24
|
+
|
|
25
|
+
## Evidence
|
|
26
|
+
|
|
27
|
+
- `tests/unit/session-pool-activation-wiring.test.ts` pins the safety invariants: gated on a
|
|
28
|
+
non-dark stage, fail-safe to local dispatch, real-deps construction, owner-side resume bridge.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- bump: patch -->
|
|
4
|
+
|
|
5
|
+
## What Changed
|
|
6
|
+
|
|
7
|
+
**Multi-Machine Session Pool — cross-machine routing fix (still dark).** Real
|
|
8
|
+
hardware bring-up of a second machine surfaced a wiring gap: the session pool
|
|
9
|
+
routes every cross-machine action (deliver / transfer / lease) to a peer by its
|
|
10
|
+
`lastKnownUrl` (its tunnel URL), but nothing in the shipped code ever wrote that
|
|
11
|
+
field — so every peer was filtered out and no session could be routed or
|
|
12
|
+
transferred across machines. A machine now advertises its own tunnel URL into
|
|
13
|
+
the mesh registry once the tunnel is up (and re-advertises after a sleep/wake
|
|
14
|
+
restart, since a quick tunnel's URL changes), and a joining machine records the
|
|
15
|
+
URL it paired through. The whole layer remains gated behind
|
|
16
|
+
`multiMachine.sessionPool` (default off / `stage:'dark'`), so single-machine
|
|
17
|
+
agents are unaffected.
|
|
18
|
+
|
|
19
|
+
## What to Tell Your User
|
|
20
|
+
|
|
21
|
+
Nothing changes yet — this is still off by default. It fixes a foundational gap
|
|
22
|
+
so that, when the session pool is enabled, conversations can actually be placed
|
|
23
|
+
on and moved between machines.
|
|
24
|
+
|
|
25
|
+
## Summary of New Capabilities
|
|
26
|
+
|
|
27
|
+
- `MeshUrlAdvertiser` (`resolveAdvertisedMeshUrl` + `advertiseSelfMeshUrl`):
|
|
28
|
+
populates a machine's `lastKnownUrl` from its tunnel URL on boot + sleep/wake.
|
|
29
|
+
- `instar join` now records the awake machine's URL on the joining (standby) side.
|
|
30
|
+
|
|
31
|
+
## Evidence
|
|
32
|
+
|
|
33
|
+
- Unit: `tests/unit/mesh-url-advertiser.test.ts` (resolver + advertiser vs a real
|
|
34
|
+
MachineIdentityManager).
|
|
35
|
+
- Wiring-integrity: `tests/unit/mesh-url-advertisement-wiring.test.ts` (asserts
|
|
36
|
+
the advertiser is actually invoked — the tier that would have caught the
|
|
37
|
+
original zero-callers gap).
|
|
38
|
+
- Side-effects: `upgrades/side-effects/mesh-url-advertisement.md`.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Side-effects review — Mesh URL advertisement (lastKnownUrl population)
|
|
2
|
+
|
|
3
|
+
## What changed
|
|
4
|
+
|
|
5
|
+
`updateMachineUrl()` (the only writer of `registry.machines[id].lastKnownUrl`)
|
|
6
|
+
had **zero callers** on main. The Multi-Machine Session Pool routes every
|
|
7
|
+
cross-machine RPC — `deliverMessage`, session transfer, lease ops — to a peer by
|
|
8
|
+
reading that peer's `lastKnownUrl`, and filters peers with `!!e.lastKnownUrl`.
|
|
9
|
+
With the field never written, every peer was filtered out and **no session
|
|
10
|
+
could ever be delivered or transferred across machines**. The feature was inert
|
|
11
|
+
across real machines despite green 3-tier tests (which inject mock peer URLs).
|
|
12
|
+
|
|
13
|
+
Found 2026-05-29 bringing up a second real machine (the mini) for the
|
|
14
|
+
session-pool real-hardware proof.
|
|
15
|
+
|
|
16
|
+
This change adds:
|
|
17
|
+
- `src/core/MeshUrlAdvertiser.ts` — `resolveAdvertisedMeshUrl(tunnel, resolvedUrl?)`
|
|
18
|
+
(named → `https://<hostname>`, quick → the runtime-resolved start URL, none →
|
|
19
|
+
null) + `advertiseSelfMeshUrl(recorder, selfId, url)` (idempotent, tolerant of
|
|
20
|
+
a missing self entry).
|
|
21
|
+
- `src/commands/server.ts` — calls the advertiser after `tunnel.start()` on both
|
|
22
|
+
the boot path and the sleep/wake restart path (a quick tunnel gets a new URL
|
|
23
|
+
after a wake, so the previously-advertised URL goes stale).
|
|
24
|
+
- `src/commands/machine.ts` (`joinMesh`) — the joiner records the URL it
|
|
25
|
+
connected through as the awake peer's `lastKnownUrl`, so a standby can route
|
|
26
|
+
back to the awake machine immediately without waiting for a registry sync.
|
|
27
|
+
|
|
28
|
+
## Blast radius
|
|
29
|
+
|
|
30
|
+
- **Dark-feature-only effect.** The populated `lastKnownUrl` is consumed solely
|
|
31
|
+
by the session-pool mesh code, which is gated behind `multiMachine.sessionPool`
|
|
32
|
+
(default `enabled:false`, `stage:'dark'`). On a single-machine agent there are
|
|
33
|
+
no peers, so nothing changes. Populating the field is otherwise inert.
|
|
34
|
+
- **No new config, no new route, no schema change** → no migration needed.
|
|
35
|
+
Existing agents pick up the behavior at boot on the next release. No
|
|
36
|
+
`PostUpdateMigrator` entry required (the code runs at startup unconditionally
|
|
37
|
+
when multi-machine is enabled and a tunnel URL exists).
|
|
38
|
+
- **Idempotent + fail-safe.** `advertiseSelfMeshUrl` writes only when the URL
|
|
39
|
+
changed and swallows the "self entry not present yet" race (returns false).
|
|
40
|
+
A tunnel-disabled machine advertises nothing (correct — it is genuinely
|
|
41
|
+
unreachable cross-machine; peers skip it rather than route to a dead URL).
|
|
42
|
+
- **No secret exposure.** A tunnel URL is already public (the dashboard link).
|
|
43
|
+
Recording it in the registry adds no new exposure.
|
|
44
|
+
|
|
45
|
+
## What this does NOT close (scoped, decision-gated follow-up)
|
|
46
|
+
|
|
47
|
+
The **awake machine learning a git-uncredentialed standby's URL** still needs one
|
|
48
|
+
of: (a) the standby having git push creds for the shared agent repo (so its
|
|
49
|
+
advertised entry syncs via the existing git-backed RegistrySyncDebouncer), or
|
|
50
|
+
(b) a signed mesh `announce-url` RPC so a standby can publish its URL to the
|
|
51
|
+
awake machine over authenticated MeshRpc without git. (b) also requires the
|
|
52
|
+
awake side to persist the joiner's identity, which today only happens via the
|
|
53
|
+
**interactive SAS pairing** confirmation — so closing this fully touches the
|
|
54
|
+
pairing security model. That is a genuine design fork (creds vs. new RPC +
|
|
55
|
+
non-interactive registration), not a silent deferral, and is raised explicitly
|
|
56
|
+
rather than hand-resolved on a production identity.
|
|
57
|
+
|
|
58
|
+
## Tests
|
|
59
|
+
|
|
60
|
+
- `tests/unit/mesh-url-advertiser.test.ts` — resolver (named/quick/disabled/none)
|
|
61
|
+
+ advertiser against a real `MachineIdentityManager` (writes, idempotent,
|
|
62
|
+
updates on change, null no-op, missing-entry tolerance).
|
|
63
|
+
- `tests/unit/mesh-url-advertisement-wiring.test.ts` — wiring-integrity: asserts
|
|
64
|
+
the advertiser is invoked on the boot + sleep/wake tunnel paths and the joiner
|
|
65
|
+
records the connect URL. This is the tier that was missing — it would have
|
|
66
|
+
caught the original "zero callers" gap.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Side effects — Session Pool activation wiring (§L4 live-ingress, dark)
|
|
2
|
+
|
|
3
|
+
## What this adds (task A of the activation)
|
|
4
|
+
Wires the SessionRouter into the live boot + the inbound message-dispatch path, GATED so it is inert until the rollout stage advances past 'dark'.
|
|
5
|
+
|
|
6
|
+
- `src/commands/server.ts`:
|
|
7
|
+
- Module-level refs `_sessionRouter` + `_sessionPoolStage()` (the idiomatic instar pattern — the inbound handler in `wireTelegramRouting` is defined above `startServer`, so the router constructed in startServer's mesh block is shared via a module ref, like `_orphanReaper`/`_topicResumeMap`).
|
|
8
|
+
- **Construction** (mesh block, when a machine identity exists): `new SessionRouter({...})` wired to the real `MachinePoolRegistry` (machineRegistry), `SessionOwnershipRegistry` (resolveOwnership + casClaimOwnership), `PlacementExecutor`, and an outbound `MeshRpcClient` (deliverMessage/spawnOnMachine to peers via `lastKnownUrl`). `_sessionPoolStage()` reads the live `multiMachine.sessionPool.{enabled,stage}` (defaults 'dark').
|
|
9
|
+
- **Interception** (inbound `onTopicMessage`, after target-session resolution): when `_sessionRouter && _sessionPoolStage() !== 'dark'`, consult `route()`; a `forwarded`/`duplicate` outcome short-circuits (the owner machine handles it); everything else falls through to the existing local dispatch. Wrapped in try/catch → falls back to local on any error.
|
|
10
|
+
|
|
11
|
+
## Risk / blast radius
|
|
12
|
+
**None in production.** The gate defaults to dark on three independent conditions: `_sessionRouter` is null without machine identity; `_sessionPoolStage()` returns 'dark' unless `enabled && stage` set (ConfigDefaults ships enabled:false/stage:dark); and the interception fails safe to local dispatch. So a single-machine, un-activated agent is byte-identical to today. The live-transfer behavior is gated by the staged rollout (StageAdvancer + E2E records).
|
|
13
|
+
|
|
14
|
+
## Tests
|
|
15
|
+
- `tests/unit/session-pool-activation-wiring.test.ts` — 4 wiring-integrity tests pinning the safety invariants: gated on non-dark stage (default-dark → inert), fail-safe try/catch → local, real-deps construction (registry/ownership/placement + MeshRpcClient), module-ref sharing. Guards against a regression that removes the gate (which would activate cross-machine routing unconditionally).
|
|
16
|
+
- SessionRouter behavior itself is covered by its 17 unit + 3 integration tests (#506).
|
|
17
|
+
|
|
18
|
+
## Remaining (the hardware operation — B/C/D)
|
|
19
|
+
Deploy to laptop + bring up the mini as machine #2; advance dark→shadow→live-transfer (each E2E-gated); test-as-self nickname-swap proof. The owner-side deliverMessage→local-spawn bridge (so a forwarded message actually spawns on the owner) is the final piece, validated on the 2-machine setup.
|
|
20
|
+
|
|
21
|
+
## Addendum — owner-side bridge (task A.2)
|
|
22
|
+
`createDeliverMessageHandler` now takes an `onAccepted` hook wired (in the mesh block) to `spawnSessionForTopic`: a FIRST-seen forwarded `deliverMessage` spawns/resumes the local session for the topic so the conversation continues on the OWNER machine — the receive-side of the §L4 handoff. Gated on `_sessionPoolStage() !== 'dark'` + `telegram` present; fire-and-forget (the durable receipt is ACKed before this runs); fails safe (logs, never throws into the dispatcher). Inert until live-transfer (a deliverMessage only arrives from a router peer). This makes the activation code-complete: outbound forward (router) + receive+ACK (handler) + owner-side resume (bridge). Remaining is purely the 2-machine hardware proof.
|