@geohar/un-bien 0.20.15 → 0.20.17
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.js +99 -708
- 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/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,10 +342,6 @@ 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.
|
|
@@ -377,6 +364,11 @@ function _stateFor(sid) {
|
|
|
377
364
|
sessionManager: null,
|
|
378
365
|
model: null,
|
|
379
366
|
thinking: null,
|
|
367
|
+
currentModel: null,
|
|
368
|
+
myRoomId: null,
|
|
369
|
+
myRoomMeta: null,
|
|
370
|
+
sessionStartedAt: null,
|
|
371
|
+
baseCtx: null,
|
|
380
372
|
};
|
|
381
373
|
_sessions.set(sid, st);
|
|
382
374
|
}
|
|
@@ -460,19 +452,6 @@ export function _getState() {
|
|
|
460
452
|
// underlying BUS events and lets the REAL bridges produce the frames (faithful);
|
|
461
453
|
// the simple ExtensionUIPromptView methods (select/confirm/input/editor) + media
|
|
462
454
|
// 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
455
|
// Owner-notify bridge (transient toasts on the app): /unbien command handlers
|
|
477
456
|
// report through notifyOwners() - this broadcasts the notice to every attached
|
|
478
457
|
// owner as extension_ui_request {method:"notify"}, the same shape the ask-flow
|
|
@@ -488,622 +467,34 @@ setOwnerNotifyHook((message, level) => {
|
|
|
488
467
|
},
|
|
489
468
|
});
|
|
490
469
|
});
|
|
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
470
|
/**
|
|
880
|
-
*
|
|
881
|
-
*
|
|
882
|
-
*
|
|
883
|
-
* SDK primitives (`_wakeAgent`, `_abortCurrentTurn`) are pi, not old protocol.
|
|
471
|
+
* Plane routers live in ./routing.ts (carved, slice 4). These thin wrappers
|
|
472
|
+
* preserve the call sites + the CommandDeps surface; the deps accessors
|
|
473
|
+
* keep module state read LIVE at each dispatch.
|
|
884
474
|
*/
|
|
885
475
|
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
|
-
});
|
|
476
|
+
routeRpcCommand(planeDeps, sender, env);
|
|
913
477
|
}
|
|
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
478
|
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
|
-
sender.send(req);
|
|
937
|
-
const panels = _panelBridge?.pendingPanels() ?? [];
|
|
938
|
-
for (const panel of panels)
|
|
939
|
-
sender.sendEnvelope({ evt: { channel: "panel", data: panel } });
|
|
940
|
-
envLog(`session_sync(ub): panels=${panels.length} + ui (transcript is the app's get_entries rpc)`);
|
|
941
|
-
// Terminator/ack on the ub plane; carries the session clock so the app can
|
|
942
|
-
// detect a pi restart. `truncated`/`limit` are gone (a replay concern;
|
|
943
|
-
// get_entries is unbounded / since-delta). `session_name` (pre-release
|
|
944
|
-
// 2026-09-18): the NAME PULL — heals a missed session_info_changed push
|
|
945
|
-
// (an iOS backgrounded socket misses live frames; the relay's stored meta
|
|
946
|
-
// is stale until our next hello, so reconnects can't carry it either).
|
|
947
|
-
// session_sync is issued on EVERY open + reconnect, so the name heals on
|
|
948
|
-
// every natural cycle. Old apps ignore the field; old extensions omit it.
|
|
949
|
-
//
|
|
950
|
-
// SOURCE = the room's DISPLAY name (myRoomMeta.name), NOT deps.sessionName:
|
|
951
|
-
// the latter is `_sessionName`, which `_cmdJoin` sets to the mesh session
|
|
952
|
-
// id LOCAL_SESSION_NAME ("local"). Sending that here clobbered the tile
|
|
953
|
-
// label with "local" on every reconnect (the flaky-name bug) — the
|
|
954
|
-
// non-live pull disagreed with the live session_info push. myRoomMeta.name
|
|
955
|
-
// is the same value pair_ok sends (deps.displayName) and tracks live
|
|
956
|
-
// renames (session_info_changed updates it), so pull == push now.
|
|
957
|
-
const displayName = deps.myRoomMeta?.name;
|
|
958
|
-
// Fork auto-nav: a fork-born session's FIRST sync echoes the app's
|
|
959
|
-
// originating fork request id (design: fork switch). take = read+unlink,
|
|
960
|
-
// so it fires once (the app navigates on first receipt; later reconnects
|
|
961
|
-
// must not re-jump). Keyed by the session id, so it survives the extension
|
|
962
|
-
// module re-eval that ctx.fork triggers.
|
|
963
|
-
const forkSm = _rootState().sessionManager;
|
|
964
|
-
const forkedFromReq = forkSm && typeof forkSm.getSessionDir === "function"
|
|
965
|
-
? takeForkLink(forkSm.getSessionDir(), forkSm.getSessionId())
|
|
966
|
-
: undefined;
|
|
967
|
-
sender.sendEnvelope({
|
|
968
|
-
ub: {
|
|
969
|
-
type: "session_sync_end",
|
|
970
|
-
...(typeof f.id === "string" ? { in_reply_to: f.id } : {}),
|
|
971
|
-
session_started_at: _sessionStartedAt ?? 0,
|
|
972
|
-
...(displayName ? { session_name: displayName } : {}),
|
|
973
|
-
...(forkedFromReq ? { forked_from_req: forkedFromReq } : {}),
|
|
974
|
-
},
|
|
975
|
-
});
|
|
976
|
-
return;
|
|
977
|
-
}
|
|
978
|
-
if (type === "terminate") {
|
|
979
|
-
// app->ext: kill THIS session. Root room only — a child room's terminate
|
|
980
|
-
// is handled by the child-room router in subagent_rooms (never reaches
|
|
981
|
-
// here; that room's conn dispatches its own ub frames). Gate on local
|
|
982
|
-
// config; the app confirms + shows red before sending.
|
|
983
|
-
if (!effectiveAllowRemoteTerminate(loadLocalConfig(process.cwd()))) {
|
|
984
|
-
envLog("terminate(ub): remote terminate disabled on this machine");
|
|
985
|
-
return;
|
|
986
|
-
}
|
|
987
|
-
envLog(`terminate(ub): graceful host shutdown requested${typeof frame.reason === "string" ? ` (${String(frame.reason)})` : ""}`);
|
|
988
|
-
_safeNotify("Terminated from the app", "warning");
|
|
989
|
-
try {
|
|
990
|
-
// ctx.shutdown() IS the /quit equivalent — pi's own source lists
|
|
991
|
-
// "extension shutdown()" alongside Ctrl+D / /quit (interactive-mode.js
|
|
992
|
-
// 3229: same graceful this.shutdown(): drain input, ui.stop() restores
|
|
993
|
-
// cooked mode + cursor, extension disposal, resume hint, exit). It sits
|
|
994
|
-
// DIRECTLY on the event ctx (ExtensionContext.shutdown — there is no
|
|
995
|
-
// ctx.actions sub-object; the first two attempts failed exactly there).
|
|
996
|
-
//
|
|
997
|
-
// MID-TURN DEFERRAL: the interactive handler fires immediately only when
|
|
998
|
-
// idle; otherwise it sets `shutdownRequested` and waits for
|
|
999
|
-
// `agent_settled` — which never arrives when nothing is running. So
|
|
1000
|
-
// abort() FIRST: aborting a running turn forces the settle that
|
|
1001
|
-
// re-checks the flag; on an idle session abort is a no-op and the
|
|
1002
|
-
// idle fast-path fires directly.
|
|
1003
|
-
//
|
|
1004
|
-
// NO process.exit fallback, ever: a raw exit leaves the TTY in raw
|
|
1005
|
-
// mode (keypresses echo escape codes until `stty sane`). No live ctx =>
|
|
1006
|
-
// log loudly and stay up; the user quits on the machine.
|
|
1007
|
-
const ctx = _liveCtx();
|
|
1008
|
-
if (ctx?.shutdown) {
|
|
1009
|
-
ctx.abort?.();
|
|
1010
|
-
ctx.shutdown();
|
|
1011
|
-
}
|
|
1012
|
-
else {
|
|
1013
|
-
envLog("terminate(ub): no live ctx — refusing raw exit (would leave the terminal in raw mode); staying up");
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
catch (err) {
|
|
1017
|
-
envLog(`terminate(ub) error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1018
|
-
}
|
|
1019
|
-
return;
|
|
1020
|
-
}
|
|
1021
|
-
if (type === "close_child_room") {
|
|
1022
|
-
// app->ext(PARENT): permanently close one of MY child rooms (done-subagent
|
|
1023
|
-
// removal — the room lingers by design otherwise). Tombstone + dispose in
|
|
1024
|
-
// subagent_rooms; the relay fires room_ended when the last conn drops.
|
|
1025
|
-
if (!effectiveAllowRemoteTerminate(loadLocalConfig(process.cwd()))) {
|
|
1026
|
-
envLog("close_child_room(ub): remote terminate disabled on this machine");
|
|
1027
|
-
return;
|
|
1028
|
-
}
|
|
1029
|
-
const roomId = frame.room_id;
|
|
1030
|
-
if (typeof roomId !== "string" || roomId.length === 0)
|
|
1031
|
-
return;
|
|
1032
|
-
const closed = _subagentRooms?.closeChildRoom(roomId) ?? false;
|
|
1033
|
-
envLog(`close_child_room(ub): room=${roomId.slice(0, 8)} closed=${closed}`);
|
|
1034
|
-
return;
|
|
1035
|
-
}
|
|
1036
|
-
if (type === "session_launch") {
|
|
1037
|
-
const f = frame;
|
|
1038
|
-
const cwd = _expandTilde(typeof f.cwd === "string" && f.cwd.length > 0 ? f.cwd : process.cwd());
|
|
1039
|
-
if (!effectiveAllowRemoteLaunch(loadLocalConfig(cwd))) {
|
|
1040
|
-
envLog("session_launch(ub): remote launch disabled on this machine");
|
|
1041
|
-
return;
|
|
1042
|
-
}
|
|
1043
|
-
// Directory allow-list (design 01M211VW9), read FRESH each request.
|
|
1044
|
-
if (!launchDirAllowed(cwd, loadConfig().launch?.dirs)) {
|
|
1045
|
-
envLog("session_launch(ub): cwd not in launch.dirs allow-list");
|
|
1046
|
-
sender.send({
|
|
1047
|
-
type: "error",
|
|
1048
|
-
code: "permission_denied",
|
|
1049
|
-
message: "Remote launch not permitted for this directory",
|
|
1050
|
-
});
|
|
1051
|
-
return;
|
|
1052
|
-
}
|
|
1053
|
-
// Backend is a MACHINE config choice (pick-one via launch.backend), not
|
|
1054
|
-
// app-chosen; rpc is a fast-follow so only tmux|herdr resolve here.
|
|
1055
|
-
const backend = loadConfig().launch?.backend === "herdr" ? "herdr" : "tmux";
|
|
1056
|
-
const launchError = _launchSession(backend, cwd, typeof f.name === "string" ? f.name : undefined,
|
|
1057
|
-
// Launch correlation: pi is spawned with UNBIEN_LAUNCH_REQ=<id> and
|
|
1058
|
-
// echoes it in room_meta, so the launching app can match the announcing
|
|
1059
|
-
// room to THIS request and auto-open the chat (resume flow).
|
|
1060
|
-
typeof f.resume === "string" && f.resume.trim().length > 0
|
|
1061
|
-
? f.resume.trim()
|
|
1062
|
-
: undefined, typeof f.id === "string" && f.id.trim().length > 0
|
|
1063
|
-
? f.id.trim()
|
|
1064
|
-
: undefined);
|
|
1065
|
-
if (launchError)
|
|
1066
|
-
envLog(`session_launch(ub) error: ${launchError}`);
|
|
1067
|
-
return;
|
|
1068
|
-
}
|
|
1069
|
-
if (type === "session_fork" || type === "session_navigate") {
|
|
1070
|
-
// app->ext: fork a NEW session / branch IN PLACE from a conversation entry.
|
|
1071
|
-
// ctx.fork / ctx.navigateTree live ONLY on the command ctx, so we can't act
|
|
1072
|
-
// here (this is the peer-channel dispatch, not a command). Self-dispatch the
|
|
1073
|
-
// registered `/unbien fork|branch` command via sendUserMessage — pi runs it
|
|
1074
|
-
// with a real ExtensionCommandContext. No stashed ctx, no startup bootstrap:
|
|
1075
|
-
// the command ctx lives exactly as long as the op that needs it. Not gated
|
|
1076
|
-
// beyond being paired — these are no more privileged than a prompt on this
|
|
1077
|
-
// same room (which the app can already send).
|
|
1078
|
-
const f = frame;
|
|
1079
|
-
const entryId = typeof f.entry_id === "string" ? f.entry_id : "";
|
|
1080
|
-
if (!entryId) {
|
|
1081
|
-
envLog(`${String(type)}(ub): missing entry_id — ignored`);
|
|
1082
|
-
return;
|
|
1083
|
-
}
|
|
1084
|
-
if (type === "session_fork") {
|
|
1085
|
-
// Thread the request id (reqId) so the new session can echo it back as
|
|
1086
|
-
// forked_from_req on its first sync — the app's auto-nav correlation.
|
|
1087
|
-
// Order: `<entryId> <reqId> [pos]` (see _cmdFork's parse).
|
|
1088
|
-
const reqId = typeof f.id === "string" ? f.id : "";
|
|
1089
|
-
const posArg = f.position === "at" || f.position === "before" ? ` ${f.position}` : "";
|
|
1090
|
-
const reqArg = reqId ? ` ${reqId}` : "";
|
|
1091
|
-
envLog(`session_fork(ub): entry=${entryId.slice(0, 8)}${posArg}`);
|
|
1092
|
-
_pi?.sendUserMessage(`/unbien fork ${entryId}${reqArg}${posArg}`, {
|
|
1093
|
-
deliverAs: "followUp",
|
|
1094
|
-
expandPromptTemplates: true,
|
|
1095
|
-
});
|
|
1096
|
-
}
|
|
1097
|
-
else {
|
|
1098
|
-
envLog(`session_navigate(ub): entry=${entryId.slice(0, 8)}`);
|
|
1099
|
-
_pi?.sendUserMessage(`/unbien branch ${entryId}`, {
|
|
1100
|
-
deliverAs: "followUp",
|
|
1101
|
-
expandPromptTemplates: true,
|
|
1102
|
-
});
|
|
1103
|
-
}
|
|
1104
|
-
return;
|
|
1105
|
-
}
|
|
479
|
+
routeUnBienPlane(planeDeps, sender, env);
|
|
1106
480
|
}
|
|
481
|
+
// ── Plane-router deps (accessors over module state — live at dispatch) ──────
|
|
482
|
+
const planeDeps = {
|
|
483
|
+
extensionUiBridge: () => _extensionUiBridge,
|
|
484
|
+
panelBridge: () => _panelBridge,
|
|
485
|
+
subagentRooms: () => _subagentRooms,
|
|
486
|
+
sessionStartedAt: () => _rootState().sessionStartedAt ?? 0,
|
|
487
|
+
myRoomMeta: () => _rootState().myRoomMeta,
|
|
488
|
+
rootState: () => _rootState(),
|
|
489
|
+
safeNotify: _safeNotify,
|
|
490
|
+
liveCtx: () => _liveCtx(),
|
|
491
|
+
pi: () => _pi,
|
|
492
|
+
// Getter: rpcDeps is declared later in the file; the dispatch reads it
|
|
493
|
+
// live at call time.
|
|
494
|
+
get rpcDeps() {
|
|
495
|
+
return rpcDeps;
|
|
496
|
+
},
|
|
497
|
+
};
|
|
1107
498
|
// ── Display-name helpers ──────────────────────────────────────────────────────
|
|
1108
499
|
/**
|
|
1109
500
|
* Resolves the name this Pi shows to the mobile app and the relay's
|
|
@@ -1240,7 +631,6 @@ function _probeCtx(label, ctx) {
|
|
|
1240
631
|
// base-ctx methods (no newSession — that's command-ctx only); command-only ops
|
|
1241
632
|
// (fork/new/branch) reach a command ctx by self-dispatching a slash command
|
|
1242
633
|
// instead of retaining one.
|
|
1243
|
-
let _lastEventCtx = null;
|
|
1244
634
|
const _noopCtx = { ui: { notify: () => undefined }, abort: () => undefined };
|
|
1245
635
|
// A single Pi process can load this extension TWICE in the SAME session:
|
|
1246
636
|
// when it is launched as `pi -e <dist>/index.js` AND un-bien is ALSO installed
|
|
@@ -1310,7 +700,7 @@ const imageDeps = {
|
|
|
1310
700
|
},
|
|
1311
701
|
rootState: _rootState,
|
|
1312
702
|
get myRoomMeta() {
|
|
1313
|
-
return
|
|
703
|
+
return _rootState().myRoomMeta;
|
|
1314
704
|
},
|
|
1315
705
|
wakeAgent: _wakeAgent,
|
|
1316
706
|
};
|
|
@@ -1326,7 +716,7 @@ const rpcDeps = {
|
|
|
1326
716
|
},
|
|
1327
717
|
rootState: _rootState,
|
|
1328
718
|
get lastEventCtx() {
|
|
1329
|
-
return
|
|
719
|
+
return _rootState().baseCtx;
|
|
1330
720
|
},
|
|
1331
721
|
imageDeps,
|
|
1332
722
|
wakeAgent: _wakeAgent,
|
|
@@ -1408,13 +798,13 @@ const relayDeps = {
|
|
|
1408
798
|
return _cachedEd25519;
|
|
1409
799
|
},
|
|
1410
800
|
get myRoomId() {
|
|
1411
|
-
return
|
|
801
|
+
return _rootState().myRoomId;
|
|
1412
802
|
},
|
|
1413
803
|
get myRoomMeta() {
|
|
1414
|
-
return
|
|
804
|
+
return _rootState().myRoomMeta;
|
|
1415
805
|
},
|
|
1416
806
|
get sessionStartedAt() {
|
|
1417
|
-
return
|
|
807
|
+
return _rootState().sessionStartedAt;
|
|
1418
808
|
},
|
|
1419
809
|
sessionCwd: () => {
|
|
1420
810
|
const sm = _rootState().sessionManager;
|
|
@@ -1504,30 +894,30 @@ const deps = {
|
|
|
1504
894
|
_cachedEd25519 = v;
|
|
1505
895
|
},
|
|
1506
896
|
get currentModel() {
|
|
1507
|
-
return
|
|
897
|
+
return _rootState().currentModel ?? undefined;
|
|
1508
898
|
},
|
|
1509
899
|
set currentModel(v) {
|
|
1510
|
-
|
|
900
|
+
_rootState().currentModel = v ?? null;
|
|
1511
901
|
},
|
|
1512
902
|
get currentThinking() {
|
|
1513
|
-
return
|
|
903
|
+
return _rootState().thinking ?? undefined;
|
|
1514
904
|
},
|
|
1515
905
|
abortCurrentTurn: () => _abortCurrentTurn(),
|
|
1516
906
|
cancelPendingAsks: () => _extensionUiBridge?.cancelAllPending() ?? 0,
|
|
1517
907
|
set currentThinking(v) {
|
|
1518
|
-
|
|
908
|
+
_rootState().thinking = v ?? null;
|
|
1519
909
|
},
|
|
1520
910
|
get myRoomId() {
|
|
1521
|
-
return
|
|
911
|
+
return _rootState().myRoomId;
|
|
1522
912
|
},
|
|
1523
913
|
set myRoomId(v) {
|
|
1524
|
-
|
|
914
|
+
_rootState().myRoomId = v;
|
|
1525
915
|
},
|
|
1526
916
|
get myRoomMeta() {
|
|
1527
|
-
return
|
|
917
|
+
return _rootState().myRoomMeta;
|
|
1528
918
|
},
|
|
1529
919
|
set myRoomMeta(v) {
|
|
1530
|
-
|
|
920
|
+
_rootState().myRoomMeta = v;
|
|
1531
921
|
},
|
|
1532
922
|
get peerShort() {
|
|
1533
923
|
return _peerShort;
|
|
@@ -1536,10 +926,10 @@ const deps = {
|
|
|
1536
926
|
_peerShort = v;
|
|
1537
927
|
},
|
|
1538
928
|
get sessionStartedAt() {
|
|
1539
|
-
return
|
|
929
|
+
return _rootState().sessionStartedAt;
|
|
1540
930
|
},
|
|
1541
931
|
set sessionStartedAt(v) {
|
|
1542
|
-
|
|
932
|
+
_rootState().sessionStartedAt = v;
|
|
1543
933
|
},
|
|
1544
934
|
get cwdLock() {
|
|
1545
935
|
return _cwdLock;
|
|
@@ -1614,7 +1004,7 @@ const deps = {
|
|
|
1614
1004
|
return _disposed;
|
|
1615
1005
|
},
|
|
1616
1006
|
get lastEventCtx() {
|
|
1617
|
-
return
|
|
1007
|
+
return _rootState().baseCtx;
|
|
1618
1008
|
},
|
|
1619
1009
|
get pi() {
|
|
1620
1010
|
return _pi;
|
|
@@ -1642,7 +1032,7 @@ const deps = {
|
|
|
1642
1032
|
handleControl: _handleControl,
|
|
1643
1033
|
relayStatus: () => _relayStatus(relayDeps),
|
|
1644
1034
|
getState: _getState,
|
|
1645
|
-
runTestScenario:
|
|
1035
|
+
runTestScenario: (scenario) => runTestScenario({ broadcast: (env) => _broadcastEnvelope(relayDeps, env), emitBus: makeTestBusEmitter(() => _pi) }, scenario),
|
|
1646
1036
|
safeNotify: _safeNotify,
|
|
1647
1037
|
renameAgent: _renameAgent,
|
|
1648
1038
|
};
|
|
@@ -1703,10 +1093,10 @@ const _testHooks = createTestHooks({
|
|
|
1703
1093
|
_lockedName = v;
|
|
1704
1094
|
},
|
|
1705
1095
|
set sessionStartedAt(v) {
|
|
1706
|
-
|
|
1096
|
+
_rootState().sessionStartedAt = v;
|
|
1707
1097
|
},
|
|
1708
1098
|
set currentModel(v) {
|
|
1709
|
-
|
|
1099
|
+
_rootState().currentModel = v ?? null;
|
|
1710
1100
|
},
|
|
1711
1101
|
set pi(v) {
|
|
1712
1102
|
_pi = v;
|
|
@@ -1799,7 +1189,7 @@ const extension = (pi) => {
|
|
|
1799
1189
|
});
|
|
1800
1190
|
_subagentRooms?.dispose();
|
|
1801
1191
|
_subagentRooms = initSubagentRooms(pi, {
|
|
1802
|
-
getParentRoomId: () =>
|
|
1192
|
+
getParentRoomId: () => _rootState().myRoomId,
|
|
1803
1193
|
getParentSessionId: () => _rootSessionId,
|
|
1804
1194
|
broadcastPanel: (msg) => _panelBroadcast(relayDeps, msg),
|
|
1805
1195
|
});
|
|
@@ -1882,19 +1272,19 @@ const extension = (pi) => {
|
|
|
1882
1272
|
const level = event?.level;
|
|
1883
1273
|
if (!level)
|
|
1884
1274
|
return;
|
|
1885
|
-
// Cache per-sid; only the ROOT projects to
|
|
1275
|
+
// Cache per-sid; only the ROOT projects to room_meta.
|
|
1886
1276
|
const sid = _sidOf(ctx);
|
|
1887
|
-
_stateFor(sid)
|
|
1277
|
+
const st = _stateFor(sid);
|
|
1278
|
+
st.thinking = level;
|
|
1888
1279
|
if (_isNonRootSid(sid))
|
|
1889
1280
|
return;
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
if (!_relay || !_myRoomId)
|
|
1281
|
+
if (st.myRoomMeta)
|
|
1282
|
+
st.myRoomMeta = { ...st.myRoomMeta, thinking: level };
|
|
1283
|
+
if (!_relay || !st.myRoomId)
|
|
1894
1284
|
return;
|
|
1895
1285
|
_relay.sendControl({
|
|
1896
1286
|
type: "room_meta_update",
|
|
1897
|
-
room_id:
|
|
1287
|
+
room_id: st.myRoomId,
|
|
1898
1288
|
meta: { thinking: level },
|
|
1899
1289
|
});
|
|
1900
1290
|
});
|
|
@@ -1970,35 +1360,36 @@ const extension = (pi) => {
|
|
|
1970
1360
|
}
|
|
1971
1361
|
if (_isNonRootSid(sid))
|
|
1972
1362
|
return; // room_meta projection is root-only
|
|
1973
|
-
// Root projection: seed the
|
|
1363
|
+
// Root projection: seed the room_meta model + hello from the root's
|
|
1974
1364
|
// cached model, once.
|
|
1975
|
-
if (!
|
|
1365
|
+
if (!st.currentModel && st.model)
|
|
1976
1366
|
_setCurrentModel(st.model);
|
|
1977
1367
|
// Plan/32 Part B: publish working=true as room_meta (raw, no debounce —
|
|
1978
1368
|
// the debounce lives in the app). Same shape as the model/thinking updates.
|
|
1979
|
-
//
|
|
1980
|
-
if (
|
|
1981
|
-
|
|
1982
|
-
if (_relay &&
|
|
1369
|
+
// myRoomMeta is the ROOM projection (driven only by the root session).
|
|
1370
|
+
if (st.myRoomMeta)
|
|
1371
|
+
st.myRoomMeta = { ...st.myRoomMeta, working: true };
|
|
1372
|
+
if (_relay && st.myRoomId) {
|
|
1983
1373
|
_relay.sendControl({
|
|
1984
1374
|
type: "room_meta_update",
|
|
1985
|
-
room_id:
|
|
1375
|
+
room_id: st.myRoomId,
|
|
1986
1376
|
meta: { working: true },
|
|
1987
1377
|
});
|
|
1988
1378
|
}
|
|
1989
1379
|
});
|
|
1990
1380
|
pi.on("turn_end", (_event, ctx) => {
|
|
1991
1381
|
const sid = _sidOf(ctx);
|
|
1992
|
-
_stateFor(sid)
|
|
1382
|
+
const st = _stateFor(sid);
|
|
1383
|
+
st.working = false;
|
|
1993
1384
|
if (_isNonRootSid(sid))
|
|
1994
1385
|
return; // room_meta is root-only
|
|
1995
1386
|
// Plan/32 Part B: publish working=false as room_meta (raw, no debounce).
|
|
1996
|
-
if (
|
|
1997
|
-
|
|
1998
|
-
if (_relay &&
|
|
1387
|
+
if (st.myRoomMeta)
|
|
1388
|
+
st.myRoomMeta = { ...st.myRoomMeta, working: false };
|
|
1389
|
+
if (_relay && st.myRoomId) {
|
|
1999
1390
|
_relay.sendControl({
|
|
2000
1391
|
type: "room_meta_update",
|
|
2001
|
-
room_id:
|
|
1392
|
+
room_id: st.myRoomId,
|
|
2002
1393
|
meta: { working: false },
|
|
2003
1394
|
});
|
|
2004
1395
|
}
|
|
@@ -2043,8 +1434,9 @@ const extension = (pi) => {
|
|
|
2043
1434
|
if (_isNonRootSid(_sidOf(ctx)))
|
|
2044
1435
|
return;
|
|
2045
1436
|
_sessionName = name;
|
|
2046
|
-
|
|
2047
|
-
|
|
1437
|
+
const st = _rootState();
|
|
1438
|
+
if (st.myRoomMeta)
|
|
1439
|
+
st.myRoomMeta = { ...st.myRoomMeta, name };
|
|
2048
1440
|
_broadcastEnvelope(relayDeps, {
|
|
2049
1441
|
evt: { channel: "session_info", data: { name } },
|
|
2050
1442
|
});
|
|
@@ -2053,10 +1445,10 @@ const extension = (pi) => {
|
|
|
2053
1445
|
// reaches attached peers, so pre-attach the Home tile went stale until open
|
|
2054
1446
|
// or reconnect. Relay >=0.6.0 merges + fans `name`; older relays drop the
|
|
2055
1447
|
// unknown field (harmless — attached peers still get the evt). Design 01M1SPN7.
|
|
2056
|
-
if (_relay &&
|
|
1448
|
+
if (_relay && st.myRoomId) {
|
|
2057
1449
|
_relay.sendControl({
|
|
2058
1450
|
type: "room_meta_update",
|
|
2059
|
-
room_id:
|
|
1451
|
+
room_id: st.myRoomId,
|
|
2060
1452
|
meta: { name },
|
|
2061
1453
|
});
|
|
2062
1454
|
}
|
|
@@ -2090,12 +1482,11 @@ const extension = (pi) => {
|
|
|
2090
1482
|
// session_start with their own ctx). Each records its OWN sessionManager —
|
|
2091
1483
|
// no cross-session clobber (was the unguarded `_sessionManager = ...` bug).
|
|
2092
1484
|
const sid = _sidOf(ctx);
|
|
2093
|
-
//
|
|
2094
|
-
//
|
|
2095
|
-
//
|
|
2096
|
-
//
|
|
2097
|
-
|
|
2098
|
-
_lastEventCtx = ctx;
|
|
1485
|
+
// Base ctx (compact/notify fallback when no fresh ctx is passed) is
|
|
1486
|
+
// PER-SESSION (plan 01M18RNH) — each session_start records its OWN; the
|
|
1487
|
+
// base-ctx resolution reads the ROOT's, so a child's ctx never leaks
|
|
1488
|
+
// (structurally replaces the old `!_isNonRootSid` guard + global).
|
|
1489
|
+
_stateFor(sid).baseCtx = ctx ?? null;
|
|
2099
1490
|
// EVENT-CTX TARGETED PROBE (run 2026-09-18, round 3): the ctx objects
|
|
2100
1491
|
// are guarded (throwing getters — proxies), so full reflection
|
|
2101
1492
|
// under-reports or throws. Per-property access, each caught, is the
|
|
@@ -2131,7 +1522,7 @@ const extension = (pi) => {
|
|
|
2131
1522
|
});
|
|
2132
1523
|
if (!_subagentRooms)
|
|
2133
1524
|
_subagentRooms = initSubagentRooms(pi, {
|
|
2134
|
-
getParentRoomId: () =>
|
|
1525
|
+
getParentRoomId: () => _rootState().myRoomId,
|
|
2135
1526
|
getParentSessionId: () => _rootSessionId,
|
|
2136
1527
|
broadcastPanel: (msg) => _panelBroadcast(relayDeps, msg),
|
|
2137
1528
|
});
|
|
@@ -2295,8 +1686,8 @@ const extension = (pi) => {
|
|
|
2295
1686
|
}
|
|
2296
1687
|
// Drop the captured event ctx immediately. On module-reuse hosts the same
|
|
2297
1688
|
// instance survives session replacement; the next session_start re-binds
|
|
2298
|
-
//
|
|
2299
|
-
|
|
1689
|
+
// the root record's baseCtx for the new session.
|
|
1690
|
+
_rootState().baseCtx = null;
|
|
2300
1691
|
// No bye reason: the process keeps running and the fresh instance re-joins
|
|
2301
1692
|
// the SAME relay room, so an explicit offline→online flap would be wrong.
|
|
2302
1693
|
// Revoke producer/Relay/bridge authority while the global node is still
|
|
@@ -2463,7 +1854,7 @@ function _deliverMeshMessageToAgent(env) {
|
|
|
2463
1854
|
* handles incoming app→pi requests.
|
|
2464
1855
|
*/
|
|
2465
1856
|
function _abortCurrentTurn(fallbackCtx) {
|
|
2466
|
-
const candidates = [
|
|
1857
|
+
const candidates = [_rootState().baseCtx, fallbackCtx];
|
|
2467
1858
|
for (const candidate of candidates) {
|
|
2468
1859
|
if (!candidate || candidate === _noopCtx)
|
|
2469
1860
|
continue;
|