instar 1.3.101 → 1.3.102

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.
@@ -0,0 +1,71 @@
1
+ /**
2
+ * PeerPresencePuller (Multi-Machine Session Pool — HTTP presence transport).
3
+ *
4
+ * The pool registry's notion of which peer machines are ONLINE was originally
5
+ * fed only by the git-synced MachineHeartbeat: each machine writes its heartbeat
6
+ * into the shared agent repo, peers pull it, and recency ⇒ online. That breaks
7
+ * for a *credential-less standby* — a machine paired into the mesh over HTTP but
8
+ * WITHOUT push access to the shared repo (e.g. a second machine gh-authed to a
9
+ * different account). Its heartbeat never reaches the router, so the router
10
+ * marks it offline and the placement engine refuses to transfer to it, even
11
+ * though the two machines can reach each other perfectly over their tunnels.
12
+ *
13
+ * This puller closes that gap with a pull-based HTTP presence channel: on a
14
+ * cadence, ask each reachable peer for its self-capacity over the signed
15
+ * /mesh/rpc `session-status` command (a read-class command any registered peer
16
+ * may issue) and record the answer into the local pool registry. A peer that
17
+ * answers is, by definition, reachable + alive — so it goes online without ever
18
+ * touching git. Symmetric by design: every mesh machine runs one, so each keeps
19
+ * its own HTTP-sourced view of peer liveness, parallel to (and idempotent with)
20
+ * the git-synced path for credentialed peers.
21
+ *
22
+ * Fully injected (peers, fetch, record, clock) so the loop logic is unit-testable
23
+ * without a live mesh. `pullOnce()` NEVER throws — an unreachable peer simply is
24
+ * not recorded this pass and ages out of `online` naturally via the registry's
25
+ * failover threshold.
26
+ */
27
+ /** A peer to consider polling: its machine id + resolved tunnel URL (null ⇒ unreachable). */
28
+ export interface PeerPresenceMachine {
29
+ machineId: string;
30
+ url: string | null;
31
+ }
32
+ /** The slice of a peer's self-capacity the puller records (from `session-status`). */
33
+ export interface PeerCapacity {
34
+ selfReportedLastSeen?: string;
35
+ loadAvg?: number;
36
+ }
37
+ export interface PeerPresencePullerDeps {
38
+ /** This machine's id — never pulled (a machine doesn't poll itself). */
39
+ selfMachineId: string;
40
+ /** The current mesh peers (id + resolved URL). Re-read each pass so newly paired peers are picked up. */
41
+ listPeers: () => PeerPresenceMachine[];
42
+ /**
43
+ * Send a signed `session-status` read to a peer and resolve its self-capacity,
44
+ * or `null` if the peer did not answer / rejected. May reject; `pullOnce`
45
+ * treats a rejection identically to a `null` (peer not recorded this pass).
46
+ */
47
+ fetchPeerCapacity: (machineId: string, url: string) => Promise<PeerCapacity | null>;
48
+ /** Record an observed peer heartbeat into the pool registry (marks it online for the failover window). */
49
+ recordHeartbeat: (obs: {
50
+ machineId: string;
51
+ selfReportedLastSeen: string;
52
+ loadAvg?: number;
53
+ }) => void;
54
+ /** Wall clock — injectable for tests. Defaults to `Date`. */
55
+ now?: () => Date;
56
+ /** Optional structured log line per pass (e.g. for the boot log). */
57
+ log?: (line: string) => void;
58
+ }
59
+ export declare class PeerPresencePuller {
60
+ private readonly d;
61
+ constructor(d: PeerPresencePullerDeps);
62
+ /**
63
+ * One presence pass: poll every reachable peer in parallel and record any that
64
+ * answer into the pool registry. Resolves with the ids recorded this pass.
65
+ * Never rejects.
66
+ */
67
+ pullOnce(): Promise<{
68
+ recorded: string[];
69
+ }>;
70
+ }
71
+ //# sourceMappingURL=PeerPresencePuller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PeerPresencePuller.d.ts","sourceRoot":"","sources":["../../src/core/PeerPresencePuller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,6FAA6F;AAC7F,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAED,sFAAsF;AACtF,MAAM,WAAW,YAAY;IAC3B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC,wEAAwE;IACxE,aAAa,EAAE,MAAM,CAAC;IACtB,yGAAyG;IACzG,SAAS,EAAE,MAAM,mBAAmB,EAAE,CAAC;IACvC;;;;OAIG;IACH,iBAAiB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IACpF,0GAA0G;IAC1G,eAAe,EAAE,CAAC,GAAG,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,oBAAoB,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACtG,6DAA6D;IAC7D,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,qEAAqE;IACrE,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAED,qBAAa,kBAAkB;IACjB,OAAO,CAAC,QAAQ,CAAC,CAAC;gBAAD,CAAC,EAAE,sBAAsB;IAEtD;;;;OAIG;IACG,QAAQ,IAAI,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;CAoBlD"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * PeerPresencePuller (Multi-Machine Session Pool — HTTP presence transport).
3
+ *
4
+ * The pool registry's notion of which peer machines are ONLINE was originally
5
+ * fed only by the git-synced MachineHeartbeat: each machine writes its heartbeat
6
+ * into the shared agent repo, peers pull it, and recency ⇒ online. That breaks
7
+ * for a *credential-less standby* — a machine paired into the mesh over HTTP but
8
+ * WITHOUT push access to the shared repo (e.g. a second machine gh-authed to a
9
+ * different account). Its heartbeat never reaches the router, so the router
10
+ * marks it offline and the placement engine refuses to transfer to it, even
11
+ * though the two machines can reach each other perfectly over their tunnels.
12
+ *
13
+ * This puller closes that gap with a pull-based HTTP presence channel: on a
14
+ * cadence, ask each reachable peer for its self-capacity over the signed
15
+ * /mesh/rpc `session-status` command (a read-class command any registered peer
16
+ * may issue) and record the answer into the local pool registry. A peer that
17
+ * answers is, by definition, reachable + alive — so it goes online without ever
18
+ * touching git. Symmetric by design: every mesh machine runs one, so each keeps
19
+ * its own HTTP-sourced view of peer liveness, parallel to (and idempotent with)
20
+ * the git-synced path for credentialed peers.
21
+ *
22
+ * Fully injected (peers, fetch, record, clock) so the loop logic is unit-testable
23
+ * without a live mesh. `pullOnce()` NEVER throws — an unreachable peer simply is
24
+ * not recorded this pass and ages out of `online` naturally via the registry's
25
+ * failover threshold.
26
+ */
27
+ export class PeerPresencePuller {
28
+ d;
29
+ constructor(d) {
30
+ this.d = d;
31
+ }
32
+ /**
33
+ * One presence pass: poll every reachable peer in parallel and record any that
34
+ * answer into the pool registry. Resolves with the ids recorded this pass.
35
+ * Never rejects.
36
+ */
37
+ async pullOnce() {
38
+ const peers = this.d.listPeers().filter((m) => m.machineId !== this.d.selfMachineId && !!m.url);
39
+ const results = await Promise.all(peers.map(async (m) => {
40
+ let cap = null;
41
+ try {
42
+ cap = await this.d.fetchPeerCapacity(m.machineId, m.url);
43
+ }
44
+ catch {
45
+ cap = null; // unreachable / rejected → not recorded; ages out of online
46
+ }
47
+ if (!cap)
48
+ return null;
49
+ const seen = cap.selfReportedLastSeen ?? (this.d.now?.() ?? new Date()).toISOString();
50
+ this.d.recordHeartbeat({ machineId: m.machineId, selfReportedLastSeen: seen, loadAvg: cap.loadAvg });
51
+ return m.machineId;
52
+ }));
53
+ const recorded = results.filter((r) => r !== null);
54
+ if (recorded.length && this.d.log)
55
+ this.d.log(`recorded ${recorded.length} peer(s) online over HTTP: ${recorded.join(', ')}`);
56
+ return { recorded };
57
+ }
58
+ }
59
+ //# sourceMappingURL=PeerPresencePuller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PeerPresencePuller.js","sourceRoot":"","sources":["../../src/core/PeerPresencePuller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAiCH,MAAM,OAAO,kBAAkB;IACA;IAA7B,YAA6B,CAAyB;QAAzB,MAAC,GAAD,CAAC,CAAwB;IAAG,CAAC;IAE1D;;;;OAIG;IACH,KAAK,CAAC,QAAQ;QACZ,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,IAAI,CAAC,CAAC,CAAC,aAAa,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAChG,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAC/B,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,EAA0B,EAAE;YAC5C,IAAI,GAAG,GAAwB,IAAI,CAAC;YACpC,IAAI,CAAC;gBACH,GAAG,GAAG,MAAM,IAAI,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,GAAa,CAAC,CAAC;YACrE,CAAC;YAAC,MAAM,CAAC;gBACP,GAAG,GAAG,IAAI,CAAC,CAAC,4DAA4D;YAC1E,CAAC;YACD,IAAI,CAAC,GAAG;gBAAE,OAAO,IAAI,CAAC;YACtB,MAAM,IAAI,GAAG,GAAG,CAAC,oBAAoB,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,EAAE,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;YACtF,IAAI,CAAC,CAAC,CAAC,eAAe,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE,oBAAoB,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;YACrG,OAAO,CAAC,CAAC,SAAS,CAAC;QACrB,CAAC,CAAC,CACH,CAAC;QACF,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;QAChE,IAAI,QAAQ,CAAC,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG;YAAE,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,YAAY,QAAQ,CAAC,MAAM,8BAA8B,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC9H,OAAO,EAAE,QAAQ,EAAE,CAAC;IACtB,CAAC;CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.101",
3
+ "version": "1.3.102",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-05-29T23:00:30.910Z",
5
- "instarVersion": "1.3.101",
4
+ "generatedAt": "2026-05-29T23:18:48.200Z",
5
+ "instarVersion": "1.3.102",
6
6
  "entryCount": 193,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -0,0 +1,56 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- bump: patch -->
4
+
5
+ ## What Changed
6
+
7
+ **Multi-machine: a second machine that can't push to your shared agent repo now
8
+ still shows up as online — so work can move to it.** When you run one agent
9
+ across two machines, the pool decides which machine is "online" (and therefore
10
+ eligible to receive a conversation) from a heartbeat. Until now that heartbeat
11
+ travelled through the shared agent git repo: each machine wrote its heartbeat
12
+ into the repo and the others read it. A second machine paired over the network
13
+ but signed into a *different* GitHub account has no way to push to that repo, so
14
+ its heartbeat never arrived — the main machine saw it as permanently offline and
15
+ refused to hand any session to it, even though the two machines could reach each
16
+ other perfectly over their tunnels.
17
+
18
+ There's now a direct presence channel that doesn't touch git: each machine
19
+ periodically asks every reachable peer "are you there?" over the same signed,
20
+ encrypted machine-to-machine channel it already uses for everything else, and
21
+ marks a peer online the moment it answers. A credential-less standby therefore
22
+ becomes a real, transfer-eligible member of the pool purely over its tunnel.
23
+
24
+ ## What to Tell Your User
25
+
26
+ If you only run on one machine, nothing changes. If you run across two machines
27
+ and the second one is signed into a different GitHub account (or otherwise can't
28
+ push to the shared repo), it will now correctly show as online in your Machines
29
+ list and become eligible to receive conversations — no extra setup, and no git
30
+ credentials needed on the second machine.
31
+
32
+ ## Summary of New Capabilities
33
+
34
+ - `PeerPresencePuller` (new core module): a pull-based HTTP presence channel.
35
+ Each mesh machine asks every reachable peer for its self-capacity over the
36
+ signed `session-status` MeshRpc command and records the answer into its local
37
+ `MachinePoolRegistry` — so a peer that can't push a git-synced heartbeat still
38
+ goes online over its tunnel. Symmetric, additive, and idempotent with the
39
+ existing git-synced heartbeat path; inert on a single-machine agent.
40
+
41
+ ## Evidence
42
+
43
+ - `tests/unit/peer-presence-puller.test.ts` — loop semantics with injected fakes
44
+ (records an answering peer; never polls self; skips a peer with no URL; a
45
+ rejecting fetch neither throws nor records; null is not recorded; clock
46
+ fallback; `listPeers` re-read each pass).
47
+ - `tests/unit/peer-presence-wiring.test.ts` — server-boot wiring: a real puller
48
+ is constructed, feeds the signed `session-status` call into
49
+ `machinePoolRegistry.recordHeartbeat`, and is actually started (immediate pass
50
+ + recurring unref-ed timer).
51
+ - `tests/integration/peer-presence-roundtrip.test.ts` — full chain over a real
52
+ loopback `/mesh/rpc` with real Ed25519 keys: a peer starts OFFLINE +
53
+ placement-ineligible on the router (no git heartbeat) and, after ONE presence
54
+ pass over the signed channel, is ONLINE + placement-eligible — with an
55
+ unreachable peer never marked online.
56
+ - Side-effects: `upgrades/side-effects/http-presence-transport.md`.
@@ -0,0 +1,75 @@
1
+ # Side-effects review — HTTP presence transport (PeerPresencePuller)
2
+
3
+ ## What was happening (real-hardware, 2026-05-29)
4
+
5
+ The Multi-Machine Session Pool's live-transfer proof (laptop ↔ Mac mini) got all
6
+ the way to the edge and surfaced one architectural gap: the mesh **forms** fine
7
+ over HTTP (non-interactive code-auth pairing, URLs exchanged, peers reachable at
8
+ their tunnels, split-brain clear), but the router would not transfer a session
9
+ to the mini because it saw the mini as `online: false`.
10
+
11
+ Root cause: the pool registry's notion of which peers are ONLINE was fed ONLY by
12
+ the **git-synced** `MachineHeartbeat` — each machine writes its heartbeat into
13
+ the shared agent repo and peers pull it. The mini is paired into the mesh over
14
+ HTTP but is `gh`-authed to a *different* account, so it has no push access to the
15
+ shared repo. Its heartbeat never reaches the router → the router marks it offline
16
+ → the placement engine refuses to transfer to an offline machine — even though
17
+ the two machines reach each other perfectly over their tunnels.
18
+
19
+ ## The fix
20
+
21
+ A pull-based HTTP presence channel that does not touch git:
22
+
23
+ - **`src/core/PeerPresencePuller.ts`** (new) — on a cadence, ask each reachable
24
+ peer for its self-capacity over the signed §L0 `session-status` MeshRpc command
25
+ and feed the answer into the local `MachinePoolRegistry.recordHeartbeat`. A peer
26
+ that answers is by definition reachable + alive, so it goes `online` purely over
27
+ HTTP. Fully injected (peers, fetch, record, clock) and `pullOnce()` NEVER throws
28
+ — an unreachable peer is simply not recorded this pass and ages out of `online`
29
+ via the registry's existing failover threshold.
30
+
31
+ - **`src/commands/server.ts`** — in the mesh-rpc block (right after the
32
+ SessionRouter is wired, where `meshClient` / `peerUrl` / `machinePoolRegistry`
33
+ are in scope), construct one `PeerPresencePuller` and start it: an immediate
34
+ `pullOnce()` plus a 30 s `unref`-ed interval. `fetchPeerCapacity` issues
35
+ `meshClient.send(peer, { type: 'session-status' }, 0)`; `recordHeartbeat`
36
+ delegates to the real pool registry.
37
+
38
+ `session-status` is a read-class command (RBAC: any *registered* peer — proven by
39
+ `verifyEnvelope`), so the call authenticates off the mutual identity established
40
+ at pairing. No router role, no epoch fence, no new route, no new config.
41
+
42
+ ## Blast radius
43
+
44
+ - **Additive + symmetric.** Every mesh machine runs one puller; each maintains
45
+ its own HTTP-sourced view of peer liveness. It runs in PARALLEL with the
46
+ existing git-synced `refreshPool` path — a credentialed peer that both
47
+ git-syncs AND answers HTTP just refreshes twice. `recordHeartbeat` is the single
48
+ arbiter and the router's-clock liveness rule is unchanged, so a peer can never
49
+ be marked *fresher* than its real `routerReceivedAt`.
50
+ - **Inert without a mesh.** A single-machine agent has no peers → `listPeers()` is
51
+ empty → `pullOnce()` is a no-op. Only constructed inside the existing
52
+ mesh-enabled block.
53
+ - **No new config / route / schema / hook / skill → no migration.** Server +
54
+ one new core module; existing agents pick it up on the next release (same shape
55
+ as the native-rebuild / wake-socket fleet fix).
56
+ - **Does NOT replace the git substrate** for ownership/placement *records* (§L0)
57
+ — only the high-frequency presence signal moves to HTTP.
58
+
59
+ ## Tests
60
+
61
+ - `tests/unit/peer-presence-puller.test.ts` — loop semantics with injected fakes:
62
+ a peer that answers is recorded (id + loadAvg + selfReportedLastSeen); self is
63
+ never polled; a peer with no URL is skipped; a fetch that REJECTS does not throw
64
+ and does not record (peer ages out); a null result is not recorded; the clock
65
+ fallback fills `selfReportedLastSeen`; `listPeers` is re-read each pass.
66
+ - `tests/unit/peer-presence-wiring.test.ts` — pins the server-boot wiring:
67
+ imports + constructs a real puller, feeds the signed `session-status` call into
68
+ `machinePoolRegistry?.recordHeartbeat`, and actually STARTS it (immediate
69
+ `pullOnce()` + recurring `unref`-ed timer) — guards against the
70
+ "constructed-but-inert" failure the Testing Integrity Standard calls out.
71
+ - `tests/integration/peer-presence-roundtrip.test.ts` — the full chain over a real
72
+ loopback `/mesh/rpc` with real Ed25519 keys: MINI starts OFFLINE +
73
+ placement-ineligible on the router (no git heartbeat); after ONE presence pass
74
+ over the signed channel it is ONLINE + placement-eligible (loadAvg carried over
75
+ HTTP) — and an unreachable peer is never marked online.