@geohar/un-bien 0.20.16 → 0.20.18
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/deps.d.ts +1 -1
- package/dist/commands/test_scenarios.d.ts +20 -0
- package/dist/commands/test_scenarios.js +414 -0
- package/dist/commands/test_scenarios.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +161 -727
- package/dist/index.js.map +1 -1
- package/dist/routing.d.ts +50 -0
- package/dist/routing.js +250 -0
- package/dist/routing.js.map +1 -0
- package/dist/session/received_images.js +1 -1
- package/dist/session/relay_lifecycle.d.ts +1 -1
- package/dist/session/relay_lifecycle.js +15 -2
- package/dist/session/relay_lifecycle.js.map +1 -1
- package/dist/session/rpc_handlers.js +1 -1
- package/dist/test_hooks.d.ts +13 -0
- package/dist/test_hooks.js +7 -0
- package/dist/test_hooks.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -35,28 +35,26 @@ import { createExtensionUiBridge, } from "./extension_ui_bridge.js";
|
|
|
35
35
|
import { createPanelBridge } from "./panel_bridge.js";
|
|
36
36
|
import { initSubagentRooms, subagentRoomsEnabled, } from "./subagent_rooms.js";
|
|
37
37
|
import { createRpcEnvelope, } from "./session/rpc_envelope.js";
|
|
38
|
-
import { dispatchRpcCommand } from "./session/rpc_inbound.js";
|
|
39
38
|
import { envLog } from "./session/debug_log.js";
|
|
40
39
|
import { chainUiNotify, notifyOwners, setOwnerNotifyHook } from "./session/owner_notify.js";
|
|
41
40
|
import { roomIdForSession } from "./rooms.js";
|
|
42
41
|
import { registerAgentTools } from "./session/tools.js";
|
|
43
42
|
import { formatPeerInventory } from "./session/peer_inventory.js";
|
|
44
43
|
import { ensureGlobalDirs, LOCAL_SESSION_NAME, sessionSockPath, skillsDir, } from "./session/global_config.js";
|
|
45
|
-
import { effectiveAutoStartRelay,
|
|
44
|
+
import { effectiveAutoStartRelay, loadLocalConfig, localConfigExists, piSessionName, resolveAgentName, saveLocalConfig, } from "./session/local_config.js";
|
|
46
45
|
import { updateFooter } from "./ui/footer.js";
|
|
47
46
|
import { fileURLToPath } from "node:url";
|
|
48
47
|
import { realpathSync } from "node:fs";
|
|
49
|
-
import {
|
|
50
|
-
import { _expandTilde, _launchSession, launchDirAllowed } from "./launch.js";
|
|
48
|
+
import { saveConfig, isValidRelayUrl, isWebSocketScheme, } from "./config.js";
|
|
51
49
|
import { _enrichToolArgs } from "./enrich_tool_args.js";
|
|
52
50
|
import { _inspectPeerRecord, _runtimeOwnerFingerprint, } from "./pairing/peer_trust.js";
|
|
53
51
|
import { _flushPendingReceivedImagePreviews, _isReceivedImageContextMessage, _registerReceivedImageRenderer, } from "./session/received_images.js";
|
|
54
52
|
import { _cmdRoot, _cmdStart, } from "./commands/lifecycle.js";
|
|
55
53
|
import { _cmdClaudeCli, _cmdInstallTarget, _cmdUninstallTarget, parseInstallTarget, _deployAgentNetworkSkill, } from "./commands/housekeeping.js";
|
|
56
54
|
import { registerUnbienCommands } from "./commands/register.js";
|
|
57
|
-
import {
|
|
55
|
+
import { makeTestBusEmitter, runTestScenario } from "./commands/test_scenarios.js";
|
|
56
|
+
import { routeRpcCommand, routeUnBienPlane } from "./routing.js";
|
|
58
57
|
import { createTestHooks } from "./test_hooks.js";
|
|
59
|
-
import { createRpcHandlers, } from "./session/rpc_handlers.js";
|
|
60
58
|
import { CTRL_PREFIX, _anyPeerActive, _broadcastEnvelope, _broadcastEnvelopeFlushed, _controlCtx, _detachPeerChannel, _emitRelayState, _getReconnectTimer, _getRelayLifecycleGeneration, _goIdle, _handleControl as _handleControlImpl, _headlessUi, _onPeerDisconnect as _onPeerDisconnectImpl, _onRelayClose, _panelBroadcast, _pushPairingAllowList, _relayStatus, _setupRelayConnection, _setRelayLifecycleGeneration, _uiBroadcast, } from "./session/relay_lifecycle.js";
|
|
61
59
|
// Relay-lifecycle seam re-exports: the control-channel sentinel + the
|
|
62
60
|
// connectivity type moved to ./session/relay_lifecycle.ts; these keep the
|
|
@@ -79,7 +77,6 @@ let _relayUrl = null; // URL used by current _relay connection
|
|
|
79
77
|
*/
|
|
80
78
|
const _activePeers = new Map();
|
|
81
79
|
let _peerShort = ""; // shortid of the most recently attached peer (UX hint only)
|
|
82
|
-
let _myRoomId = null; // this Pi's room id (derived from the session id)
|
|
83
80
|
/** True when a relay start was DEFERRED because no session id existed yet
|
|
84
81
|
* (design 01M1CAW0). The root session_start handler re-runs the start once
|
|
85
82
|
* the session id becomes available. */
|
|
@@ -98,14 +95,6 @@ function _deriveRoomId(_cwd, _name) {
|
|
|
98
95
|
const sid = _rootState().sessionManager?.getSessionId();
|
|
99
96
|
return sid ? roomIdForSession(sid) : null;
|
|
100
97
|
}
|
|
101
|
-
// Plan/28 Wave D.1: `thinking` published alongside `model` so the app's
|
|
102
|
-
// Quick Actions sheet hydrates the thinking segmented control on first
|
|
103
|
-
// open instead of starting null. The SDK fires `thinking_level_select`
|
|
104
|
-
// on every change (initial load + user toggle), mirrored to room_meta
|
|
105
|
-
// the same way model is — apps subscribe to one channel for both.
|
|
106
|
-
let _myRoomMeta = null;
|
|
107
|
-
let _currentModel; // last-known model name
|
|
108
|
-
let _currentThinking; // last-known thinking level
|
|
109
98
|
// ── Agent-network session (plano 19) ──────────────────────────────────────────
|
|
110
99
|
// MeshNode owns both the local UDS mesh (SessionPeer) and the optional
|
|
111
100
|
// cross-PC relay bridge (BrokerRemote + PiForwardClient). The bridge is
|
|
@@ -170,7 +159,7 @@ function _refreshSessionPeerCount(peer, ctx) {
|
|
|
170
159
|
}
|
|
171
160
|
/** Friendly model name for room_meta (plano 18). undefined when SDK has none yet. */
|
|
172
161
|
function _currentModelName() {
|
|
173
|
-
return
|
|
162
|
+
return _rootState().currentModel ?? undefined;
|
|
174
163
|
}
|
|
175
164
|
/**
|
|
176
165
|
* Cache the active model name and fan it out to subscribed apps via a
|
|
@@ -182,13 +171,14 @@ function _currentModelName() {
|
|
|
182
171
|
* otherwise never surface their model.
|
|
183
172
|
*/
|
|
184
173
|
function _setCurrentModel(name) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
174
|
+
const st = _rootState();
|
|
175
|
+
st.currentModel = name;
|
|
176
|
+
if (st.myRoomMeta)
|
|
177
|
+
st.myRoomMeta = { ...st.myRoomMeta, model: name };
|
|
178
|
+
if (_relay && st.myRoomId) {
|
|
189
179
|
_relay.sendControl({
|
|
190
180
|
type: "room_meta_update",
|
|
191
|
-
room_id:
|
|
181
|
+
room_id: st.myRoomId,
|
|
192
182
|
meta: { model: name },
|
|
193
183
|
});
|
|
194
184
|
}
|
|
@@ -201,12 +191,13 @@ function _setCurrentModel(name) {
|
|
|
201
191
|
* so room_meta.working must be bracketed manually around compaction.
|
|
202
192
|
*/
|
|
203
193
|
function _publishWorking(working) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
194
|
+
const st = _rootState();
|
|
195
|
+
if (st.myRoomMeta)
|
|
196
|
+
st.myRoomMeta = { ...st.myRoomMeta, working };
|
|
197
|
+
if (_relay && st.myRoomId) {
|
|
207
198
|
_relay.sendControl({
|
|
208
199
|
type: "room_meta_update",
|
|
209
|
-
room_id:
|
|
200
|
+
room_id: st.myRoomId,
|
|
210
201
|
meta: { working },
|
|
211
202
|
});
|
|
212
203
|
}
|
|
@@ -276,13 +267,13 @@ function _attachBridgeIfReady() {
|
|
|
276
267
|
}
|
|
277
268
|
/**
|
|
278
269
|
* Prefer an explicit ctx, else the always-fresh session_start ctx
|
|
279
|
-
* (
|
|
270
|
+
* (the root record's baseCtx, re-captured on every session_start so it is never stale
|
|
280
271
|
* capture). Relay/async paths reach the ctx-only surfaces (ui/abort/compact)
|
|
281
272
|
* through this, since they fire outside any live pi event.
|
|
282
273
|
* @see https://github.com/jacobaraujo7/remote_pi/issues/55
|
|
283
274
|
*/
|
|
284
275
|
function _liveCtx(preferred) {
|
|
285
|
-
return preferred ??
|
|
276
|
+
return preferred ?? _rootState().baseCtx ?? null;
|
|
286
277
|
}
|
|
287
278
|
/**
|
|
288
279
|
* Read `ctx.ui` without letting a stale-ctx getter become an uncaughtException.
|
|
@@ -351,14 +342,41 @@ function _refreshFooter(ctx) {
|
|
|
351
342
|
// setStatus/setTitle can also throw if the runner went stale mid-call.
|
|
352
343
|
}
|
|
353
344
|
}
|
|
354
|
-
// Epoch ms when the state machine entered 'started' (last /unbien start).
|
|
355
|
-
// Used by session_sync to let the app detect Pi restarts (and force a full
|
|
356
|
-
// replay). Cleared on _goIdle.
|
|
357
|
-
let _sessionStartedAt = null;
|
|
358
345
|
let _pendingMeshMessages = [];
|
|
359
346
|
// agent-run active/generation now live PER-SESSION in _stateFor(sid).agentRun;
|
|
360
347
|
// mesh delivery targets the ROOT run, so the drain reads _rootState().agentRun.
|
|
361
348
|
let _meshDrainScheduled = false;
|
|
349
|
+
/** A freshly-zeroed session record (root or subagent child). */
|
|
350
|
+
function _freshSessionState() {
|
|
351
|
+
return {
|
|
352
|
+
turnId: null,
|
|
353
|
+
working: false,
|
|
354
|
+
agentRun: { active: false, generation: 0 },
|
|
355
|
+
sessionManager: null,
|
|
356
|
+
model: null,
|
|
357
|
+
thinking: null,
|
|
358
|
+
currentModel: null,
|
|
359
|
+
myRoomId: null,
|
|
360
|
+
myRoomMeta: null,
|
|
361
|
+
sessionStartedAt: null,
|
|
362
|
+
baseCtx: null,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
// The ROOT session's state — a SINGLE stable record for the process lifetime,
|
|
366
|
+
// NOT an entry keyed by the (rotating) session id. The relay-room projection
|
|
367
|
+
// fields (myRoomId/myRoomMeta/sessionStartedAt) outlive a session replacement
|
|
368
|
+
// by contract: _goIdle preserves them across stop/start, and session_shutdown
|
|
369
|
+
// does not clear them — the relay room survives a New/Fork/Reload within the
|
|
370
|
+
// same process. Keying them on _rootSessionId (as the prior refactor did) reset
|
|
371
|
+
// them to null on every sid rotation, opening a window between session_start
|
|
372
|
+
// and the async _cmdStart where a reconnect/sync announced a NAMELESS room —
|
|
373
|
+
// the relay filled the absent room_meta.name with its default placeholder.
|
|
374
|
+
// session_start now POPULATES the per-session fields (sessionManager/baseCtx/
|
|
375
|
+
// model/thinking/…) in place; session_shutdown clears exactly those. The record
|
|
376
|
+
// itself is never recreated, so the projection rides through a replacement.
|
|
377
|
+
let _rootRecord = _freshSessionState();
|
|
378
|
+
// Subagent (non-root) child records, keyed by their own session id. The root
|
|
379
|
+
// is never an entry here — root access routes through _rootRecord.
|
|
362
380
|
const _sessions = new Map();
|
|
363
381
|
// The session bound to the app room. null until the ROOT session_start fires;
|
|
364
382
|
// while null, everything is treated as root (single-session / test harness).
|
|
@@ -368,23 +386,24 @@ function _rootKey() {
|
|
|
368
386
|
return _rootSessionId ?? "__root__";
|
|
369
387
|
}
|
|
370
388
|
function _stateFor(sid) {
|
|
389
|
+
if (sid === _rootKey())
|
|
390
|
+
return _rootRecord; // root → stable singleton
|
|
371
391
|
let st = _sessions.get(sid);
|
|
372
392
|
if (!st) {
|
|
373
|
-
st =
|
|
374
|
-
turnId: null,
|
|
375
|
-
working: false,
|
|
376
|
-
agentRun: { active: false, generation: 0 },
|
|
377
|
-
sessionManager: null,
|
|
378
|
-
model: null,
|
|
379
|
-
thinking: null,
|
|
380
|
-
};
|
|
393
|
+
st = _freshSessionState();
|
|
381
394
|
_sessions.set(sid, st);
|
|
382
395
|
}
|
|
383
396
|
return st;
|
|
384
397
|
}
|
|
385
|
-
/** The root session's record (
|
|
398
|
+
/** The root session's record (stable for the process lifetime). */
|
|
386
399
|
function _rootState() {
|
|
387
|
-
return
|
|
400
|
+
return _rootRecord;
|
|
401
|
+
}
|
|
402
|
+
/** Test-only: replace the root record with a fresh slate (projection fields
|
|
403
|
+
* included) so tests don't leak state across boundaries. Production never
|
|
404
|
+
* calls this — session_shutdown clears per-session fields in place instead. */
|
|
405
|
+
function _resetRootRecordForTest() {
|
|
406
|
+
_rootRecord = _freshSessionState();
|
|
388
407
|
}
|
|
389
408
|
/** sessionId of the firing handler's ctx, defaulting to the root key. */
|
|
390
409
|
function _sidOf(ctx) {
|
|
@@ -460,19 +479,6 @@ export function _getState() {
|
|
|
460
479
|
// underlying BUS events and lets the REAL bridges produce the frames (faithful);
|
|
461
480
|
// the simple ExtensionUIPromptView methods (select/confirm/input/editor) + media
|
|
462
481
|
// have no bus producer, so they're broadcast directly. See design 01M152YD….
|
|
463
|
-
const _TEST_SVG_B64 = Buffer.from('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 60">' +
|
|
464
|
-
'<rect width="120" height="60" rx="8" fill="#4c8bf5"/>' +
|
|
465
|
-
'<text x="60" y="37" font-size="16" fill="white" text-anchor="middle"' +
|
|
466
|
-
' font-family="sans-serif">un-bien</text></svg>').toString("base64");
|
|
467
|
-
function _emitTestBus(channel, data) {
|
|
468
|
-
try {
|
|
469
|
-
;
|
|
470
|
-
_pi?.events?.emit(channel, data);
|
|
471
|
-
}
|
|
472
|
-
catch {
|
|
473
|
-
/* bus absent — best effort */
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
482
|
// Owner-notify bridge (transient toasts on the app): /unbien command handlers
|
|
477
483
|
// report through notifyOwners() - this broadcasts the notice to every attached
|
|
478
484
|
// owner as extension_ui_request {method:"notify"}, the same shape the ask-flow
|
|
@@ -488,628 +494,34 @@ setOwnerNotifyHook((message, level) => {
|
|
|
488
494
|
},
|
|
489
495
|
});
|
|
490
496
|
});
|
|
491
|
-
/** Run one canned UI-test scenario. Returns a short status for the notify. */
|
|
492
|
-
function _runTestScenario(scenario) {
|
|
493
|
-
const s = (scenario.trim().split(/\s+/)[0] || "help").toLowerCase();
|
|
494
|
-
const id = `test-${Date.now()}`;
|
|
495
|
-
switch (s) {
|
|
496
|
-
case "ask-select":
|
|
497
|
-
_broadcastEnvelope(relayDeps, {
|
|
498
|
-
rpc: {
|
|
499
|
-
type: "extension_ui_request",
|
|
500
|
-
id,
|
|
501
|
-
method: "select",
|
|
502
|
-
title: "Pick one (test)",
|
|
503
|
-
options: ["Alpha", "Beta", "Gamma"],
|
|
504
|
-
},
|
|
505
|
-
});
|
|
506
|
-
return "sent ask-select";
|
|
507
|
-
case "ask-confirm":
|
|
508
|
-
_broadcastEnvelope(relayDeps, {
|
|
509
|
-
rpc: {
|
|
510
|
-
type: "extension_ui_request",
|
|
511
|
-
id,
|
|
512
|
-
method: "confirm",
|
|
513
|
-
title: "Confirm (test)",
|
|
514
|
-
message: "Proceed with the test action?",
|
|
515
|
-
},
|
|
516
|
-
});
|
|
517
|
-
return "sent ask-confirm";
|
|
518
|
-
case "ask-input":
|
|
519
|
-
_broadcastEnvelope(relayDeps, {
|
|
520
|
-
rpc: {
|
|
521
|
-
type: "extension_ui_request",
|
|
522
|
-
id,
|
|
523
|
-
method: "input",
|
|
524
|
-
title: "Input (test)",
|
|
525
|
-
placeholder: "Type something…",
|
|
526
|
-
},
|
|
527
|
-
});
|
|
528
|
-
return "sent ask-input";
|
|
529
|
-
case "ask-editor":
|
|
530
|
-
_broadcastEnvelope(relayDeps, {
|
|
531
|
-
rpc: {
|
|
532
|
-
type: "extension_ui_request",
|
|
533
|
-
id,
|
|
534
|
-
method: "editor",
|
|
535
|
-
title: "Editor (test)",
|
|
536
|
-
prefill: "edit me",
|
|
537
|
-
},
|
|
538
|
-
});
|
|
539
|
-
return "sent ask-editor";
|
|
540
|
-
case "ask-notify":
|
|
541
|
-
_broadcastEnvelope(relayDeps, {
|
|
542
|
-
rpc: {
|
|
543
|
-
type: "extension_ui_request",
|
|
544
|
-
id,
|
|
545
|
-
method: "notify",
|
|
546
|
-
message: "This is a test notice.",
|
|
547
|
-
notify_type: "info",
|
|
548
|
-
},
|
|
549
|
-
});
|
|
550
|
-
return "sent ask-notify";
|
|
551
|
-
case "ask-rich":
|
|
552
|
-
_emitTestBus("@eko24ive/pi-ask:started", {
|
|
553
|
-
version: 1,
|
|
554
|
-
flowId: `test-flow-${Date.now()}`,
|
|
555
|
-
source: "test",
|
|
556
|
-
title: "Rich ask (test)",
|
|
557
|
-
questions: [
|
|
558
|
-
{
|
|
559
|
-
id: "q1",
|
|
560
|
-
prompt: "Which approach?",
|
|
561
|
-
type: "single",
|
|
562
|
-
options: [
|
|
563
|
-
{ value: "a", label: "Approach A", description: "the safe one" },
|
|
564
|
-
{ value: "b", label: "Approach B", preview: "preview text here" },
|
|
565
|
-
],
|
|
566
|
-
},
|
|
567
|
-
{
|
|
568
|
-
id: "q2",
|
|
569
|
-
prompt: "Anything to add?",
|
|
570
|
-
type: "single",
|
|
571
|
-
options: [{ value: "ok", label: "Looks good", freeform: true }],
|
|
572
|
-
},
|
|
573
|
-
],
|
|
574
|
-
});
|
|
575
|
-
return "emitted pi-ask:started (rich)";
|
|
576
|
-
case "plan":
|
|
577
|
-
_emitTestBus("plan:snapshot", {
|
|
578
|
-
ns: "test",
|
|
579
|
-
seq: 1,
|
|
580
|
-
items: [
|
|
581
|
-
{
|
|
582
|
-
id: "t1",
|
|
583
|
-
kind: "plan",
|
|
584
|
-
title: "Design the thing",
|
|
585
|
-
status: "done",
|
|
586
|
-
deps: [],
|
|
587
|
-
},
|
|
588
|
-
{
|
|
589
|
-
id: "t2",
|
|
590
|
-
kind: "plan",
|
|
591
|
-
title: "Build the thing",
|
|
592
|
-
status: "in_progress",
|
|
593
|
-
deps: ["t1"],
|
|
594
|
-
},
|
|
595
|
-
{
|
|
596
|
-
id: "t3",
|
|
597
|
-
kind: "plan",
|
|
598
|
-
title: "Test the thing",
|
|
599
|
-
status: "pending",
|
|
600
|
-
deps: ["t2"],
|
|
601
|
-
},
|
|
602
|
-
],
|
|
603
|
-
});
|
|
604
|
-
return "emitted plan:snapshot";
|
|
605
|
-
case "subagents":
|
|
606
|
-
_emitTestBus("subagents:created", {
|
|
607
|
-
id: "sa1",
|
|
608
|
-
type: "explore",
|
|
609
|
-
description: "Explore the codebase",
|
|
610
|
-
});
|
|
611
|
-
_emitTestBus("subagents:started", { id: "sa1" });
|
|
612
|
-
_emitTestBus("subagents:created", {
|
|
613
|
-
id: "sa2",
|
|
614
|
-
type: "plan",
|
|
615
|
-
description: "Draft an implementation plan",
|
|
616
|
-
});
|
|
617
|
-
_emitTestBus("subagents:completed", { id: "sa2" });
|
|
618
|
-
return "emitted subagents lifecycle";
|
|
619
|
-
case "svg": {
|
|
620
|
-
// A TOOL card renders standalone; the app pulls tool-emitted images from
|
|
621
|
-
// INSIDE the tool_execution_end `result` (imagesFromToolResult unwraps
|
|
622
|
-
// `{content:[{type:"image",data,mimeType}]}`) and renders them below the
|
|
623
|
-
// card (WireImageView -> SVGImageView). Deliver the SVG that way.
|
|
624
|
-
const tc = `tc-svg-${Date.now()}`;
|
|
625
|
-
_broadcastEnvelope(relayDeps, { rpc: { type: "turn_start" } });
|
|
626
|
-
_broadcastEnvelope(relayDeps, {
|
|
627
|
-
rpc: {
|
|
628
|
-
type: "tool_execution_start",
|
|
629
|
-
toolCallId: tc,
|
|
630
|
-
toolName: "render_svg",
|
|
631
|
-
args: { note: "test svg" },
|
|
632
|
-
},
|
|
633
|
-
});
|
|
634
|
-
_broadcastEnvelope(relayDeps, {
|
|
635
|
-
rpc: {
|
|
636
|
-
type: "tool_execution_end",
|
|
637
|
-
toolCallId: tc,
|
|
638
|
-
result: {
|
|
639
|
-
content: [
|
|
640
|
-
{ type: "text", text: "rendered a test SVG" },
|
|
641
|
-
{
|
|
642
|
-
type: "image",
|
|
643
|
-
data: _TEST_SVG_B64,
|
|
644
|
-
mimeType: "image/svg+xml",
|
|
645
|
-
},
|
|
646
|
-
],
|
|
647
|
-
},
|
|
648
|
-
isError: false,
|
|
649
|
-
},
|
|
650
|
-
});
|
|
651
|
-
_broadcastEnvelope(relayDeps, { rpc: { type: "agent_settled" } });
|
|
652
|
-
return "sent svg (envelope tool_execution_end + image)";
|
|
653
|
-
}
|
|
654
|
-
case "tool": {
|
|
655
|
-
const tc = `tc-${Date.now()}`;
|
|
656
|
-
_broadcastEnvelope(relayDeps, { rpc: { type: "turn_start" } });
|
|
657
|
-
_broadcastEnvelope(relayDeps, {
|
|
658
|
-
rpc: {
|
|
659
|
-
type: "tool_execution_start",
|
|
660
|
-
toolCallId: tc,
|
|
661
|
-
toolName: "bash",
|
|
662
|
-
args: { command: "echo hello" },
|
|
663
|
-
},
|
|
664
|
-
});
|
|
665
|
-
_broadcastEnvelope(relayDeps, {
|
|
666
|
-
rpc: {
|
|
667
|
-
type: "tool_execution_end",
|
|
668
|
-
toolCallId: tc,
|
|
669
|
-
result: "hello\n",
|
|
670
|
-
isError: false,
|
|
671
|
-
},
|
|
672
|
-
});
|
|
673
|
-
_broadcastEnvelope(relayDeps, { rpc: { type: "agent_settled" } });
|
|
674
|
-
return "sent tool pair (envelope)";
|
|
675
|
-
}
|
|
676
|
-
case "diff": {
|
|
677
|
-
// Exercise the rich diff rendering: aux `{hunks}` (input Edit diff) rides
|
|
678
|
-
// ALONGSIDE the raw edit `tool_execution_start`. OUTPUT is classified
|
|
679
|
-
// app-side from the result, so no aux.output rides the end frame.
|
|
680
|
-
const tc = `tc-diff-${Date.now()}`;
|
|
681
|
-
const hunks = [
|
|
682
|
-
{
|
|
683
|
-
lines: [
|
|
684
|
-
{ kind: "context", oldLine: 1, newLine: 1, text: "const a = 1;" },
|
|
685
|
-
{ kind: "remove", oldLine: 2, text: "const b = 2;" },
|
|
686
|
-
{ kind: "add", newLine: 2, text: "const b = 3;" },
|
|
687
|
-
{ kind: "context", oldLine: 3, newLine: 3, text: "const c = 4;" },
|
|
688
|
-
],
|
|
689
|
-
},
|
|
690
|
-
];
|
|
691
|
-
_broadcastEnvelope(relayDeps, { rpc: { type: "turn_start" } });
|
|
692
|
-
_broadcastEnvelope(relayDeps, {
|
|
693
|
-
rpc: {
|
|
694
|
-
type: "tool_execution_start",
|
|
695
|
-
toolCallId: tc,
|
|
696
|
-
toolName: "edit",
|
|
697
|
-
args: {
|
|
698
|
-
path: "demo.ts",
|
|
699
|
-
old_string: "const b = 2;",
|
|
700
|
-
new_string: "const b = 3;",
|
|
701
|
-
},
|
|
702
|
-
},
|
|
703
|
-
aux: { hunks },
|
|
704
|
-
});
|
|
705
|
-
_broadcastEnvelope(relayDeps, {
|
|
706
|
-
rpc: {
|
|
707
|
-
type: "tool_execution_end",
|
|
708
|
-
toolCallId: tc,
|
|
709
|
-
result: "edited demo.ts",
|
|
710
|
-
isError: false,
|
|
711
|
-
},
|
|
712
|
-
});
|
|
713
|
-
_broadcastEnvelope(relayDeps, { rpc: { type: "agent_settled" } });
|
|
714
|
-
return "sent diff (edit + aux hunks — shows the Diff⇄Content toggle)";
|
|
715
|
-
}
|
|
716
|
-
case "code-shell": {
|
|
717
|
-
// bash-family result → the app classifies it into a `code` block (lang
|
|
718
|
-
// shell), syntax-highlighted. OUTPUT is app-side now — no aux stamped.
|
|
719
|
-
const tc = `tc-sh-${Date.now()}`;
|
|
720
|
-
_broadcastEnvelope(relayDeps, { rpc: { type: "turn_start" } });
|
|
721
|
-
_broadcastEnvelope(relayDeps, {
|
|
722
|
-
rpc: {
|
|
723
|
-
type: "tool_execution_start",
|
|
724
|
-
toolCallId: tc,
|
|
725
|
-
toolName: "bash",
|
|
726
|
-
args: { command: "ls -la" },
|
|
727
|
-
},
|
|
728
|
-
});
|
|
729
|
-
_broadcastEnvelope(relayDeps, {
|
|
730
|
-
rpc: {
|
|
731
|
-
type: "tool_execution_end",
|
|
732
|
-
toolCallId: tc,
|
|
733
|
-
result: "total 24\ndrwxr-xr-x 5 geo staff 160 Aug 29 10:00 .\n-rw-r--r-- 1 geo staff 1024 index.ts\n-rw-r--r-- 1 geo staff 512 README.md",
|
|
734
|
-
isError: false,
|
|
735
|
-
},
|
|
736
|
-
});
|
|
737
|
-
_broadcastEnvelope(relayDeps, { rpc: { type: "agent_settled" } });
|
|
738
|
-
return "sent code-shell (bash output → code block, lang shell)";
|
|
739
|
-
}
|
|
740
|
-
case "code-file": {
|
|
741
|
-
// read-family with a *.swift path → `code` block, lang inferred from the
|
|
742
|
-
// extension (swift) and highlighted.
|
|
743
|
-
const tc = `tc-rd-${Date.now()}`;
|
|
744
|
-
_broadcastEnvelope(relayDeps, { rpc: { type: "turn_start" } });
|
|
745
|
-
_broadcastEnvelope(relayDeps, {
|
|
746
|
-
rpc: {
|
|
747
|
-
type: "tool_execution_start",
|
|
748
|
-
toolCallId: tc,
|
|
749
|
-
toolName: "read",
|
|
750
|
-
args: { path: "/src/Greeter.swift" },
|
|
751
|
-
},
|
|
752
|
-
});
|
|
753
|
-
_broadcastEnvelope(relayDeps, {
|
|
754
|
-
rpc: {
|
|
755
|
-
type: "tool_execution_end",
|
|
756
|
-
toolCallId: tc,
|
|
757
|
-
result: 'struct Greeter {\n let name: String\n func greet() -> String {\n return "Hello, \\(name)!"\n }\n}',
|
|
758
|
-
isError: false,
|
|
759
|
-
},
|
|
760
|
-
});
|
|
761
|
-
_broadcastEnvelope(relayDeps, { rpc: { type: "agent_settled" } });
|
|
762
|
-
return "sent code-file (read .swift → highlighted code block)";
|
|
763
|
-
}
|
|
764
|
-
case "diff-output": {
|
|
765
|
-
// A tool whose RESULT already embeds a unified diff → the app parses it
|
|
766
|
-
// into a `diff` block (re-reading persisted text; replay-safe).
|
|
767
|
-
const tc = `tc-do-${Date.now()}`;
|
|
768
|
-
_broadcastEnvelope(relayDeps, { rpc: { type: "turn_start" } });
|
|
769
|
-
_broadcastEnvelope(relayDeps, {
|
|
770
|
-
rpc: {
|
|
771
|
-
type: "tool_execution_start",
|
|
772
|
-
toolCallId: tc,
|
|
773
|
-
toolName: "bash",
|
|
774
|
-
args: { command: "git diff" },
|
|
775
|
-
},
|
|
776
|
-
});
|
|
777
|
-
_broadcastEnvelope(relayDeps, {
|
|
778
|
-
rpc: {
|
|
779
|
-
type: "tool_execution_end",
|
|
780
|
-
toolCallId: tc,
|
|
781
|
-
result: 'diff --git a/app.ts b/app.ts\n--- a/app.ts\n+++ b/app.ts\n@@ -1,3 +1,3 @@\n const port = 3000;\n-const host = "127.0.0.1";\n+const host = "0.0.0.0";\n start(host, port);',
|
|
782
|
-
isError: false,
|
|
783
|
-
},
|
|
784
|
-
});
|
|
785
|
-
_broadcastEnvelope(relayDeps, { rpc: { type: "agent_settled" } });
|
|
786
|
-
return "sent diff-output (result embeds a unified diff → diff block)";
|
|
787
|
-
}
|
|
788
|
-
case "write": {
|
|
789
|
-
// write carries the new file text in args.content; no live diff → the
|
|
790
|
-
// card shows the Content view (new text as a code block, replay-safe).
|
|
791
|
-
const tc = `tc-wr-${Date.now()}`;
|
|
792
|
-
const content = "export function add(a: number, b: number): number {\n return a + b;\n}";
|
|
793
|
-
_broadcastEnvelope(relayDeps, { rpc: { type: "turn_start" } });
|
|
794
|
-
_broadcastEnvelope(relayDeps, {
|
|
795
|
-
rpc: {
|
|
796
|
-
type: "tool_execution_start",
|
|
797
|
-
toolCallId: tc,
|
|
798
|
-
toolName: "write",
|
|
799
|
-
args: { path: "/src/math.ts", content },
|
|
800
|
-
},
|
|
801
|
-
});
|
|
802
|
-
_broadcastEnvelope(relayDeps, {
|
|
803
|
-
rpc: {
|
|
804
|
-
type: "tool_execution_end",
|
|
805
|
-
toolCallId: tc,
|
|
806
|
-
result: "wrote /src/math.ts",
|
|
807
|
-
isError: false,
|
|
808
|
-
},
|
|
809
|
-
});
|
|
810
|
-
_broadcastEnvelope(relayDeps, { rpc: { type: "agent_settled" } });
|
|
811
|
-
return "sent write (args.content → content-as-code block)";
|
|
812
|
-
}
|
|
813
|
-
case "agent": {
|
|
814
|
-
_broadcastEnvelope(relayDeps, { rpc: { type: "turn_start" } });
|
|
815
|
-
_broadcastEnvelope(relayDeps, {
|
|
816
|
-
rpc: {
|
|
817
|
-
type: "message_update",
|
|
818
|
-
assistantMessageEvent: {
|
|
819
|
-
type: "text_delta",
|
|
820
|
-
delta: "This is a ",
|
|
821
|
-
},
|
|
822
|
-
},
|
|
823
|
-
});
|
|
824
|
-
_broadcastEnvelope(relayDeps, {
|
|
825
|
-
rpc: {
|
|
826
|
-
type: "message_update",
|
|
827
|
-
assistantMessageEvent: {
|
|
828
|
-
type: "text_delta",
|
|
829
|
-
delta: "test agent message.",
|
|
830
|
-
},
|
|
831
|
-
},
|
|
832
|
-
});
|
|
833
|
-
_broadcastEnvelope(relayDeps, {
|
|
834
|
-
rpc: {
|
|
835
|
-
type: "message_end",
|
|
836
|
-
message: {
|
|
837
|
-
role: "assistant",
|
|
838
|
-
content: [{ type: "text", text: "This is a test agent message." }],
|
|
839
|
-
},
|
|
840
|
-
},
|
|
841
|
-
});
|
|
842
|
-
_broadcastEnvelope(relayDeps, { rpc: { type: "agent_settled" } });
|
|
843
|
-
return "sent agent message (envelope)";
|
|
844
|
-
}
|
|
845
|
-
case "error":
|
|
846
|
-
_broadcastEnvelope(relayDeps, {
|
|
847
|
-
rpc: {
|
|
848
|
-
type: "message_end",
|
|
849
|
-
message: {
|
|
850
|
-
role: "assistant",
|
|
851
|
-
stopReason: "error",
|
|
852
|
-
errorMessage: "This is a test error.",
|
|
853
|
-
},
|
|
854
|
-
},
|
|
855
|
-
});
|
|
856
|
-
_broadcastEnvelope(relayDeps, { rpc: { type: "agent_settled" } });
|
|
857
|
-
return "sent error (envelope message_end/error)";
|
|
858
|
-
case "all":
|
|
859
|
-
for (const sc of [
|
|
860
|
-
"ask-notify",
|
|
861
|
-
"plan",
|
|
862
|
-
"subagents",
|
|
863
|
-
"svg",
|
|
864
|
-
"tool",
|
|
865
|
-
"diff",
|
|
866
|
-
"code-shell",
|
|
867
|
-
"code-file",
|
|
868
|
-
"diff-output",
|
|
869
|
-
"write",
|
|
870
|
-
"agent",
|
|
871
|
-
"error",
|
|
872
|
-
])
|
|
873
|
-
_runTestScenario(sc);
|
|
874
|
-
return "sent all (ask-notify, plan, subagents, svg, tool, diff, code-shell, code-file, diff-output, write, agent, error)";
|
|
875
|
-
default:
|
|
876
|
-
return "usage: /unbien test <ask-select|ask-confirm|ask-input|ask-editor|ask-notify|ask-rich|plan|subagents|svg|tool|diff|code-shell|code-file|diff-output|write|agent|error|all>";
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
497
|
/**
|
|
880
|
-
*
|
|
881
|
-
*
|
|
882
|
-
*
|
|
883
|
-
* SDK primitives (`_wakeAgent`, `_abortCurrentTurn`) are pi, not old protocol.
|
|
498
|
+
* Plane routers live in ./routing.ts (carved, slice 4). These thin wrappers
|
|
499
|
+
* preserve the call sites + the CommandDeps surface; the deps accessors
|
|
500
|
+
* keep module state read LIVE at each dispatch.
|
|
884
501
|
*/
|
|
885
502
|
function _routeRpcCommandFrom(sender, env) {
|
|
886
|
-
|
|
887
|
-
if (!frame || typeof frame !== "object")
|
|
888
|
-
return; // no {evt} inbound today
|
|
889
|
-
envLog(`rpc inbound: ${String(frame.type)}`);
|
|
890
|
-
// extension_ui_response is a reply to an extension-issued dialog, not a command —
|
|
891
|
-
// route it straight to the ui bridge (same target as the stock path).
|
|
892
|
-
if (frame.type === "extension_ui_response") {
|
|
893
|
-
// SAFETY: the type-discriminator check directly above proves this frame is
|
|
894
|
-
// an extension_ui_response envelope, which is the ExtensionUiResponseWire shape.
|
|
895
|
-
_extensionUiBridge?.respond(frame);
|
|
896
|
-
return;
|
|
897
|
-
}
|
|
898
|
-
// session_sync (reconstruction) is un-bien's OWN protocol — dispatched on the
|
|
899
|
-
// un plane by _routeUnBienPlaneFrom, NOT here. Only byte-faithful pi rpc
|
|
900
|
-
// commands + extension_ui_response ride this rpc dispatch.
|
|
901
|
-
const handlers = createRpcHandlers(rpcDeps, sender);
|
|
902
|
-
void dispatchRpcCommand(frame, handlers)
|
|
903
|
-
.then((resp) => {
|
|
904
|
-
// Envelope-native ONLY: no stock fallback. An unhandled rpc type is
|
|
905
|
-
// ignored (forward-compat). un-bien's own commands (session_sync,
|
|
906
|
-
// session_launch) ride the un plane via _routeUnBienPlaneFrom.
|
|
907
|
-
if (resp)
|
|
908
|
-
sender.sendEnvelope(resp);
|
|
909
|
-
})
|
|
910
|
-
.catch((err) => {
|
|
911
|
-
console.error(`[un-bien] rpc inbound dispatch failed: ${String(err)}`);
|
|
912
|
-
});
|
|
503
|
+
routeRpcCommand(planeDeps, sender, env);
|
|
913
504
|
}
|
|
914
|
-
/**
|
|
915
|
-
* un-bien plane inbound (`type:"ub"`): dispatch un-bien's OWN protocol frames by
|
|
916
|
-
* their inner `.type`. app->ext today: `session_sync` (reconstruction request)
|
|
917
|
-
* and `session_launch` (mesh remote-launch). These are NOT pi rpc — the
|
|
918
|
-
* EXTENSION acts. The reconstruction REPLAY frames it emits stay byte-faithful
|
|
919
|
-
* pi rpc frames on the rpc plane; only the request + `session_sync_end`
|
|
920
|
-
* terminator are un-plane frames.
|
|
921
|
-
*/
|
|
922
505
|
function _routeUnBienPlaneFrom(sender, env) {
|
|
923
|
-
|
|
924
|
-
if (!frame || typeof frame !== "object")
|
|
925
|
-
return;
|
|
926
|
-
const type = frame.type;
|
|
927
|
-
envLog(`ub inbound: ${String(type)}`);
|
|
928
|
-
if (type === "session_sync") {
|
|
929
|
-
const f = frame;
|
|
930
|
-
// session_sync now carries ONLY un-bien's NON-rpc display state: panels +
|
|
931
|
-
// pending extension_ui. The TRANSCRIPT is the app's OWN native get_entries
|
|
932
|
-
// rpc (reduced by SessionState.applyEntries) — NOT replayed here. Design
|
|
933
|
-
// 01M15FMQ: separate the rpc transcript (get_entries) from un-bien panel/ui
|
|
934
|
-
// state, each an independent app-driven request issued on open + reconnect.
|
|
935
|
-
for (const req of _extensionUiBridge?.pendingRequests() ?? [])
|
|
936
|
-
// ENVELOPE the replay: the app's handleRouted only dispatches
|
|
937
|
-
// {rpc|evt|ub} frames — a bare extension_ui_request fell through both
|
|
938
|
-
// branches and was silently dropped, so every session_sync replay was
|
|
939
|
-
// a no-op and the terminator then retired the still-pending prompt as
|
|
940
|
-
// "stale" (the "ask not displayed on phone" bug, 2026-09-25). The live
|
|
941
|
-
// ask path sends the same shape enveloped ({rpc}); mirror it here.
|
|
942
|
-
sender.sendEnvelope({ rpc: req });
|
|
943
|
-
const panels = _panelBridge?.pendingPanels() ?? [];
|
|
944
|
-
for (const panel of panels)
|
|
945
|
-
sender.sendEnvelope({ evt: { channel: "panel", data: panel } });
|
|
946
|
-
envLog(`session_sync(ub): panels=${panels.length} + ui (transcript is the app's get_entries rpc)`);
|
|
947
|
-
// Terminator/ack on the ub plane; carries the session clock so the app can
|
|
948
|
-
// detect a pi restart. `truncated`/`limit` are gone (a replay concern;
|
|
949
|
-
// get_entries is unbounded / since-delta). `session_name` (pre-release
|
|
950
|
-
// 2026-09-18): the NAME PULL — heals a missed session_info_changed push
|
|
951
|
-
// (an iOS backgrounded socket misses live frames; the relay's stored meta
|
|
952
|
-
// is stale until our next hello, so reconnects can't carry it either).
|
|
953
|
-
// session_sync is issued on EVERY open + reconnect, so the name heals on
|
|
954
|
-
// every natural cycle. Old apps ignore the field; old extensions omit it.
|
|
955
|
-
//
|
|
956
|
-
// SOURCE = the room's DISPLAY name (myRoomMeta.name), NOT deps.sessionName:
|
|
957
|
-
// the latter is `_sessionName`, which `_cmdJoin` sets to the mesh session
|
|
958
|
-
// id LOCAL_SESSION_NAME ("local"). Sending that here clobbered the tile
|
|
959
|
-
// label with "local" on every reconnect (the flaky-name bug) — the
|
|
960
|
-
// non-live pull disagreed with the live session_info push. myRoomMeta.name
|
|
961
|
-
// is the same value pair_ok sends (deps.displayName) and tracks live
|
|
962
|
-
// renames (session_info_changed updates it), so pull == push now.
|
|
963
|
-
const displayName = deps.myRoomMeta?.name;
|
|
964
|
-
// Fork auto-nav: a fork-born session's FIRST sync echoes the app's
|
|
965
|
-
// originating fork request id (design: fork switch). take = read+unlink,
|
|
966
|
-
// so it fires once (the app navigates on first receipt; later reconnects
|
|
967
|
-
// must not re-jump). Keyed by the session id, so it survives the extension
|
|
968
|
-
// module re-eval that ctx.fork triggers.
|
|
969
|
-
const forkSm = _rootState().sessionManager;
|
|
970
|
-
const forkedFromReq = forkSm && typeof forkSm.getSessionDir === "function"
|
|
971
|
-
? takeForkLink(forkSm.getSessionDir(), forkSm.getSessionId())
|
|
972
|
-
: undefined;
|
|
973
|
-
sender.sendEnvelope({
|
|
974
|
-
ub: {
|
|
975
|
-
type: "session_sync_end",
|
|
976
|
-
...(typeof f.id === "string" ? { in_reply_to: f.id } : {}),
|
|
977
|
-
session_started_at: _sessionStartedAt ?? 0,
|
|
978
|
-
...(displayName ? { session_name: displayName } : {}),
|
|
979
|
-
...(forkedFromReq ? { forked_from_req: forkedFromReq } : {}),
|
|
980
|
-
},
|
|
981
|
-
});
|
|
982
|
-
return;
|
|
983
|
-
}
|
|
984
|
-
if (type === "terminate") {
|
|
985
|
-
// app->ext: kill THIS session. Root room only — a child room's terminate
|
|
986
|
-
// is handled by the child-room router in subagent_rooms (never reaches
|
|
987
|
-
// here; that room's conn dispatches its own ub frames). Gate on local
|
|
988
|
-
// config; the app confirms + shows red before sending.
|
|
989
|
-
if (!effectiveAllowRemoteTerminate(loadLocalConfig(process.cwd()))) {
|
|
990
|
-
envLog("terminate(ub): remote terminate disabled on this machine");
|
|
991
|
-
return;
|
|
992
|
-
}
|
|
993
|
-
envLog(`terminate(ub): graceful host shutdown requested${typeof frame.reason === "string" ? ` (${String(frame.reason)})` : ""}`);
|
|
994
|
-
_safeNotify("Terminated from the app", "warning");
|
|
995
|
-
try {
|
|
996
|
-
// ctx.shutdown() IS the /quit equivalent — pi's own source lists
|
|
997
|
-
// "extension shutdown()" alongside Ctrl+D / /quit (interactive-mode.js
|
|
998
|
-
// 3229: same graceful this.shutdown(): drain input, ui.stop() restores
|
|
999
|
-
// cooked mode + cursor, extension disposal, resume hint, exit). It sits
|
|
1000
|
-
// DIRECTLY on the event ctx (ExtensionContext.shutdown — there is no
|
|
1001
|
-
// ctx.actions sub-object; the first two attempts failed exactly there).
|
|
1002
|
-
//
|
|
1003
|
-
// MID-TURN DEFERRAL: the interactive handler fires immediately only when
|
|
1004
|
-
// idle; otherwise it sets `shutdownRequested` and waits for
|
|
1005
|
-
// `agent_settled` — which never arrives when nothing is running. So
|
|
1006
|
-
// abort() FIRST: aborting a running turn forces the settle that
|
|
1007
|
-
// re-checks the flag; on an idle session abort is a no-op and the
|
|
1008
|
-
// idle fast-path fires directly.
|
|
1009
|
-
//
|
|
1010
|
-
// NO process.exit fallback, ever: a raw exit leaves the TTY in raw
|
|
1011
|
-
// mode (keypresses echo escape codes until `stty sane`). No live ctx =>
|
|
1012
|
-
// log loudly and stay up; the user quits on the machine.
|
|
1013
|
-
const ctx = _liveCtx();
|
|
1014
|
-
if (ctx?.shutdown) {
|
|
1015
|
-
ctx.abort?.();
|
|
1016
|
-
ctx.shutdown();
|
|
1017
|
-
}
|
|
1018
|
-
else {
|
|
1019
|
-
envLog("terminate(ub): no live ctx — refusing raw exit (would leave the terminal in raw mode); staying up");
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
catch (err) {
|
|
1023
|
-
envLog(`terminate(ub) error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1024
|
-
}
|
|
1025
|
-
return;
|
|
1026
|
-
}
|
|
1027
|
-
if (type === "close_child_room") {
|
|
1028
|
-
// app->ext(PARENT): permanently close one of MY child rooms (done-subagent
|
|
1029
|
-
// removal — the room lingers by design otherwise). Tombstone + dispose in
|
|
1030
|
-
// subagent_rooms; the relay fires room_ended when the last conn drops.
|
|
1031
|
-
if (!effectiveAllowRemoteTerminate(loadLocalConfig(process.cwd()))) {
|
|
1032
|
-
envLog("close_child_room(ub): remote terminate disabled on this machine");
|
|
1033
|
-
return;
|
|
1034
|
-
}
|
|
1035
|
-
const roomId = frame.room_id;
|
|
1036
|
-
if (typeof roomId !== "string" || roomId.length === 0)
|
|
1037
|
-
return;
|
|
1038
|
-
const closed = _subagentRooms?.closeChildRoom(roomId) ?? false;
|
|
1039
|
-
envLog(`close_child_room(ub): room=${roomId.slice(0, 8)} closed=${closed}`);
|
|
1040
|
-
return;
|
|
1041
|
-
}
|
|
1042
|
-
if (type === "session_launch") {
|
|
1043
|
-
const f = frame;
|
|
1044
|
-
const cwd = _expandTilde(typeof f.cwd === "string" && f.cwd.length > 0 ? f.cwd : process.cwd());
|
|
1045
|
-
if (!effectiveAllowRemoteLaunch(loadLocalConfig(cwd))) {
|
|
1046
|
-
envLog("session_launch(ub): remote launch disabled on this machine");
|
|
1047
|
-
return;
|
|
1048
|
-
}
|
|
1049
|
-
// Directory allow-list (design 01M211VW9), read FRESH each request.
|
|
1050
|
-
if (!launchDirAllowed(cwd, loadConfig().launch?.dirs)) {
|
|
1051
|
-
envLog("session_launch(ub): cwd not in launch.dirs allow-list");
|
|
1052
|
-
sender.send({
|
|
1053
|
-
type: "error",
|
|
1054
|
-
code: "permission_denied",
|
|
1055
|
-
message: "Remote launch not permitted for this directory",
|
|
1056
|
-
});
|
|
1057
|
-
return;
|
|
1058
|
-
}
|
|
1059
|
-
// Backend is a MACHINE config choice (pick-one via launch.backend), not
|
|
1060
|
-
// app-chosen; rpc is a fast-follow so only tmux|herdr resolve here.
|
|
1061
|
-
const backend = loadConfig().launch?.backend === "herdr" ? "herdr" : "tmux";
|
|
1062
|
-
const launchError = _launchSession(backend, cwd, typeof f.name === "string" ? f.name : undefined,
|
|
1063
|
-
// Launch correlation: pi is spawned with UNBIEN_LAUNCH_REQ=<id> and
|
|
1064
|
-
// echoes it in room_meta, so the launching app can match the announcing
|
|
1065
|
-
// room to THIS request and auto-open the chat (resume flow).
|
|
1066
|
-
typeof f.resume === "string" && f.resume.trim().length > 0
|
|
1067
|
-
? f.resume.trim()
|
|
1068
|
-
: undefined, typeof f.id === "string" && f.id.trim().length > 0
|
|
1069
|
-
? f.id.trim()
|
|
1070
|
-
: undefined);
|
|
1071
|
-
if (launchError)
|
|
1072
|
-
envLog(`session_launch(ub) error: ${launchError}`);
|
|
1073
|
-
return;
|
|
1074
|
-
}
|
|
1075
|
-
if (type === "session_fork" || type === "session_navigate") {
|
|
1076
|
-
// app->ext: fork a NEW session / branch IN PLACE from a conversation entry.
|
|
1077
|
-
// ctx.fork / ctx.navigateTree live ONLY on the command ctx, so we can't act
|
|
1078
|
-
// here (this is the peer-channel dispatch, not a command). Self-dispatch the
|
|
1079
|
-
// registered `/unbien fork|branch` command via sendUserMessage — pi runs it
|
|
1080
|
-
// with a real ExtensionCommandContext. No stashed ctx, no startup bootstrap:
|
|
1081
|
-
// the command ctx lives exactly as long as the op that needs it. Not gated
|
|
1082
|
-
// beyond being paired — these are no more privileged than a prompt on this
|
|
1083
|
-
// same room (which the app can already send).
|
|
1084
|
-
const f = frame;
|
|
1085
|
-
const entryId = typeof f.entry_id === "string" ? f.entry_id : "";
|
|
1086
|
-
if (!entryId) {
|
|
1087
|
-
envLog(`${String(type)}(ub): missing entry_id — ignored`);
|
|
1088
|
-
return;
|
|
1089
|
-
}
|
|
1090
|
-
if (type === "session_fork") {
|
|
1091
|
-
// Thread the request id (reqId) so the new session can echo it back as
|
|
1092
|
-
// forked_from_req on its first sync — the app's auto-nav correlation.
|
|
1093
|
-
// Order: `<entryId> <reqId> [pos]` (see _cmdFork's parse).
|
|
1094
|
-
const reqId = typeof f.id === "string" ? f.id : "";
|
|
1095
|
-
const posArg = f.position === "at" || f.position === "before" ? ` ${f.position}` : "";
|
|
1096
|
-
const reqArg = reqId ? ` ${reqId}` : "";
|
|
1097
|
-
envLog(`session_fork(ub): entry=${entryId.slice(0, 8)}${posArg}`);
|
|
1098
|
-
_pi?.sendUserMessage(`/unbien fork ${entryId}${reqArg}${posArg}`, {
|
|
1099
|
-
deliverAs: "followUp",
|
|
1100
|
-
expandPromptTemplates: true,
|
|
1101
|
-
});
|
|
1102
|
-
}
|
|
1103
|
-
else {
|
|
1104
|
-
envLog(`session_navigate(ub): entry=${entryId.slice(0, 8)}`);
|
|
1105
|
-
_pi?.sendUserMessage(`/unbien branch ${entryId}`, {
|
|
1106
|
-
deliverAs: "followUp",
|
|
1107
|
-
expandPromptTemplates: true,
|
|
1108
|
-
});
|
|
1109
|
-
}
|
|
1110
|
-
return;
|
|
1111
|
-
}
|
|
506
|
+
routeUnBienPlane(planeDeps, sender, env);
|
|
1112
507
|
}
|
|
508
|
+
// ── Plane-router deps (accessors over module state — live at dispatch) ──────
|
|
509
|
+
const planeDeps = {
|
|
510
|
+
extensionUiBridge: () => _extensionUiBridge,
|
|
511
|
+
panelBridge: () => _panelBridge,
|
|
512
|
+
subagentRooms: () => _subagentRooms,
|
|
513
|
+
sessionStartedAt: () => _rootState().sessionStartedAt ?? 0,
|
|
514
|
+
myRoomMeta: () => _rootState().myRoomMeta,
|
|
515
|
+
rootState: () => _rootState(),
|
|
516
|
+
safeNotify: _safeNotify,
|
|
517
|
+
liveCtx: () => _liveCtx(),
|
|
518
|
+
pi: () => _pi,
|
|
519
|
+
// Getter: rpcDeps is declared later in the file; the dispatch reads it
|
|
520
|
+
// live at call time.
|
|
521
|
+
get rpcDeps() {
|
|
522
|
+
return rpcDeps;
|
|
523
|
+
},
|
|
524
|
+
};
|
|
1113
525
|
// ── Display-name helpers ──────────────────────────────────────────────────────
|
|
1114
526
|
/**
|
|
1115
527
|
* Resolves the name this Pi shows to the mobile app and the relay's
|
|
@@ -1246,7 +658,6 @@ function _probeCtx(label, ctx) {
|
|
|
1246
658
|
// base-ctx methods (no newSession — that's command-ctx only); command-only ops
|
|
1247
659
|
// (fork/new/branch) reach a command ctx by self-dispatching a slash command
|
|
1248
660
|
// instead of retaining one.
|
|
1249
|
-
let _lastEventCtx = null;
|
|
1250
661
|
const _noopCtx = { ui: { notify: () => undefined }, abort: () => undefined };
|
|
1251
662
|
// A single Pi process can load this extension TWICE in the SAME session:
|
|
1252
663
|
// when it is launched as `pi -e <dist>/index.js` AND un-bien is ALSO installed
|
|
@@ -1316,7 +727,7 @@ const imageDeps = {
|
|
|
1316
727
|
},
|
|
1317
728
|
rootState: _rootState,
|
|
1318
729
|
get myRoomMeta() {
|
|
1319
|
-
return
|
|
730
|
+
return _rootState().myRoomMeta;
|
|
1320
731
|
},
|
|
1321
732
|
wakeAgent: _wakeAgent,
|
|
1322
733
|
};
|
|
@@ -1332,7 +743,7 @@ const rpcDeps = {
|
|
|
1332
743
|
},
|
|
1333
744
|
rootState: _rootState,
|
|
1334
745
|
get lastEventCtx() {
|
|
1335
|
-
return
|
|
746
|
+
return _rootState().baseCtx;
|
|
1336
747
|
},
|
|
1337
748
|
imageDeps,
|
|
1338
749
|
wakeAgent: _wakeAgent,
|
|
@@ -1414,13 +825,13 @@ const relayDeps = {
|
|
|
1414
825
|
return _cachedEd25519;
|
|
1415
826
|
},
|
|
1416
827
|
get myRoomId() {
|
|
1417
|
-
return
|
|
828
|
+
return _rootState().myRoomId;
|
|
1418
829
|
},
|
|
1419
830
|
get myRoomMeta() {
|
|
1420
|
-
return
|
|
831
|
+
return _rootState().myRoomMeta;
|
|
1421
832
|
},
|
|
1422
833
|
get sessionStartedAt() {
|
|
1423
|
-
return
|
|
834
|
+
return _rootState().sessionStartedAt;
|
|
1424
835
|
},
|
|
1425
836
|
sessionCwd: () => {
|
|
1426
837
|
const sm = _rootState().sessionManager;
|
|
@@ -1510,30 +921,30 @@ const deps = {
|
|
|
1510
921
|
_cachedEd25519 = v;
|
|
1511
922
|
},
|
|
1512
923
|
get currentModel() {
|
|
1513
|
-
return
|
|
924
|
+
return _rootState().currentModel ?? undefined;
|
|
1514
925
|
},
|
|
1515
926
|
set currentModel(v) {
|
|
1516
|
-
|
|
927
|
+
_rootState().currentModel = v ?? null;
|
|
1517
928
|
},
|
|
1518
929
|
get currentThinking() {
|
|
1519
|
-
return
|
|
930
|
+
return _rootState().thinking ?? undefined;
|
|
1520
931
|
},
|
|
1521
932
|
abortCurrentTurn: () => _abortCurrentTurn(),
|
|
1522
933
|
cancelPendingAsks: () => _extensionUiBridge?.cancelAllPending() ?? 0,
|
|
1523
934
|
set currentThinking(v) {
|
|
1524
|
-
|
|
935
|
+
_rootState().thinking = v ?? null;
|
|
1525
936
|
},
|
|
1526
937
|
get myRoomId() {
|
|
1527
|
-
return
|
|
938
|
+
return _rootState().myRoomId;
|
|
1528
939
|
},
|
|
1529
940
|
set myRoomId(v) {
|
|
1530
|
-
|
|
941
|
+
_rootState().myRoomId = v;
|
|
1531
942
|
},
|
|
1532
943
|
get myRoomMeta() {
|
|
1533
|
-
return
|
|
944
|
+
return _rootState().myRoomMeta;
|
|
1534
945
|
},
|
|
1535
946
|
set myRoomMeta(v) {
|
|
1536
|
-
|
|
947
|
+
_rootState().myRoomMeta = v;
|
|
1537
948
|
},
|
|
1538
949
|
get peerShort() {
|
|
1539
950
|
return _peerShort;
|
|
@@ -1542,10 +953,10 @@ const deps = {
|
|
|
1542
953
|
_peerShort = v;
|
|
1543
954
|
},
|
|
1544
955
|
get sessionStartedAt() {
|
|
1545
|
-
return
|
|
956
|
+
return _rootState().sessionStartedAt;
|
|
1546
957
|
},
|
|
1547
958
|
set sessionStartedAt(v) {
|
|
1548
|
-
|
|
959
|
+
_rootState().sessionStartedAt = v;
|
|
1549
960
|
},
|
|
1550
961
|
get cwdLock() {
|
|
1551
962
|
return _cwdLock;
|
|
@@ -1620,7 +1031,7 @@ const deps = {
|
|
|
1620
1031
|
return _disposed;
|
|
1621
1032
|
},
|
|
1622
1033
|
get lastEventCtx() {
|
|
1623
|
-
return
|
|
1034
|
+
return _rootState().baseCtx;
|
|
1624
1035
|
},
|
|
1625
1036
|
get pi() {
|
|
1626
1037
|
return _pi;
|
|
@@ -1648,7 +1059,7 @@ const deps = {
|
|
|
1648
1059
|
handleControl: _handleControl,
|
|
1649
1060
|
relayStatus: () => _relayStatus(relayDeps),
|
|
1650
1061
|
getState: _getState,
|
|
1651
|
-
runTestScenario:
|
|
1062
|
+
runTestScenario: (scenario) => runTestScenario({ broadcast: (env) => _broadcastEnvelope(relayDeps, env), emitBus: makeTestBusEmitter(() => _pi) }, scenario),
|
|
1652
1063
|
safeNotify: _safeNotify,
|
|
1653
1064
|
renameAgent: _renameAgent,
|
|
1654
1065
|
};
|
|
@@ -1709,10 +1120,10 @@ const _testHooks = createTestHooks({
|
|
|
1709
1120
|
_lockedName = v;
|
|
1710
1121
|
},
|
|
1711
1122
|
set sessionStartedAt(v) {
|
|
1712
|
-
|
|
1123
|
+
_rootState().sessionStartedAt = v;
|
|
1713
1124
|
},
|
|
1714
1125
|
set currentModel(v) {
|
|
1715
|
-
|
|
1126
|
+
_rootState().currentModel = v ?? null;
|
|
1716
1127
|
},
|
|
1717
1128
|
set pi(v) {
|
|
1718
1129
|
_pi = v;
|
|
@@ -1736,6 +1147,7 @@ const _testHooks = createTestHooks({
|
|
|
1736
1147
|
},
|
|
1737
1148
|
activePeers: _activePeers,
|
|
1738
1149
|
rootState: _rootState,
|
|
1150
|
+
resetRootRecord: _resetRootRecordForTest,
|
|
1739
1151
|
seedRootSession(sid) {
|
|
1740
1152
|
_rootSessionId = sid;
|
|
1741
1153
|
_stateFor(sid).sessionManager = {
|
|
@@ -1762,6 +1174,7 @@ export const _getCachedPublicKeyForTest = _testHooks.getCachedPublicKeyForTest;
|
|
|
1762
1174
|
export const _setSessionStartedAtForTest = _testHooks.setSessionStartedAtForTest;
|
|
1763
1175
|
export const _setCurrentModelForTest = _testHooks.setCurrentModelForTest;
|
|
1764
1176
|
export const _getCurrentTurnIdForTest = _testHooks.getCurrentTurnIdForTest;
|
|
1177
|
+
export const _getRootProjectionForTest = _testHooks.getRootProjectionForTest;
|
|
1765
1178
|
export const _setPiForTest = _testHooks.setPiForTest;
|
|
1766
1179
|
export const _hasPendingReconnect = _testHooks.hasPendingReconnect;
|
|
1767
1180
|
export const _getActivePeerCountForTest = _testHooks.getActivePeerCountForTest;
|
|
@@ -1805,7 +1218,7 @@ const extension = (pi) => {
|
|
|
1805
1218
|
});
|
|
1806
1219
|
_subagentRooms?.dispose();
|
|
1807
1220
|
_subagentRooms = initSubagentRooms(pi, {
|
|
1808
|
-
getParentRoomId: () =>
|
|
1221
|
+
getParentRoomId: () => _rootState().myRoomId,
|
|
1809
1222
|
getParentSessionId: () => _rootSessionId,
|
|
1810
1223
|
broadcastPanel: (msg) => _panelBroadcast(relayDeps, msg),
|
|
1811
1224
|
});
|
|
@@ -1888,19 +1301,19 @@ const extension = (pi) => {
|
|
|
1888
1301
|
const level = event?.level;
|
|
1889
1302
|
if (!level)
|
|
1890
1303
|
return;
|
|
1891
|
-
// Cache per-sid; only the ROOT projects to
|
|
1304
|
+
// Cache per-sid; only the ROOT projects to room_meta.
|
|
1892
1305
|
const sid = _sidOf(ctx);
|
|
1893
|
-
_stateFor(sid)
|
|
1306
|
+
const st = _stateFor(sid);
|
|
1307
|
+
st.thinking = level;
|
|
1894
1308
|
if (_isNonRootSid(sid))
|
|
1895
1309
|
return;
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
if (!_relay || !_myRoomId)
|
|
1310
|
+
if (st.myRoomMeta)
|
|
1311
|
+
st.myRoomMeta = { ...st.myRoomMeta, thinking: level };
|
|
1312
|
+
if (!_relay || !st.myRoomId)
|
|
1900
1313
|
return;
|
|
1901
1314
|
_relay.sendControl({
|
|
1902
1315
|
type: "room_meta_update",
|
|
1903
|
-
room_id:
|
|
1316
|
+
room_id: st.myRoomId,
|
|
1904
1317
|
meta: { thinking: level },
|
|
1905
1318
|
});
|
|
1906
1319
|
});
|
|
@@ -1976,35 +1389,36 @@ const extension = (pi) => {
|
|
|
1976
1389
|
}
|
|
1977
1390
|
if (_isNonRootSid(sid))
|
|
1978
1391
|
return; // room_meta projection is root-only
|
|
1979
|
-
// Root projection: seed the
|
|
1392
|
+
// Root projection: seed the room_meta model + hello from the root's
|
|
1980
1393
|
// cached model, once.
|
|
1981
|
-
if (!
|
|
1394
|
+
if (!st.currentModel && st.model)
|
|
1982
1395
|
_setCurrentModel(st.model);
|
|
1983
1396
|
// Plan/32 Part B: publish working=true as room_meta (raw, no debounce —
|
|
1984
1397
|
// the debounce lives in the app). Same shape as the model/thinking updates.
|
|
1985
|
-
//
|
|
1986
|
-
if (
|
|
1987
|
-
|
|
1988
|
-
if (_relay &&
|
|
1398
|
+
// myRoomMeta is the ROOM projection (driven only by the root session).
|
|
1399
|
+
if (st.myRoomMeta)
|
|
1400
|
+
st.myRoomMeta = { ...st.myRoomMeta, working: true };
|
|
1401
|
+
if (_relay && st.myRoomId) {
|
|
1989
1402
|
_relay.sendControl({
|
|
1990
1403
|
type: "room_meta_update",
|
|
1991
|
-
room_id:
|
|
1404
|
+
room_id: st.myRoomId,
|
|
1992
1405
|
meta: { working: true },
|
|
1993
1406
|
});
|
|
1994
1407
|
}
|
|
1995
1408
|
});
|
|
1996
1409
|
pi.on("turn_end", (_event, ctx) => {
|
|
1997
1410
|
const sid = _sidOf(ctx);
|
|
1998
|
-
_stateFor(sid)
|
|
1411
|
+
const st = _stateFor(sid);
|
|
1412
|
+
st.working = false;
|
|
1999
1413
|
if (_isNonRootSid(sid))
|
|
2000
1414
|
return; // room_meta is root-only
|
|
2001
1415
|
// Plan/32 Part B: publish working=false as room_meta (raw, no debounce).
|
|
2002
|
-
if (
|
|
2003
|
-
|
|
2004
|
-
if (_relay &&
|
|
1416
|
+
if (st.myRoomMeta)
|
|
1417
|
+
st.myRoomMeta = { ...st.myRoomMeta, working: false };
|
|
1418
|
+
if (_relay && st.myRoomId) {
|
|
2005
1419
|
_relay.sendControl({
|
|
2006
1420
|
type: "room_meta_update",
|
|
2007
|
-
room_id:
|
|
1421
|
+
room_id: st.myRoomId,
|
|
2008
1422
|
meta: { working: false },
|
|
2009
1423
|
});
|
|
2010
1424
|
}
|
|
@@ -2049,8 +1463,9 @@ const extension = (pi) => {
|
|
|
2049
1463
|
if (_isNonRootSid(_sidOf(ctx)))
|
|
2050
1464
|
return;
|
|
2051
1465
|
_sessionName = name;
|
|
2052
|
-
|
|
2053
|
-
|
|
1466
|
+
const st = _rootState();
|
|
1467
|
+
if (st.myRoomMeta)
|
|
1468
|
+
st.myRoomMeta = { ...st.myRoomMeta, name };
|
|
2054
1469
|
_broadcastEnvelope(relayDeps, {
|
|
2055
1470
|
evt: { channel: "session_info", data: { name } },
|
|
2056
1471
|
});
|
|
@@ -2059,10 +1474,10 @@ const extension = (pi) => {
|
|
|
2059
1474
|
// reaches attached peers, so pre-attach the Home tile went stale until open
|
|
2060
1475
|
// or reconnect. Relay >=0.6.0 merges + fans `name`; older relays drop the
|
|
2061
1476
|
// unknown field (harmless — attached peers still get the evt). Design 01M1SPN7.
|
|
2062
|
-
if (_relay &&
|
|
1477
|
+
if (_relay && st.myRoomId) {
|
|
2063
1478
|
_relay.sendControl({
|
|
2064
1479
|
type: "room_meta_update",
|
|
2065
|
-
room_id:
|
|
1480
|
+
room_id: st.myRoomId,
|
|
2066
1481
|
meta: { name },
|
|
2067
1482
|
});
|
|
2068
1483
|
}
|
|
@@ -2096,12 +1511,22 @@ const extension = (pi) => {
|
|
|
2096
1511
|
// session_start with their own ctx). Each records its OWN sessionManager —
|
|
2097
1512
|
// no cross-session clobber (was the unguarded `_sessionManager = ...` bug).
|
|
2098
1513
|
const sid = _sidOf(ctx);
|
|
2099
|
-
//
|
|
2100
|
-
// is the
|
|
2101
|
-
//
|
|
2102
|
-
//
|
|
2103
|
-
|
|
2104
|
-
|
|
1514
|
+
// Claim root + adopt the root sid BEFORE any _stateFor(sid) write. The root
|
|
1515
|
+
// record is the stable _rootRecord, but _stateFor(sid) only routes there
|
|
1516
|
+
// once sid === _rootKey() — i.e. once _rootSessionId === sid. Writing
|
|
1517
|
+
// baseCtx/sessionManager before the adoption lands them in an orphaned
|
|
1518
|
+
// per-sid map entry (the deferred re-arm then reads _rootState and sees
|
|
1519
|
+
// null). For a subagent child _claimRootSession returns false (root owns
|
|
1520
|
+
// the slot), so _rootSessionId is untouched and _stateFor(sid) correctly
|
|
1521
|
+
// gives the child its own map entry.
|
|
1522
|
+
const isRootClaim = _claimRootSession(pi);
|
|
1523
|
+
if (isRootClaim && ctx?.sessionManager)
|
|
1524
|
+
_rootSessionId = sid;
|
|
1525
|
+
// Base ctx (compact/notify fallback when no fresh ctx is passed) is
|
|
1526
|
+
// PER-SESSION (plan 01M18RNH) — each session_start records its OWN; the
|
|
1527
|
+
// base-ctx resolution reads the ROOT's, so a child's ctx never leaks
|
|
1528
|
+
// (structurally replaces the old `!_isNonRootSid` guard + global).
|
|
1529
|
+
_stateFor(sid).baseCtx = ctx ?? null;
|
|
2105
1530
|
// EVENT-CTX TARGETED PROBE (run 2026-09-18, round 3): the ctx objects
|
|
2106
1531
|
// are guarded (throwing getters — proxies), so full reflection
|
|
2107
1532
|
// under-reports or throws. Per-property access, each caught, is the
|
|
@@ -2119,7 +1544,7 @@ const extension = (pi) => {
|
|
|
2119
1544
|
// The root claim also fixes _rootSessionId (re-captured across replacement);
|
|
2120
1545
|
// only when a real sessionManager is present (ctx-less test events stay in
|
|
2121
1546
|
// null-root mode where every event is treated as root).
|
|
2122
|
-
if (
|
|
1547
|
+
if (isRootClaim) {
|
|
2123
1548
|
if (ctx?.sessionManager)
|
|
2124
1549
|
_rootSessionId = sid;
|
|
2125
1550
|
if (!_extensionUiBridge)
|
|
@@ -2137,7 +1562,7 @@ const extension = (pi) => {
|
|
|
2137
1562
|
});
|
|
2138
1563
|
if (!_subagentRooms)
|
|
2139
1564
|
_subagentRooms = initSubagentRooms(pi, {
|
|
2140
|
-
getParentRoomId: () =>
|
|
1565
|
+
getParentRoomId: () => _rootState().myRoomId,
|
|
2141
1566
|
getParentSessionId: () => _rootSessionId,
|
|
2142
1567
|
broadcastPanel: (msg) => _panelBroadcast(relayDeps, msg),
|
|
2143
1568
|
});
|
|
@@ -2299,10 +1724,19 @@ const extension = (pi) => {
|
|
|
2299
1724
|
_subagentRooms = null;
|
|
2300
1725
|
_releaseRootSession(pi);
|
|
2301
1726
|
}
|
|
2302
|
-
// Drop the captured
|
|
2303
|
-
// instance survives session replacement; the next
|
|
2304
|
-
//
|
|
2305
|
-
|
|
1727
|
+
// Drop the captured base ctx on the stable root record. On module-reuse
|
|
1728
|
+
// hosts the same instance survives session replacement; the next
|
|
1729
|
+
// session_start re-binds it for the new session. We clear ONLY baseCtx here
|
|
1730
|
+
// (mirroring the pre-01M18RNH global semantics): the relay-room PROJECTION
|
|
1731
|
+
// (myRoomId/myRoomMeta/sessionStartedAt) must survive the replacement by
|
|
1732
|
+
// contract (_goIdle keeps it too; _cmdStart overwrites it on the new
|
|
1733
|
+
// connect), and the other per-session fields (model/thinking/currentModel/
|
|
1734
|
+
// sessionManager/…) are either re-populated by the new session_start + its
|
|
1735
|
+
// events or intentionally fall back to the prior value when the replacement
|
|
1736
|
+
// ctx carries no sessionManager (the ctx-less same-module path). Clearing
|
|
1737
|
+
// sessionManager here, for instance, defers `_cmdStart` (no roomId) and
|
|
1738
|
+
// breaks that path — see the same-module replacement suite.
|
|
1739
|
+
_rootState().baseCtx = null;
|
|
2306
1740
|
// No bye reason: the process keeps running and the fresh instance re-joins
|
|
2307
1741
|
// the SAME relay room, so an explicit offline→online flap would be wrong.
|
|
2308
1742
|
// Revoke producer/Relay/bridge authority while the global node is still
|
|
@@ -2469,7 +1903,7 @@ function _deliverMeshMessageToAgent(env) {
|
|
|
2469
1903
|
* handles incoming app→pi requests.
|
|
2470
1904
|
*/
|
|
2471
1905
|
function _abortCurrentTurn(fallbackCtx) {
|
|
2472
|
-
const candidates = [
|
|
1906
|
+
const candidates = [_rootState().baseCtx, fallbackCtx];
|
|
2473
1907
|
for (const candidate of candidates) {
|
|
2474
1908
|
if (!candidate || candidate === _noopCtx)
|
|
2475
1909
|
continue;
|