@geohar/unbien-durable 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +52 -2
- package/dist/agent_config.d.ts +56 -0
- package/dist/agent_config.js +92 -0
- package/dist/agent_config.js.map +1 -0
- package/dist/bridge.d.ts +175 -0
- package/dist/bridge.js +921 -0
- package/dist/bridge.js.map +1 -0
- package/dist/event_source.d.ts +53 -0
- package/dist/event_source.js +124 -0
- package/dist/event_source.js.map +1 -0
- package/dist/extension.d.ts +34 -0
- package/dist/extension.js +46 -0
- package/dist/extension.js.map +1 -0
- package/dist/host.d.ts +2 -0
- package/dist/host.js +261 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/launch_wait_quit.d.ts +12 -0
- package/dist/launch_wait_quit.js +136 -0
- package/dist/launch_wait_quit.js.map +1 -0
- package/dist/mesh_delivery.d.ts +28 -0
- package/dist/mesh_delivery.js +31 -0
- package/dist/mesh_delivery.js.map +1 -0
- package/dist/mirror.d.ts +56 -0
- package/dist/mirror.js +136 -0
- package/dist/mirror.js.map +1 -0
- package/dist/ollama_cloud.d.ts +2 -0
- package/dist/ollama_cloud.js +98 -0
- package/dist/ollama_cloud.js.map +1 -0
- package/dist/pi_launch.d.ts +50 -0
- package/dist/pi_launch.js +84 -0
- package/dist/pi_launch.js.map +1 -0
- package/dist/port.d.ts +94 -0
- package/dist/port.js +121 -0
- package/dist/port.js.map +1 -0
- package/dist/registrar.d.ts +7 -0
- package/dist/registrar.js +31 -0
- package/dist/registrar.js.map +1 -0
- package/dist/thinking.d.ts +24 -0
- package/dist/thinking.js +36 -0
- package/dist/thinking.js.map +1 -0
- package/dist/view_translator.d.ts +83 -0
- package/dist/view_translator.js +317 -0
- package/dist/view_translator.js.map +1 -0
- package/package.json +63 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 George Harker
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,53 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @geohar/unbien-durable
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A [pi-durable](https://github.com/earendil-works/pi) agent host on the
|
|
4
|
+
[un-bien](https://github.com/georgeharker/un-bien) mesh and relay. The
|
|
5
|
+
Un Bien app talks to a durable conversation exactly as it talks to a pi
|
|
6
|
+
session: the same transcript, live streaming, tool cards, model and thinking
|
|
7
|
+
controls, and pairing.
|
|
8
|
+
|
|
9
|
+
Durable conversations have stable ids and survive restarts, so the app
|
|
10
|
+
reconnects to the same room and its history after the host restarts.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install -g @geohar/unbien-durable
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Running the host
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
unbien-durable [--cwd <dir>] [--name <agent-name>] [--relay <url>]
|
|
20
|
+
[--db <path>] [--model <provider/model>]
|
|
21
|
+
[--thinking <level>] [--no-pairing-qr]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
| Option | Meaning |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `--cwd` | Working directory of the conversation (default: the current directory) |
|
|
27
|
+
| `--name` | Name shown in the app and on the mesh (default: `durable`) |
|
|
28
|
+
| `--relay` | Relay URL (default: the configured un-bien relay) |
|
|
29
|
+
| `--db` | JSONL storage directory (default: `~/.local/state/un-bien/durable/session`) |
|
|
30
|
+
| `--model` | `provider/modelId` for a new conversation |
|
|
31
|
+
| `--thinking` | Thinking level a conversation still at `off` starts at (default: `medium`, clamped to the model); `off` keeps pi-durable's default |
|
|
32
|
+
| `--no-pairing-qr` | Don't mint the rotating pairing QR; pair with `unbien pair-token` instead |
|
|
33
|
+
|
|
34
|
+
Provider API keys come from the environment, as for pi (`ANTHROPIC_API_KEY`,
|
|
35
|
+
`OPENAI_API_KEY`, `OLLAMA_API_KEY`, …). `UNBIEN_NO_PAIRING_QR` and
|
|
36
|
+
`UNBIEN_DURABLE_THINKING` match the flags.
|
|
37
|
+
|
|
38
|
+
## As a library
|
|
39
|
+
|
|
40
|
+
The package also exports the binding itself, for hosts that compose their own
|
|
41
|
+
harness (models, tools, extensions):
|
|
42
|
+
|
|
43
|
+
- `DurableBridge`: serves one conversation over the relay. Pass the harness's
|
|
44
|
+
`models` to enable the app's model, thinking, rename and queue controls.
|
|
45
|
+
- `DurableSessionPort` / `DurableSessionControl` / `DurableAgentConfig`: the
|
|
46
|
+
un-bien core ports over a durable `Conversation`.
|
|
47
|
+
- `ConversationViewSource` / `ViewTranslator`: the conversation view as pi's
|
|
48
|
+
event plane.
|
|
49
|
+
- `createUnBienDurableExtension`: the mesh tools and `launch_pi_task`.
|
|
50
|
+
|
|
51
|
+
## Requirements
|
|
52
|
+
|
|
53
|
+
Node 22.19 or later. Built on `@earendil-works/pi-durable` 1.5.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable binding's AgentConfigPort: the app's model / thinking-level /
|
|
3
|
+
* session-name / queue verbs over a durable Conversation.
|
|
4
|
+
*
|
|
5
|
+
* getModelState / listModels / setModel / setThinkingLevel
|
|
6
|
+
* → `conversation.agent()` / `configure()` against the harness's model
|
|
7
|
+
* registry. setModel accepts only models with credentials
|
|
8
|
+
* (`models.getAvailable()`), the same answer pi's registry gives, and
|
|
9
|
+
* like pi clamps the thinking level to what the new model supports;
|
|
10
|
+
* setThinkingLevel clamps to the current model. The agent state is
|
|
11
|
+
* durable, so both survive restarts.
|
|
12
|
+
* setSessionName → the bridge's rename (room name, live confirmation,
|
|
13
|
+
* persisted record).
|
|
14
|
+
* clearQueue → aborts each queued steer / follow-up submission in
|
|
15
|
+
* `pi.inbox`; resolves to the text of those withdrawn.
|
|
16
|
+
*
|
|
17
|
+
* There is no command registry, so `isKnownSlash` is absent: slash tokens
|
|
18
|
+
* pass to the agent as text.
|
|
19
|
+
*/
|
|
20
|
+
import type { Context } from "@earendil-works/chord";
|
|
21
|
+
import { type Models } from "@earendil-works/pi-ai/models";
|
|
22
|
+
import type { Conversation, Harness, InboxState } from "@earendil-works/pi-durable";
|
|
23
|
+
import type { AgentConfigPort, ThinkingLevel, WireModel } from "@geohar/un-bien/core";
|
|
24
|
+
export interface DurableAgentConfigOptions {
|
|
25
|
+
/** The conversation served now (the bridge retargets it). */
|
|
26
|
+
conversation(): Conversation;
|
|
27
|
+
harness: Harness;
|
|
28
|
+
models: Models;
|
|
29
|
+
context: Context;
|
|
30
|
+
/** The conversation's queue, as of the latest view value. */
|
|
31
|
+
inbox(): InboxState | undefined;
|
|
32
|
+
/** Rename the served session (the bridge's room + record). */
|
|
33
|
+
rename(name: string): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
export declare class DurableAgentConfig implements AgentConfigPort {
|
|
36
|
+
private readonly opts;
|
|
37
|
+
constructor(opts: DurableAgentConfigOptions);
|
|
38
|
+
getModelState(): Promise<{
|
|
39
|
+
model?: {
|
|
40
|
+
id: string;
|
|
41
|
+
provider: string;
|
|
42
|
+
};
|
|
43
|
+
thinkingLevel?: string;
|
|
44
|
+
}>;
|
|
45
|
+
listModels(): Promise<{
|
|
46
|
+
models: WireModel[];
|
|
47
|
+
current?: WireModel;
|
|
48
|
+
}>;
|
|
49
|
+
setModel(provider: string, modelId: string): Promise<WireModel>;
|
|
50
|
+
setThinkingLevel(level: ThinkingLevel): Promise<void>;
|
|
51
|
+
setSessionName(name: string): Promise<void>;
|
|
52
|
+
clearQueue(): Promise<{
|
|
53
|
+
steering: string[];
|
|
54
|
+
followUp: string[];
|
|
55
|
+
}>;
|
|
56
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { clampThinkingLevel } from "@earendil-works/pi-ai/models";
|
|
2
|
+
import { wireFromModel } from "@geohar/un-bien/durable";
|
|
3
|
+
export class DurableAgentConfig {
|
|
4
|
+
opts;
|
|
5
|
+
constructor(opts) {
|
|
6
|
+
this.opts = opts;
|
|
7
|
+
}
|
|
8
|
+
async getModelState() {
|
|
9
|
+
const agent = await this.opts.conversation().agent(this.opts.context);
|
|
10
|
+
return {
|
|
11
|
+
...(agent.model
|
|
12
|
+
? { model: { id: agent.model.modelId, provider: agent.model.provider } }
|
|
13
|
+
: {}),
|
|
14
|
+
thinkingLevel: agent.thinkingLevel,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
async listModels() {
|
|
18
|
+
const [available, agent] = await Promise.all([
|
|
19
|
+
this.opts.models.getAvailable(),
|
|
20
|
+
this.opts.conversation().agent(this.opts.context),
|
|
21
|
+
]);
|
|
22
|
+
const current = agent.model
|
|
23
|
+
? this.opts.models.getModel(agent.model.provider, agent.model.modelId)
|
|
24
|
+
: undefined;
|
|
25
|
+
return {
|
|
26
|
+
models: available.map(wireFromModel),
|
|
27
|
+
current: current ? wireFromModel(current) : undefined,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
async setModel(provider, modelId) {
|
|
31
|
+
const model = this.opts.models.getModel(provider, modelId);
|
|
32
|
+
if (!model)
|
|
33
|
+
throw new Error(`model "${provider}/${modelId}" not in registry`);
|
|
34
|
+
const available = await this.opts.models.getAvailable(provider);
|
|
35
|
+
if (!available.some((m) => m.id === modelId))
|
|
36
|
+
throw new Error("no auth configured for this model");
|
|
37
|
+
const conversation = this.opts.conversation();
|
|
38
|
+
const agent = await conversation.agent(this.opts.context);
|
|
39
|
+
const thinkingLevel = clampThinkingLevel(model, agent.thinkingLevel);
|
|
40
|
+
await conversation.configure({
|
|
41
|
+
model: { provider, modelId },
|
|
42
|
+
...(thinkingLevel !== agent.thinkingLevel ? { thinkingLevel } : {}),
|
|
43
|
+
}, this.opts.context);
|
|
44
|
+
return wireFromModel(model);
|
|
45
|
+
}
|
|
46
|
+
async setThinkingLevel(level) {
|
|
47
|
+
const conversation = this.opts.conversation();
|
|
48
|
+
const agent = await conversation.agent(this.opts.context);
|
|
49
|
+
const model = agent.model
|
|
50
|
+
? this.opts.models.getModel(agent.model.provider, agent.model.modelId)
|
|
51
|
+
: undefined;
|
|
52
|
+
const target = model
|
|
53
|
+
? clampThinkingLevel(model, level)
|
|
54
|
+
: level;
|
|
55
|
+
await conversation.configure({ thinkingLevel: target }, this.opts.context);
|
|
56
|
+
}
|
|
57
|
+
async setSessionName(name) {
|
|
58
|
+
await this.opts.rename(name);
|
|
59
|
+
}
|
|
60
|
+
async clearQueue() {
|
|
61
|
+
const cleared = { steering: [], followUp: [] };
|
|
62
|
+
for (const item of this.opts.inbox()?.items ?? []) {
|
|
63
|
+
if (item.mode === "write")
|
|
64
|
+
continue;
|
|
65
|
+
const submission = await this.opts.harness.submission(item.id, this.opts.context);
|
|
66
|
+
if (!submission)
|
|
67
|
+
continue;
|
|
68
|
+
// "already_placed": the run took it first; "settled": already done.
|
|
69
|
+
if ((await submission.abort(this.opts.context)) !== "aborted")
|
|
70
|
+
continue;
|
|
71
|
+
const text = inputText(item.content);
|
|
72
|
+
if (item.mode === "steer")
|
|
73
|
+
cleared.steering.push(text);
|
|
74
|
+
else
|
|
75
|
+
cleared.followUp.push(text);
|
|
76
|
+
}
|
|
77
|
+
return cleared;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** The text of a queued input: a string, or its text blocks joined. */
|
|
81
|
+
function inputText(content) {
|
|
82
|
+
if (typeof content === "string")
|
|
83
|
+
return content;
|
|
84
|
+
if (!Array.isArray(content))
|
|
85
|
+
return "";
|
|
86
|
+
return content
|
|
87
|
+
.filter((block) => block?.type === "text" &&
|
|
88
|
+
typeof block.text === "string")
|
|
89
|
+
.map((block) => block.text)
|
|
90
|
+
.join("\n");
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=agent_config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent_config.js","sourceRoot":"","sources":["../src/agent_config.ts"],"names":[],"mappings":"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"}
|
package/dist/bridge.d.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type { Context } from "@earendil-works/chord";
|
|
2
|
+
import type { ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { Models } from "@earendil-works/pi-ai/models";
|
|
4
|
+
import type { Conversation, Harness } from "@earendil-works/pi-durable";
|
|
5
|
+
import { RelayClient, type Ed25519Keypair } from "@geohar/un-bien/durable";
|
|
6
|
+
import type { NotifyLevel } from "@geohar/un-bien/core";
|
|
7
|
+
export interface DurableBridgeOptions {
|
|
8
|
+
harness: Harness;
|
|
9
|
+
context: Context;
|
|
10
|
+
/** http(s):// relay URL (converted to ws(s):// at the boundary). */
|
|
11
|
+
relayUrl: string;
|
|
12
|
+
/** Display name for room_meta + pair_ok. */
|
|
13
|
+
displayName: string;
|
|
14
|
+
/** Machine name the app records (defaults to os.hostname()). Distinct for
|
|
15
|
+
* the durable host so the app lists it as its own machine, not the Mac. */
|
|
16
|
+
machineName?: string;
|
|
17
|
+
/** The conversations' working directory. */
|
|
18
|
+
cwd: string;
|
|
19
|
+
/** Pairing QR rotation cadence in ms (default: the 60s token TTL, so the
|
|
20
|
+
* invite renews exactly as the previous token expires). Tests shrink it
|
|
21
|
+
* to observe rotation in real time. */
|
|
22
|
+
pairingRotationMs?: number;
|
|
23
|
+
/** Suppress the auto-minted rotating pairing QR (no user gesture minted
|
|
24
|
+
* it — headless hosts pair via the CLI instead). Default: false, the
|
|
25
|
+
* rotation mints as before. */
|
|
26
|
+
pairingQr?: boolean;
|
|
27
|
+
/** Relay client factory — tests inject a fake; production gets the real
|
|
28
|
+
* RelayClient. */
|
|
29
|
+
createRelay?: (wsUrl: string, keypair: Ed25519Keypair) => RelayClient;
|
|
30
|
+
/** The harness's model registry. With it, each attached conversation
|
|
31
|
+
* still at thinking level "off" (pi-durable's default) is raised to
|
|
32
|
+
* `thinkingLevel`, clamped to its model — see thinking.ts. */
|
|
33
|
+
models?: Models;
|
|
34
|
+
/** Default thinking level (default "medium", pi's). "off" disables. */
|
|
35
|
+
thinkingLevel?: ModelThinkingLevel;
|
|
36
|
+
/** Host log surface (notify + async failures). */
|
|
37
|
+
log(message: string, level?: NotifyLevel): void;
|
|
38
|
+
/** Graceful host shutdown (terminate frame) — close bridge + harness. */
|
|
39
|
+
shutdownHost(): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
export declare class DurableBridge {
|
|
42
|
+
private readonly opts;
|
|
43
|
+
private relay;
|
|
44
|
+
private readonly activePeers;
|
|
45
|
+
private keypair;
|
|
46
|
+
private conversation;
|
|
47
|
+
private mirror;
|
|
48
|
+
private busy;
|
|
49
|
+
private port;
|
|
50
|
+
private control;
|
|
51
|
+
private events;
|
|
52
|
+
private rpcEnvelope;
|
|
53
|
+
private myRoomId;
|
|
54
|
+
private sessionStartedAt;
|
|
55
|
+
private reconnectTimer;
|
|
56
|
+
private reconnectAttempt;
|
|
57
|
+
private stopped;
|
|
58
|
+
private pairingRotationActive;
|
|
59
|
+
private pairingRotationTimer;
|
|
60
|
+
/** The last minted rotating invite (null when paired/not rotating) —
|
|
61
|
+
* surfaced via currentPairingInvite() for host-side web pages. */
|
|
62
|
+
private lastPairingInvite;
|
|
63
|
+
/** The served session's display name: the newest rename recorded in the
|
|
64
|
+
* conversation, else the host's displayName. */
|
|
65
|
+
private sessionName;
|
|
66
|
+
private agentConfig;
|
|
67
|
+
constructor(opts: DurableBridgeOptions);
|
|
68
|
+
/** The currently served conversation (stable id across restarts). */
|
|
69
|
+
get currentConversation(): Conversation;
|
|
70
|
+
/** Live busy flag (run in flight) — the port's `streaming`. */
|
|
71
|
+
get isBusy(): boolean;
|
|
72
|
+
/** Attach to a conversation: port + control + watch + event fanout. */
|
|
73
|
+
attach(conversation: Conversation): Promise<void>;
|
|
74
|
+
/** Connect the relay + announce this conversation's room. */
|
|
75
|
+
start(): Promise<void>;
|
|
76
|
+
/** Full teardown (host shutdown / retarget). */
|
|
77
|
+
stop(): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Session replacement WITHOUT replacement machinery: flush a
|
|
80
|
+
* session_shutdown to the old room, stop everything, attach to the new
|
|
81
|
+
* conversation, reconnect under its room. The app drops the old
|
|
82
|
+
* conversation on the new hello (changed sessionId), exactly as it does
|
|
83
|
+
* for pi.
|
|
84
|
+
*/
|
|
85
|
+
retarget(conversation: Conversation): Promise<void>;
|
|
86
|
+
private buildPort;
|
|
87
|
+
private buildControl;
|
|
88
|
+
private connectRelay;
|
|
89
|
+
private scheduleReconnect;
|
|
90
|
+
/** Fail-closed signed allow-list push (the relay refuses rooms whose
|
|
91
|
+
* owners are not listed). Same single-signer path as the pi extension. */
|
|
92
|
+
private pushPairingAllowList;
|
|
93
|
+
/**
|
|
94
|
+
* The auto-listener: pair_request from new devices, reconnect attach for
|
|
95
|
+
* known peers, unknown-peer refusal. The durable port of the pi
|
|
96
|
+
* extension's listener — no listener-authority generations (a stopped
|
|
97
|
+
* bridge is `this.relay !== relay`, checked directly).
|
|
98
|
+
*/
|
|
99
|
+
private installAutoListener;
|
|
100
|
+
private handlePairRequest;
|
|
101
|
+
/**
|
|
102
|
+
* The startup pairing invite: while the machine has no paired peers, the
|
|
103
|
+
* bridge ITSELF mints the rotating QR on the same qrSession the
|
|
104
|
+
* pair_request handler consumes from — one process issues AND validates
|
|
105
|
+
* the token, so a scan can never hit "token was not issued by this host"
|
|
106
|
+
* (the failure mode of minting in a separate CLI process). Armed on every
|
|
107
|
+
* successful relay connect (startup + reconnects); the first tick reads
|
|
108
|
+
* peers.json and retires silently when a peer already exists, so a
|
|
109
|
+
* revoked-then-restarted host re-arms on its next start().
|
|
110
|
+
*/
|
|
111
|
+
private armPairingRotation;
|
|
112
|
+
/** Fire-and-forget one rotation step: a rejection (listPeers on a wedged
|
|
113
|
+
* broker socket, …) logs + retires the rotation instead of escaping as
|
|
114
|
+
* an unhandled rejection — the rotation re-arms on the next successful
|
|
115
|
+
* relay connect. */
|
|
116
|
+
private tickPairingRotation;
|
|
117
|
+
/** One rotation step: retire when peers exist, otherwise mint + log a
|
|
118
|
+
* fresh invite and schedule the next tick. */
|
|
119
|
+
private rotatePairingQR;
|
|
120
|
+
/** Cancel the rotation (a peer paired / teardown). No qrSession.clear()
|
|
121
|
+
* here: the outstanding token either was just consumed by the pairing
|
|
122
|
+
* handshake or dies with the process — consumption is same-process by
|
|
123
|
+
* design. */
|
|
124
|
+
private stopPairingRotation;
|
|
125
|
+
/** The CURRENT pairing invite as a deep-link URI — what a /pair web page
|
|
126
|
+
* renders as a tappable link. null when not rotating (paired already, or
|
|
127
|
+
* rotation stopped). Updated on every rotation tick. */
|
|
128
|
+
currentPairingInvite(): string | null;
|
|
129
|
+
/** Ask attached app peers to authorize: ONE live toast
|
|
130
|
+
* (extension_ui_request notify) is the visible channel. An authorize
|
|
131
|
+
* pending expires in minutes, so nothing replays it; a late peer gets a
|
|
132
|
+
* fresh mint when the model calls the tool again. The request is also
|
|
133
|
+
* recorded as a hidden app entry (customType "un-bien:auth-required",
|
|
134
|
+
* display:false), kept for the record and out of model context.
|
|
135
|
+
*
|
|
136
|
+
* `message` is the elicit text and names the real upstream server;
|
|
137
|
+
* `server` is the caller's label (the combiner bridge, say), kept in
|
|
138
|
+
* details. `url` is appended unless the message already carries it, and
|
|
139
|
+
* may be empty for a prompt with nothing to open. */
|
|
140
|
+
sendAuthorizeCard(server: string, url: string, message: string): Promise<void>;
|
|
141
|
+
/** Rename the served session: record it in the conversation (a hidden app
|
|
142
|
+
* entry, read back at attach), then confirm it live as pi does — the
|
|
143
|
+
* session_info evt to attached apps and room_meta_update so the relay
|
|
144
|
+
* pushes the name to room subscribers. */
|
|
145
|
+
private renameSession;
|
|
146
|
+
/** Record a run that ended without an answer as a display app entry, so
|
|
147
|
+
* the app shows why (live, and on replay). `nothing_to_answer` (a
|
|
148
|
+
* continue with nothing pending) is not a failure anyone needs to see. */
|
|
149
|
+
private recordRunFailure;
|
|
150
|
+
/** Mint a FRESH pairing invite on demand — for host-side pairing pages
|
|
151
|
+
* (/pair) so a SECOND device can be added after the rotation retired, or
|
|
152
|
+
* an operator can open pairing mode without a restart. One process mints
|
|
153
|
+
* + consumes (this bridge), so the token race is structurally excluded.
|
|
154
|
+
* Multi-device safe: adding a peer appends to peers.json; the page's
|
|
155
|
+
* caller gates availability (enable/disable toggle). */
|
|
156
|
+
forcePairingInvite(): string | null;
|
|
157
|
+
private emitPairOk;
|
|
158
|
+
/** un-plane error reply, correlated to the request id when present.
|
|
159
|
+
* Same best-effort wire as sendEnvelope — a relay hiccup mid-reply must
|
|
160
|
+
* not throw into the fire-and-forget ub dispatch. */
|
|
161
|
+
private sendUbError;
|
|
162
|
+
private sendInner;
|
|
163
|
+
/** Attach (or re-attach) an owner channel and say hello. */
|
|
164
|
+
private attachOwner;
|
|
165
|
+
private buildRpcDeps;
|
|
166
|
+
/** Envelope-carried pi rpc command dispatch (the shared handlers). */
|
|
167
|
+
private routeRpc;
|
|
168
|
+
/** un-bien plane: session_sync (minimal ack) + session_fork/
|
|
169
|
+
* session_navigate + terminate. */
|
|
170
|
+
private routeUnBienPlane;
|
|
171
|
+
/** Stock ClientMessage switch: ping/pong + cancel. */
|
|
172
|
+
private routeStock;
|
|
173
|
+
private broadcast;
|
|
174
|
+
private broadcastFlushed;
|
|
175
|
+
}
|