@ai-ecoverse/slicc-agent 5.10.1 → 6.0.1

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/README.md CHANGED
@@ -28,15 +28,19 @@ await agent.prompt('What is in /home?');
28
28
 
29
29
  - `openAgent({ models, model, storage?, registry?, settings? })` opens a durable Harness (in memory unless `storage` is given) and its root conversation, and resumes work a previous worker left unfinished.
30
30
  - `openOpfsSqliteStorage({ directory?, file? })` keeps the session in wasm SQLite ([`@sqlite.org/sqlite-wasm`](https://www.npmjs.com/package/@sqlite.org/sqlite-wasm)) in an `opfs-sahpool` pool, by default `/.slicc/agent/` in OPFS. It needs a dedicated worker. Only one worker holds a pool at a time: the opener takes a Web Lock per directory and waits until every file of the pool can be opened before sqlite-wasm installs it, because a failed install deletes the pool's directory. `openMemorySqliteStorage()` keeps the same database in memory.
31
- - `hostAgent(agent)` serves the agent from one Chord host named `agent` (`serveAgent` in [`src/host.ts`](src/host.ts)). Its remote contracts, in [`src/services.ts`](src/services.ts), are `slicc.agent.control` (send with `whenBusy` `steer`, `followUp` or `reject`, wait, withdraw, abort, compact) and `slicc.agent.transcript` (the conversation's durable view as replicated state, plus `deliveries`: how each user entry was delivered, `run`, `steer` or `follow-up`, derived by the host from where each send it accepted was placed: after a tool result it steered, queued first it's a follow-up or a late steer per its `whenBusy`, placed at once it started a run), `slicc.agent.settings`, `slicc.agent.remote` (remote control), `slicc.agent.pages` (Panels) and `slicc.agent.guest` (Guest seats). `serveConnections(self, host)` hands it every `MessagePort` the page sends, and each port becomes one Chord link whose principal is a seat: `owner` (a page of the owner, with its own Panels), `control` (a follower with Control), `view` (a follower without) or `guest` (a guest seat, with its id, label and epoch), plus the link's number, so every link gets its own objects. Each factory picks a variant per seat. The contracts take their methods' arguments as one JSON array, and the client in [`src/serving.ts`](src/serving.ts) turns them back into the `AgentControl`, `AgentSettings`, `AgentRemote` and `AgentPages` interfaces and their replicated states, so callers don't see Chord.
31
+ - `hostAgent(agent)` serves the agent from one Chord host named `agent` (`serveAgent` in [`src/host.ts`](src/host.ts)). Its remote contracts, in [`src/services.ts`](src/services.ts), are `slicc.agent.control` (send with `whenBusy` `steer`, `followUp` or `reject`, wait, withdraw, abort, compact) and `slicc.agent.transcript` (the conversation's durable view as replicated state, plus `deliveries`: how each user entry was delivered, `run`, `steer` or `follow-up`, derived by the host from where each send it accepted was placed: after a tool result it steered, queued first it's a follow-up or a late steer per its `whenBusy`, placed at once it started a run), `slicc.agent.settings`, `slicc.agent.remote` (remote control), `slicc.agent.pages` (Panels) and `slicc.agent.guest` (Guest seats). `serveConnections(self, host)` hands it every `MessagePort` the page sends, and each port becomes one Chord link whose principal is a seat: `owner` (a page of the owner, with its own Panels), `control` (a follower with Control), `view` (a follower without) or `guest` (a guest seat, with its id, label and epoch), plus the link's number, so every link gets its own objects. The principal is typed (`AgentPrincipal` in [`src/serving.ts`](src/serving.ts)); the host's own code acts as `{ seat: 'host' }`, which no link gets. Each factory picks a variant per seat. Every contract places its objects on the host `agent` (`hostNamed`), so no caller names a host. The contracts take their methods' arguments as one JSON array, and the client in [`src/serving.ts`](src/serving.ts) turns them back into the `AgentControl`, `AgentSettings`, `AgentRemote` and `AgentPages` interfaces and their replicated states, so callers don't see Chord.
32
32
  - `@ai-ecoverse/slicc-agent/page` has what the page needs (`startAgent`, `connectAgent`) without the agent's runtime: no pi-durable, pi-ai, providers or SQLite, which load only in the worker. `@ai-ecoverse/slicc-agent/spectrum` is just as light, and a test keeps both that way.
33
33
  - `startAgent({ worker })` takes the Web Lock `slicc-agent`, so one tab at a time owns the agent worker, and waits while another tab holds it. `connect()` returns an `AgentConnection` (`control`, `transcript`, `prompt()`, and `webhook(name, { id?, headers?, body })`, which hands a webhook delivery, such as one from the tray hub, to the licks without a chord `Context`), `restart()` replaces the worker (durable resumes from SQLite), and `release()` stops it and lets the next tab take over. `port()` returns a raw `MessagePort` into the worker instead, for another tab: hand it over (through a SharedWorker, say), and that tab calls `connectAgent(port)`. A caller that already holds the lock passes `held: true`. A connection's `closed` resolves when its port ends (with the error, if any), and from then on its states keep their last values.
34
34
  - **Followers over a link** (PR 18): `port({ readOnly: true })` and `connect({ readOnly: true })` give a view-only connection, and `port({ pages: false })` a full one that never takes `panel` requests (see Panels).
35
35
  - **What a view-only connection can do:** it sees the transcript, the agents, settings state and remote-control `watch`, and may call `wait` and `changesOpen`. It's still an owner device, so it has the owner-device methods (`OWNER_DEVICE_METHODS` in [`src/services.ts`](src/services.ts)) with the owner's powers: `decide`, `guestOpen`, `guestRevoke`, `guestAlwaysForget` and `approvalPolicySet`. Control gates driving, not approvals.
36
36
  - **What it can't:** every other `AgentControl` method, settings action and remote-control `prompt` or `abort` is refused with `EACCES` ("this follower can watch, but not send, steer or abort"). Send and compact answer `accepted: false` with that code. The `view` seat's variants don't have those methods at all (`view` for control and settings, `watch` for remote control, `none` for Panels); the follower's client answers the missing ones with the refusals ([`src/readonly.ts`](src/readonly.ts)), so the worker never runs them. A `watch` stream is readable only over the link that opened it.
37
37
  - **Carrying the connection over a byte stream:** `linkEndpoint({ write, close })` is a `PortEndpoint` that `connectAgent` accepts. Feed it the stream's bytes with `receive(bytes)` and its end with `end(error?)`. On the owner's side, `bridgePort(port, { write, close })` does the same for the `MessagePort` from `port()`.
38
- - **The bytes:** the owner first posts a hello, its server id, then each Chord frame travels as its JSON text in UTF-8 (`PortLink` in [`src/wire.ts`](src/wire.ts)), and `{ close: true }` ends the port. On a byte stream each message becomes a frame: a `u32` big-endian length, then a tag (0 data, 1 hello, 2 close), then the payload. A frame is at most 16 MiB, and a malformed one ends the link.
39
- - **Another agent version:** when Chord refuses the handshake because the owner's contracts don't match (`upgrade_required` or `contract_mismatch`), `connectAgent` and `connectGuest` close the link and reject once with `code: 'EVERSION'` and the sentence "That SLICC runs another version of the agent, so this tab can't follow it. Update both SLICCs, then reload this page." (Chord's error is its `cause`). It is final: a follower should show the sentence and not retry.
38
+ - **The bytes:** the owner first posts a hello, its server id, then each Chord frame travels as its JSON text in UTF-8 (`PortLink` in [`src/wire.ts`](src/wire.ts)), and `{ close: true }` ends the port. The owner holds its own Chord frames until the client's Chord hello arrives, so it knows which Chord it talks to before it says anything. `PortLink.send` returns each frame's size, so Chord's flow control applies: once 256 KiB are unacknowledged, state waits at the owner, coalesced per member, instead of piling up in front of a slow WebRTC follower, while calls and results still pass. On a byte stream each message becomes a frame: a `u32` big-endian length, then a tag (0 data, 1 hello, 2 close), then the payload. A frame is at most 16 MiB, and a malformed one ends the link.
39
+ - **Another agent version:** `connectAgent` and `connectGuest` close the link and reject once with `code: 'EVERSION'` and the sentence "That SLICC runs another version of the agent, so this tab can't follow it. Update both SLICCs, then reload this page." It is final: a follower should show the sentence and not retry. That happens when:
40
+ - Chord refuses the handshake because the owner's contracts don't match (`upgrade_required` or `contract_mismatch`; Chord's error is the `cause`);
41
+ - Chord reports another link protocol (`onProtocolMismatch`, from Chord 1.3 on);
42
+ - the owner runs agent 5.x, whose Chord 1.2 hello carries no protocol version: the client leaves at once.
43
+ - **A 5.x follower or guest of a 6.x owner:** the owner sees its Chord 1.2 hello, never hands that link to Chord, and answers each of its holds with Chord 1.2's `gone` and `upgrade_required`, which 5.5.2 and later already turn into `EVERSION`. Agent 6.0 runs Chord 1.3, whose link protocol (4) refuses 1.2 peers, so 5.x and 6.x don't follow each other.
40
44
 
41
45
  ### Bash and files on slicc-kernel
42
46
 
@@ -623,7 +627,7 @@ Every approval, of a guest's prompt, of a tool call in a run a guest started, or
623
627
  - **Requester** comes from the kernel's process tag: `{ kind: 'agent', agentId }` is the cone or scoop by its name, `{ kind: 'user', where }` is the kernel user with `where` (`terminal:…` reads "a terminal", `follower:<device>` the `where` the page passes, such as "on Pat's phone", or "a linked device", with the device id kept as `requester.device`), and anything else is the kernel user alone.
624
628
  - **Detail:** the command as the kernel resolved it (`path` and the arguments, shell-quoted), `target`, `cwd`, and `alwaysScope: 'cwd'` when the request offers Always (`req.always === true`, which the page sets from its own checks), since the kernel's Always writes a rule for that directory.
625
629
  - **Answers:** `approved` resolves its answer and anything else `deny`. Every device of the owner answers with the same powers: a follower, with or without Control, sees the same item, Always included when the request offers it, and its `always` counts as the owner tab's would. `device` on the item names the device that answered, from `owner.port({ device })`.
626
- - **Ends:** when the kernel aborts `signal` (`withdrawn`) the item is withdrawn, and on `expired` the page keeps its link until the record's own timeout settles it (`unanswered`), so it never reads as withdrawn. When the page's link closes the worker withdraws its open sudo items (`disconnected`), a restart or release of the owner denies the ones still waiting, and a reload withdraws the rest (`restart`). The kernel already allows one pending request per user.
630
+ - **Ends:** when the kernel aborts `signal` (`withdrawn`) the item is withdrawn, and on `expired` the page keeps its link until the record's own timeout settles it (`unanswered`), so it never reads as withdrawn. When the page's link closes the worker withdraws its open sudo items (`disconnected`). A restart or release of the owner first ends the old worker, then denies the requests still waiting, so the old worker never sees their links close, and the next worker withdraws their items (`restart`). The kernel already allows one pending request per user.
627
631
  - **Scoops:** a scoop's request sends its cone a `sudo-request` lick without actions once a person has to answer it (at once on the `user` tier, after the judge hands it on with the `agent` policy, never when the judge decides), so the cone knows the scoop waits and can't approve it itself. A cone that asks gets none: it waits in its own command.
628
632
  - **With the host's own approvals:** bios has its own `approvals` port for sudo, and a page that spreads `{ ...bios, ...createAgentModel(connection) }` would lose it. Pass that port in instead, as `createAgentModel(connection, { approvals: biosPort })`. The returned port then lists both sets, oldest first, sends `decide` to whichever port listed the id, and passes on both ports' `approvals` and `ended` events. It has `decide` when either side can answer; asked for an item its owner can't answer, it rejects with "This request can't be answered in this tab." Without the option, it lists only the agent's items.
629
633
 
@@ -675,7 +679,7 @@ A guest seat (plan D13, PR 19) is a revocable seat on one cone: the guest sees t
675
679
 
676
680
  - **Minting.** The tray hub mints the seat and its one-time URL (v6's routes, D10); the page hands the agent adapter its routes: `createAgentModel(connection, { guests: { mint({ label, expiresAt, gates, agentId }) → { id, url }, revoke(id) } })`. Its `guests` port (spectrum's `GuestPort`) mints through the hub, then opens the seat in the worker with `AgentControl.guestOpen({ id, label, agentId, expiresAt, gates })`; if the worker refuses, the hub's seat is revoked again. `list()` is the transcript's `guests` state (`{ id, label, agentId, createdAt, expiresAt, gates, connected, pending, alwaysTools }`), and the URL is returned once and never kept. `unavailable()` says "Inviting guests needs the tray hub." without a hub, and "Only the owner's devices can invite guests." on a guest. Every owner device can invite, with or without Control. `gates()` (spectrum's `GuestPort.gates?()`) lists the gates the approval policy allows for messages and tools, in display order, for the invite pickers; `scoop:*`, for "any scoop", shows only when the policy offers the scoop tier.
677
681
  - **The record.** Seats live in the session document `slicc.guests`, so they survive reloads and restarts: id, label (control characters stripped, at most 64 characters), cone, expiry (at most the policy's `guests.maxSeatDays`, 30 by default), the two gates (`user`, `agent`, `cone`, `scoop:<name>` or `off`, fixed when the seat is opened and checked then against the policy's `allowedMessageGates` and `allowedToolGates`; by default the tools gate can't be `off` or `cone`, from any caller: "A guest's tool calls need a review: by a person, the judge or a scoop.") and an epoch. Revoking (`guestRevoke(id)`) keeps a tombstone, bumps the epoch, withdraws the guest's messages still queued in the cone, and ends every guest link of the seat (`invalidatePrincipal`). An expired seat ends the same way at its expiry. Tombstones go 365 days after they end, the longest a seat can be opened for.
678
- - **Joining.** The hub resolves the guest's token to the seat's id and label, and the owner page opens `owner.port({ guest: { id, label } })` and bridges it like a follower's. Before Chord sees the link, the worker checks that the seat exists, isn't revoked or expired, and has that label; otherwise it closes the port. The link's principal is `{ seat: 'guest', link, guest: { id, name, epoch } }`, and anything else the port asks for (`readOnly: false`, `pages`) is ignored.
682
+ - **Joining.** The hub resolves the guest's token to the seat's id and label, and the owner page opens `owner.port({ guest: { id, label } })` and bridges it like a follower's. Before Chord sees the link, the worker checks that the seat exists, isn't revoked or expired, and has that label; otherwise it closes the port. The link's principal is `{ seat: 'guest', link, guest: { id, name, epoch } }`, and anything else the port asks for (`readOnly: false`, `pages`) is ignored. A guest link has at most four calls in flight (Chord's `maxCalls`): further ones are answered at once with `unavailable` ("Too many calls in progress on this connection (4)"), and the link stays open. A revoked guest leaves nothing behind on the host: Chord 1.3 drops each disposed object, where 1.2 kept one per guest principal (seat and epoch), with its link, until the worker restarted.
679
683
  - **What a guest gets.** Every contract served from the agent's host refuses a guest principal (`forbidden`) unless its factory is opened with `openToGuests`, so a contract added later is closed to guests until someone opens it. The only open one is `slicc.agent.guest`: its `guest` variant has the shared cone's transcript (its user, assistant, tool and compaction entries; `pi.live`; `pi.agent`'s model and thinking level; and only the guest's own queued messages) and the seat, plus `propose({ text, requestId })`. Everyone else gets its empty `none` variant.
680
684
  - **The guest's page** calls `connectGuest(endpoint)` from `@ai-ecoverse/slicc-agent/page`, which resolves an `AgentConnection` for that one cone with `guest` (the seat as the guest sees it); `connectAgent` on a guest port is forbidden. `createAgentModel(guest).agent` shows the transcript and sends through `propose`; every other control method, settings action, `prompt` and `webhook` rejects with `EACCES` ("a guest can only watch and send messages"). A port that isn't a valid seat rejects with `EACCES` "This guest link isn't valid any more."
681
685
  - **Messages.** With the messages gate `off`, a guest's message is submitted to the cone as a follow-up placed as a `slicc.guest-input` entry (`{ seat, name, approval: null }`), and the model reads it after the line "[A guest message from "<label>", shared through a guest seat, NOT from the cone's owner. …]". The spectrum adapter shows it as a user message with `origin: 'guest'` and `from` the label, without that line, queued or placed. Under any other gate it waits as a pending `guest-message` approval (tier = the gate, requester from the seat), which times out like other approvals. The guest contract's `proposals` state lists the seat's own, and the guest's adapter shows a pending one as its queued message and an ended one as a notice: "Nobody approved this in time.", "It was declined." or "It was withdrawn." Once approved it is delivered exactly once (request id `guest:<seat>:<approval>`, and the seat record keeps the ids it delivered) as the same entry with `approval` set; a seat revoked or expired by then gets nothing. #253 makes the delivery durable across a crash between approval and submit, and adds the review UX.
@@ -713,7 +717,7 @@ The agent never holds a secret's value. Values live in the vault (`vault.sliccy.
713
717
 
714
718
  Fixes to pi stay in this repository. [`patches/patches.json`](patches/patches.json) lists each one with its `kind`, `package`, `patchedVersion`, `reason`, `removeWhen` and `verify` command, and an optional `marker` (a file in the package and a string it must contain):
715
719
 
716
- - `monkeypatch`: code that runs in users' browsers is patched at runtime. pi-mcp parses SSE with Node's `Buffer.byteLength`, which browsers don't have. `patchSse()` in [`src/mcp/sse.ts`](src/mcp/sse.ts) replaces `StreamableHttpTransport.prototype.consumeSse` with the same parser measuring bytes with `TextEncoder`, before any server connects. It checks that the code it replaces still looks as expected and throws, naming its manifest entry (named in [`src/patches.ts`](src/patches.ts)), if not. It's the only one: pi-server's handshake-timer patch went with pi-server in 5.0.
720
+ - `monkeypatch`: code that runs in users' browsers is patched at runtime. pi-mcp parses SSE with Node's `Buffer.byteLength`, which browsers don't have. `patchSse()` in [`src/mcp/sse.ts`](src/mcp/sse.ts) replaces `StreamableHttpTransport.prototype.consumeSse` with the same parser measuring bytes with `TextEncoder`, before any server connects. It checks that the code it replaces still looks as expected and throws, naming its manifest entry (named in [`src/patches.ts`](src/patches.ts)), if not. The AWS SDK has the other one: in the browser, Bedrock's request handler is `WebSocketFetchHandler`, which passes https requests to `FetchHttpHandler` without their abort signal, so Stop couldn't cancel a Bedrock request that hadn't answered yet (#350). [`src/bedrock.ts`](src/bedrock.ts) wraps the Bedrock provider and, before its first request, replaces `WebSocketFetchHandler.prototype.handle` with one that forwards the signal. The patch must reach the copy the client imports. The agent therefore depends on `@aws-sdk/middleware-websocket` with a caret range, as the client does, so npm and seven's pnpm install resolve one copy for both. If Bedrock answers without going through the patched handler, the agent warns once in the worker console, naming the entry. pi-server's handshake-timer patch went with pi-server in 5.0.
717
721
  - `patch-package`: repository-only fixes go in `patches/<package>+<version>.patch`, applied by a `postinstall` that runs [patch-package](https://github.com/ds300/patch-package). There are none yet.
718
722
  - `environment`: something the host must provide.
719
723
  - pi-codemode runs QuickJS in a `node:worker_threads` Worker. slicc-bios must serve a `node:worker_threads` stub that wraps a module Web Worker; inside the worker, the stub gives `parentPort` and `workerData` from the first message. `test/integration/shims/node-worker-threads.js` is that stub, and the integration bundles alias to it.
@@ -723,9 +727,9 @@ Fixes to pi stay in this repository. [`patches/patches.json`](patches/patches.js
723
727
  - The prune takes the install from 99 to 58 packages and `node_modules` from 143 to 92 MB.
724
728
  - The markers keep pi-ai's two Google APIs (Gemini and Vertex) lazy. Seven offers only the Bedrock and Adobe providers: if a Google provider is ever added, the bios override has to go first, and a failing marker is the reminder.
725
729
 
726
- `npm run lint:patches` (part of `npm run lint`) fails when an installed version differs from `patchedVersion`, a marker is gone, a monkeypatch isn't named in `src/patches.ts`, a patch file and its entry don't match, or `renovate.json` doesn't route the package to the `patched dependencies` group with automerge off. That group covers every `@earendil-works/*` package, so a pi bump is always a reviewed PR: rerun each entry's `verify`, then move `patchedVersion` forward or drop the entry.
730
+ `npm run lint:patches` (part of `npm run lint`) fails when an installed version differs from `patchedVersion`, a marker is gone, a monkeypatch isn't named in `src/patches.ts`, a patch file and its entry don't match, or `renovate.json` doesn't route the package to the `patched dependencies` group with automerge off. That group covers every `@earendil-works/*` package and `@aws-sdk/middleware-websocket`, so a pi bump is always a reviewed PR: rerun each entry's `verify`, then move `patchedVersion` forward or drop the entry.
727
731
 
728
- **Two Chords.** pi-durable 1.3 pins `@earendil-works/chord` 1.3.0, while the link and serving code stays on 1.2.0, because Chord 1.3's link protocol refuses 1.2 peers (moving is the agent 6.0 port). npm and pnpm nest Chord 1.3 under pi-durable, which uses only its state, context, delta, JSON and output layer, whose interfaces match 1.2's, so the two copies interoperate. `npm run lint:chord` (part of `npm run lint`) fails if any package other than pi-durable gets its own Chord, or if pi-durable imports anything outside that layer, such as `createHost` or a link. Renovate holds `@earendil-works/chord` below 1.3. The second copy is 450 KB of unminified JavaScript in 23 modules, all of which the unbundled worker loads because pi-durable imports Chord's root index; bundled and minified, the agent worker grows by 22 KB.
732
+ **One Chord.** `@earendil-works/chord` is pinned to the version pi-durable pins (1.3.0), so the link, the serving code and pi-durable's state layer share one copy. `npm run lint:chord` (part of `npm run lint`) fails if the lockfile has a second Chord that the agent would load, or none. The `slicc-agent-5` devDependency brings its own Chord 1.2 for the version tests only, which the check skips because it's dev-only.
729
733
 
730
734
  ## Develop
731
735
 
@@ -1,7 +1,7 @@
1
- import { createHost } from "@earendil-works/chord";
2
1
  import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
2
+ import { clientHost, holding } from "../client.js";
3
3
  import { AgentSudoService, SUDO_METHODS } from "../services.js";
4
- import { AGENT_HOST, bound } from "../serving.js";
4
+ import { bound } from "../serving.js";
5
5
  import { PortLink } from "../wire.js";
6
6
  const UNAVAILABLE = "The agent can't take this sudo request.";
7
7
  const unavailable = (cause) => Object.assign(new Error(UNAVAILABLE), { code: "EAGAIN", ...cause ? { cause } : {} });
@@ -18,10 +18,10 @@ function halted(stop) {
18
18
  }
19
19
  async function open(endpoint, stop) {
20
20
  const link = new PortLink(endpoint);
21
- const host = createHost({ name: `sudo:${crypto.randomUUID()}`, kind: "client" });
21
+ let host;
22
22
  const close = async () => {
23
23
  link.close("closed");
24
- await host.close().catch(() => void 0);
24
+ await host?.close().catch(() => void 0);
25
25
  };
26
26
  const ended = link.ended.then((reason) => {
27
27
  throw reason ?? new Error("the agent closed the link");
@@ -31,11 +31,11 @@ async function open(endpoint, stop) {
31
31
  stopped.catch(() => void 0);
32
32
  try {
33
33
  await Promise.race([link.hello, ended, stopped]);
34
- host.connect(link);
35
- const handle = host.get(AgentSudoService, {}, { at: AGENT_HOST });
36
- await Promise.race([handle.ready, ended, stopped]);
37
- if (handle.kind !== "sudo") throw unavailable();
38
- const root = (run, options) => host.root(run, options);
34
+ const client = clientHost(link, "sudo");
35
+ host = client;
36
+ const [handle] = await Promise.race([holding(client, link, [AgentSudoService]), stopped]);
37
+ if (handle.variant !== "sudo") throw unavailable();
38
+ const root = (run, options) => client.root(run, options);
39
39
  const sudo = bound(handle, root, SUDO_METHODS, missing);
40
40
  return { sudo, approvals: handle.approvals, ended, close };
41
41
  } catch (error) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/approvals/sudo-client.ts"],
4
- "sourcesContent": ["import { createHost, type Readable } from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport { AgentSudoService, SUDO_METHODS } from '../services.ts';\nimport { AGENT_HOST, bound, type Remote, type Root } from '../serving.ts';\nimport { type PortEndpoint, PortLink } from '../wire.ts';\nimport type { Approval } from './record.ts';\nimport type { AgentSudo, SudoAnswer, SudoRequest } from './sudo.ts';\n\nexport interface ApproveOptions {\n signal?: AbortSignal;\n where?: string;\n}\n\nexport const UNAVAILABLE = \"The agent can't take this sudo request.\";\n\nexport const unavailable = (cause?: unknown): Error =>\n Object.assign(new Error(UNAVAILABLE), { code: 'EAGAIN', ...(cause ? { cause } : {}) });\n\nconst missing = () => () => Promise.reject(unavailable());\n\nexport function answerOf(item: Approval | undefined): SudoAnswer | null {\n if (!item || item.state === 'pending') return null;\n return item.state === 'approved' && item.answer ? item.answer : 'deny';\n}\n\nfunction halted(stop: AbortSignal | undefined): Promise<never> {\n return new Promise((_, reject) => {\n if (stop?.aborted) reject(unavailable());\n stop?.addEventListener('abort', () => reject(unavailable()), { once: true });\n });\n}\n\nasync function open(endpoint: PortEndpoint, stop: AbortSignal | undefined) {\n const link = new PortLink(endpoint);\n const host = createHost({ name: `sudo:${crypto.randomUUID()}`, kind: 'client' });\n const close = async () => {\n link.close('closed');\n await host.close().catch(() => undefined);\n };\n const ended = link.ended.then((reason) => {\n throw reason ?? new Error('the agent closed the link');\n });\n ended.catch(() => undefined);\n const stopped = halted(stop);\n stopped.catch(() => undefined);\n try {\n await Promise.race([link.hello, ended, stopped]);\n host.connect(link);\n const handle = host.get(AgentSudoService, {}, { at: AGENT_HOST }) as unknown as Remote;\n await Promise.race([handle.ready, ended, stopped]);\n if (handle.kind !== 'sudo') throw unavailable();\n const root: Root = (run, options) => host.root(run, options);\n const sudo = bound<Omit<AgentSudo, 'approvals'>>(handle, root, SUDO_METHODS, missing);\n return { sudo, approvals: handle.approvals as Readable<Approval[] | undefined>, ended, close };\n } catch (error) {\n await close();\n throw (error as { code?: unknown })?.code === 'EAGAIN' ? error : unavailable(error);\n }\n}\n\nfunction settled(\n approvals: Readable<Approval[] | undefined>,\n id: string,\n signals: { signal: AbortSignal | undefined; stop: AbortSignal | undefined },\n ended: Promise<never>\n): Promise<{ answer: SudoAnswer; aborted: unknown }> {\n const { signal, stop } = signals;\n return new Promise((resolve) => {\n let off: () => void = () => undefined;\n let done = false;\n const finish = (answer: SudoAnswer, aborted: unknown = null) => {\n if (done) return;\n done = true;\n off();\n signal?.removeEventListener('abort', abort);\n stop?.removeEventListener('abort', halt);\n resolve({ answer, aborted });\n };\n const abort = () => finish('deny', signal?.reason ?? 'withdrawn');\n const halt = () => finish('deny');\n if (signal?.aborted) return abort();\n if (stop?.aborted) return halt();\n signal?.addEventListener('abort', abort, { once: true });\n stop?.addEventListener('abort', halt, { once: true });\n ended.catch(() => finish('deny'));\n off = approvals.subscribe((items) => {\n const answer = answerOf(items?.find((item) => item.id === id));\n if (answer) finish(answer);\n });\n if (done) off();\n });\n}\n\nexport const WITHDRAW_MS = 5000;\n\nconst delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\n\nfunction timedOut(\n link: { approvals: Readable<Approval[] | undefined>; ended: Promise<never> },\n id: string,\n expiresAt: number\n): Promise<unknown> {\n let off: () => void = () => undefined;\n const left = new Promise<void>((resolve) => {\n off = link.approvals.subscribe((items) => {\n if (answerOf(items?.find((item) => item.id === id))) resolve();\n });\n });\n const wait = Math.max(0, expiresAt - Date.now()) + WITHDRAW_MS;\n return Promise.race([left, link.ended.catch(() => undefined), delay(wait)]).finally(() => off());\n}\n\nexport async function askSudo(\n endpoint: PortEndpoint,\n request: SudoRequest,\n options: ApproveOptions = {},\n stop?: AbortSignal\n): Promise<SudoAnswer> {\n const { signal } = options;\n const link = await open(endpoint, stop);\n try {\n if (signal?.aborted || stop?.aborted) return 'deny';\n const asked = await link.sudo.request(request, options.where ?? null, BACKGROUND_CONTEXT);\n if (asked.id === null) throw new Error(asked.error);\n const signals = { signal, stop };\n const { answer, aborted } = await settled(link.approvals, asked.id, signals, link.ended);\n if (aborted === 'expired') await timedOut(link, asked.id, request.expiresAt);\n else if (aborted)\n await Promise.race([\n link.sudo.withdraw(asked.id, String(aborted), BACKGROUND_CONTEXT),\n delay(WITHDRAW_MS),\n ]);\n return answer;\n } finally {\n await link.close();\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { Readable } from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport { clientHost, holding } from '../client.ts';\nimport { AgentSudoService, SUDO_METHODS } from '../services.ts';\nimport { bound, type Remote, type Root } from '../serving.ts';\nimport { type PortEndpoint, PortLink } from '../wire.ts';\nimport type { Approval } from './record.ts';\nimport type { AgentSudo, SudoAnswer, SudoRequest } from './sudo.ts';\n\nexport interface ApproveOptions {\n signal?: AbortSignal;\n where?: string;\n}\n\nexport const UNAVAILABLE = \"The agent can't take this sudo request.\";\n\nexport const unavailable = (cause?: unknown): Error =>\n Object.assign(new Error(UNAVAILABLE), { code: 'EAGAIN', ...(cause ? { cause } : {}) });\n\nconst missing = () => () => Promise.reject(unavailable());\n\nexport function answerOf(item: Approval | undefined): SudoAnswer | null {\n if (!item || item.state === 'pending') return null;\n return item.state === 'approved' && item.answer ? item.answer : 'deny';\n}\n\nfunction halted(stop: AbortSignal | undefined): Promise<never> {\n return new Promise((_, reject) => {\n if (stop?.aborted) reject(unavailable());\n stop?.addEventListener('abort', () => reject(unavailable()), { once: true });\n });\n}\n\nasync function open(endpoint: PortEndpoint, stop: AbortSignal | undefined) {\n const link = new PortLink(endpoint);\n let host: ReturnType<typeof clientHost> | undefined;\n const close = async () => {\n link.close('closed');\n await host?.close().catch(() => undefined);\n };\n const ended = link.ended.then((reason) => {\n throw reason ?? new Error('the agent closed the link');\n });\n ended.catch(() => undefined);\n const stopped = halted(stop);\n stopped.catch(() => undefined);\n try {\n await Promise.race([link.hello, ended, stopped]);\n const client = clientHost(link, 'sudo');\n host = client;\n const [handle] = (await Promise.race([holding(client, link, [AgentSudoService]), stopped])) as [\n Remote,\n ];\n if (handle.variant !== 'sudo') throw unavailable();\n const root: Root = (run, options) => client.root(run, options);\n const sudo = bound<Omit<AgentSudo, 'approvals'>>(handle, root, SUDO_METHODS, missing);\n return { sudo, approvals: handle.approvals as Readable<Approval[]>, ended, close };\n } catch (error) {\n await close();\n throw (error as { code?: unknown })?.code === 'EAGAIN' ? error : unavailable(error);\n }\n}\n\nfunction settled(\n approvals: Readable<Approval[]>,\n id: string,\n signals: { signal: AbortSignal | undefined; stop: AbortSignal | undefined },\n ended: Promise<never>\n): Promise<{ answer: SudoAnswer; aborted: unknown }> {\n const { signal, stop } = signals;\n return new Promise((resolve) => {\n let off: () => void = () => undefined;\n let done = false;\n const finish = (answer: SudoAnswer, aborted: unknown = null) => {\n if (done) return;\n done = true;\n off();\n signal?.removeEventListener('abort', abort);\n stop?.removeEventListener('abort', halt);\n resolve({ answer, aborted });\n };\n const abort = () => finish('deny', signal?.reason ?? 'withdrawn');\n const halt = () => finish('deny');\n if (signal?.aborted) return abort();\n if (stop?.aborted) return halt();\n signal?.addEventListener('abort', abort, { once: true });\n stop?.addEventListener('abort', halt, { once: true });\n ended.catch(() => finish('deny'));\n off = approvals.subscribe((items) => {\n const answer = answerOf(items?.find((item) => item.id === id));\n if (answer) finish(answer);\n });\n if (done) off();\n });\n}\n\nexport const WITHDRAW_MS = 5000;\n\nconst delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\n\nfunction timedOut(\n link: { approvals: Readable<Approval[]>; ended: Promise<never> },\n id: string,\n expiresAt: number\n): Promise<unknown> {\n let off: () => void = () => undefined;\n const left = new Promise<void>((resolve) => {\n off = link.approvals.subscribe((items) => {\n if (answerOf(items?.find((item) => item.id === id))) resolve();\n });\n });\n const wait = Math.max(0, expiresAt - Date.now()) + WITHDRAW_MS;\n return Promise.race([left, link.ended.catch(() => undefined), delay(wait)]).finally(() => off());\n}\n\nexport async function askSudo(\n endpoint: PortEndpoint,\n request: SudoRequest,\n options: ApproveOptions = {},\n stop?: AbortSignal\n): Promise<SudoAnswer> {\n const { signal } = options;\n const link = await open(endpoint, stop);\n try {\n if (signal?.aborted || stop?.aborted) return 'deny';\n const asked = await link.sudo.request(request, options.where ?? null, BACKGROUND_CONTEXT);\n if (asked.id === null) throw new Error(asked.error);\n const signals = { signal, stop };\n const { answer, aborted } = await settled(link.approvals, asked.id, signals, link.ended);\n if (aborted === 'expired') await timedOut(link, asked.id, request.expiresAt);\n else if (aborted)\n await Promise.race([\n link.sudo.withdraw(asked.id, String(aborted), BACKGROUND_CONTEXT),\n delay(WITHDRAW_MS),\n ]);\n return answer;\n } finally {\n await link.close();\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,72 @@
1
+ import { lazyStream } from "@earendil-works/pi-ai/api/lazy";
2
+ import { BEDROCK_ABORT_PATCH } from "./patches.js";
3
+ import { BEDROCK } from "./preferred.js";
4
+ const patched = /* @__PURE__ */ Symbol.for("slicc.agent.bedrock-abort");
5
+ const DROPS = /this\.httpHandler\.handle\(\w+\)/;
6
+ const ANSWERED = /* @__PURE__ */ new Set(["stop", "length", "toolUse"]);
7
+ const UNPATCHED_COPY = `${BEDROCK_ABORT_PATCH}: Bedrock answered through a WebSocketFetchHandler the agent didn't patch, so Stop can't cancel its requests; the agent and @aws-sdk/client-bedrock-runtime load different copies of @aws-sdk/middleware-websocket`;
8
+ function patchWebSocketHandler(target) {
9
+ const proto = target.prototype;
10
+ const done = proto[patched];
11
+ if (done) return done;
12
+ const original = proto.handle;
13
+ if (typeof original !== "function" || !DROPS.test(String(original))) {
14
+ throw new Error(
15
+ `${BEDROCK_ABORT_PATCH}: WebSocketFetchHandler.prototype.handle no longer calls this.httpHandler.handle(request); revisit patches.json`
16
+ );
17
+ }
18
+ const probe = { sent: 0 };
19
+ proto.handle = async function handle(request, options) {
20
+ if (request.protocol === "ws:" || request.protocol === "wss:")
21
+ return original.call(this, request, options);
22
+ probe.sent += 1;
23
+ this.config = await this.configPromise;
24
+ return this.httpHandler.handle(request, options);
25
+ };
26
+ proto[patched] = probe;
27
+ return probe;
28
+ }
29
+ let loading;
30
+ function patchBedrockAbort(load = () => import("@aws-sdk/middleware-websocket")) {
31
+ loading ??= load().then(
32
+ ({ WebSocketFetchHandler }) => patchWebSocketHandler(WebSocketFetchHandler),
33
+ (error) => {
34
+ loading = void 0;
35
+ throw error;
36
+ }
37
+ );
38
+ return loading;
39
+ }
40
+ function abortableBedrock(provider, patch = patchBedrockAbort, warn = (message) => console.warn(message)) {
41
+ if (provider.id !== BEDROCK) return provider;
42
+ let warned = false;
43
+ const checked = (stream, probe) => ({
44
+ [Symbol.asyncIterator]: () => stream[Symbol.asyncIterator](),
45
+ async result() {
46
+ const message = await stream.result();
47
+ if (!warned && probe.sent === 0 && ANSWERED.has(message.stopReason)) {
48
+ warned = true;
49
+ warn(UNPATCHED_COPY);
50
+ }
51
+ return message;
52
+ }
53
+ });
54
+ const streams = {
55
+ stream: (model, context, options) => lazyStream(model, async () => {
56
+ const probe = await patch();
57
+ return checked(provider.stream(model, context, options), probe);
58
+ }),
59
+ streamSimple: (model, context, options) => lazyStream(model, async () => {
60
+ const probe = await patch();
61
+ return checked(provider.streamSimple(model, context, options), probe);
62
+ })
63
+ };
64
+ return { ...provider, ...streams };
65
+ }
66
+ export {
67
+ UNPATCHED_COPY,
68
+ abortableBedrock,
69
+ patchBedrockAbort,
70
+ patchWebSocketHandler
71
+ };
72
+ //# sourceMappingURL=bedrock.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/bedrock.ts"],
4
+ "sourcesContent": ["import type {\n AssistantMessage,\n AssistantMessageEvent,\n AssistantMessageEventStream,\n Provider,\n ProviderStreams,\n} from '@earendil-works/pi-ai';\nimport { lazyStream } from '@earendil-works/pi-ai/api/lazy';\nimport { BEDROCK_ABORT_PATCH } from './patches.ts';\nimport { BEDROCK } from './preferred.ts';\n\nconst patched = Symbol.for('slicc.agent.bedrock-abort');\nconst DROPS = /this\\.httpHandler\\.handle\\(\\w+\\)/;\nconst ANSWERED = new Set(['stop', 'length', 'toolUse']);\n\nexport const UNPATCHED_COPY = `${BEDROCK_ABORT_PATCH}: Bedrock answered through a WebSocketFetchHandler the agent didn't patch, so Stop can't cancel its requests; the agent and @aws-sdk/client-bedrock-runtime load different copies of @aws-sdk/middleware-websocket`;\n\nexport interface Probe {\n sent: number;\n}\n\ninterface HttpRequest {\n protocol?: string;\n}\n\ninterface Handle {\n handle(request: HttpRequest, options?: unknown): Promise<unknown>;\n}\n\ninterface WebSocketHandler extends Handle {\n config: unknown;\n configPromise: Promise<unknown>;\n httpHandler: Handle;\n}\n\nexport function patchWebSocketHandler(target: { prototype: object }): Probe {\n const proto = target.prototype as WebSocketHandler & { [patched]?: Probe };\n const done = proto[patched];\n if (done) return done;\n const original = proto.handle;\n if (typeof original !== 'function' || !DROPS.test(String(original))) {\n throw new Error(\n `${BEDROCK_ABORT_PATCH}: WebSocketFetchHandler.prototype.handle no longer calls this.httpHandler.handle(request); revisit patches.json`\n );\n }\n const probe: Probe = { sent: 0 };\n proto.handle = async function handle(request, options) {\n if (request.protocol === 'ws:' || request.protocol === 'wss:')\n return original.call(this, request, options);\n probe.sent += 1;\n this.config = await this.configPromise;\n return this.httpHandler.handle(request, options);\n };\n proto[patched] = probe;\n return probe;\n}\n\nlet loading: Promise<Probe> | undefined;\n\nexport function patchBedrockAbort(\n load: () => Promise<{ WebSocketFetchHandler: { prototype: object } }> = () =>\n import('@aws-sdk/middleware-websocket')\n): Promise<Probe> {\n loading ??= load().then(\n ({ WebSocketFetchHandler }) => patchWebSocketHandler(WebSocketFetchHandler),\n (error: unknown) => {\n loading = undefined;\n throw error;\n }\n );\n return loading;\n}\n\nexport function abortableBedrock(\n provider: Provider,\n patch: () => Promise<Probe> = patchBedrockAbort,\n warn: (message: string) => void = (message) => console.warn(message)\n): Provider {\n if (provider.id !== BEDROCK) return provider;\n let warned = false;\n const checked = (stream: AssistantMessageEventStream, probe: Probe) => ({\n [Symbol.asyncIterator]: (): AsyncIterator<AssistantMessageEvent> =>\n stream[Symbol.asyncIterator](),\n async result(): Promise<AssistantMessage> {\n const message = await stream.result();\n if (!warned && probe.sent === 0 && ANSWERED.has(message.stopReason)) {\n warned = true;\n warn(UNPATCHED_COPY);\n }\n return message;\n },\n });\n const streams: Pick<ProviderStreams, 'stream' | 'streamSimple'> = {\n stream: (model, context, options) =>\n lazyStream(model, async () => {\n const probe = await patch();\n return checked(provider.stream(model, context, options), probe);\n }),\n streamSimple: (model, context, options) =>\n lazyStream(model, async () => {\n const probe = await patch();\n return checked(provider.streamSimple(model, context, options), probe);\n }),\n };\n return { ...provider, ...streams };\n}\n"],
5
+ "mappings": "AAOA,SAAS,kBAAkB;AAC3B,SAAS,2BAA2B;AACpC,SAAS,eAAe;AAExB,MAAM,UAAU,uBAAO,IAAI,2BAA2B;AACtD,MAAM,QAAQ;AACd,MAAM,WAAW,oBAAI,IAAI,CAAC,QAAQ,UAAU,SAAS,CAAC;AAE/C,MAAM,iBAAiB,GAAG,mBAAmB;AAoB7C,SAAS,sBAAsB,QAAsC;AAC1E,QAAM,QAAQ,OAAO;AACrB,QAAM,OAAO,MAAM,OAAO;AAC1B,MAAI,KAAM,QAAO;AACjB,QAAM,WAAW,MAAM;AACvB,MAAI,OAAO,aAAa,cAAc,CAAC,MAAM,KAAK,OAAO,QAAQ,CAAC,GAAG;AACnE,UAAM,IAAI;AAAA,MACR,GAAG,mBAAmB;AAAA,IACxB;AAAA,EACF;AACA,QAAM,QAAe,EAAE,MAAM,EAAE;AAC/B,QAAM,SAAS,eAAe,OAAO,SAAS,SAAS;AACrD,QAAI,QAAQ,aAAa,SAAS,QAAQ,aAAa;AACrD,aAAO,SAAS,KAAK,MAAM,SAAS,OAAO;AAC7C,UAAM,QAAQ;AACd,SAAK,SAAS,MAAM,KAAK;AACzB,WAAO,KAAK,YAAY,OAAO,SAAS,OAAO;AAAA,EACjD;AACA,QAAM,OAAO,IAAI;AACjB,SAAO;AACT;AAEA,IAAI;AAEG,SAAS,kBACd,OAAwE,MACtE,OAAO,+BAA+B,GACxB;AAChB,cAAY,KAAK,EAAE;AAAA,IACjB,CAAC,EAAE,sBAAsB,MAAM,sBAAsB,qBAAqB;AAAA,IAC1E,CAAC,UAAmB;AAClB,gBAAU;AACV,YAAM;AAAA,IACR;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,iBACd,UACA,QAA8B,mBAC9B,OAAkC,CAAC,YAAY,QAAQ,KAAK,OAAO,GACzD;AACV,MAAI,SAAS,OAAO,QAAS,QAAO;AACpC,MAAI,SAAS;AACb,QAAM,UAAU,CAAC,QAAqC,WAAkB;AAAA,IACtE,CAAC,OAAO,aAAa,GAAG,MACtB,OAAO,OAAO,aAAa,EAAE;AAAA,IAC/B,MAAM,SAAoC;AACxC,YAAM,UAAU,MAAM,OAAO,OAAO;AACpC,UAAI,CAAC,UAAU,MAAM,SAAS,KAAK,SAAS,IAAI,QAAQ,UAAU,GAAG;AACnE,iBAAS;AACT,aAAK,cAAc;AAAA,MACrB;AACA,aAAO;AAAA,IACT;AAAA,EACF;AACA,QAAM,UAA4D;AAAA,IAChE,QAAQ,CAAC,OAAO,SAAS,YACvB,WAAW,OAAO,YAAY;AAC5B,YAAM,QAAQ,MAAM,MAAM;AAC1B,aAAO,QAAQ,SAAS,OAAO,OAAO,SAAS,OAAO,GAAG,KAAK;AAAA,IAChE,CAAC;AAAA,IACH,cAAc,CAAC,OAAO,SAAS,YAC7B,WAAW,OAAO,YAAY;AAC5B,YAAM,QAAQ,MAAM,MAAM;AAC1B,aAAO,QAAQ,SAAS,aAAa,OAAO,SAAS,OAAO,GAAG,KAAK;AAAA,IACtE,CAAC;AAAA,EACL;AACA,SAAO,EAAE,GAAG,UAAU,GAAG,QAAQ;AACnC;",
6
+ "names": []
7
+ }
package/dist/client.js CHANGED
@@ -16,8 +16,8 @@ import {
16
16
  SETTINGS_METHODS,
17
17
  TRANSCRIPT_STATES
18
18
  } from "./services.js";
19
- import { AGENT_HOST, bound, kept, replicated } from "./serving.js";
20
- import { PortLink } from "./wire.js";
19
+ import { bound, replicated, SELF } from "./serving.js";
20
+ import { otherVersion, PortLink } from "./wire.js";
21
21
  function lasting(state) {
22
22
  let last;
23
23
  let frozen = false;
@@ -52,44 +52,62 @@ function lastingSettings(settings, hold) {
52
52
  };
53
53
  }
54
54
  const VERSIONS = /* @__PURE__ */ new Set(["upgrade_required", "contract_mismatch"]);
55
- const OTHER_VERSION = "That SLICC runs another version of the agent, so this tab can't follow it. Update both SLICCs, then reload this page.";
56
- const final = (error) => isChordError(error) && VERSIONS.has(error.code) ? Object.assign(new Error(OTHER_VERSION, { cause: error }), { code: "EVERSION" }) : error;
55
+ const final = (error) => isChordError(error) && VERSIONS.has(error.code) ? otherVersion(error) : error;
57
56
  const ended = (link) => link.ended.then((reason) => {
58
57
  throw reason ?? new Error("the agent closed the link");
59
58
  });
59
+ function clientHost(link, name) {
60
+ const host = createHost({
61
+ name: `${name}:${crypto.randomUUID()}`,
62
+ kind: "client",
63
+ self: SELF
64
+ });
65
+ host.connect(link, { principal: SELF, onProtocolMismatch: () => link.mismatched() });
66
+ return host;
67
+ }
68
+ async function holding(host, link, contracts) {
69
+ const stop = new AbortController();
70
+ const getting = contracts.map(
71
+ (contract) => host.get(contract, {}, { signal: stop.signal })
72
+ );
73
+ try {
74
+ return await Promise.race([Promise.all(getting), ended(link)]);
75
+ } catch (error) {
76
+ stop.abort();
77
+ for (const got of getting)
78
+ void got.then(
79
+ (ref) => ref.release(),
80
+ () => void 0
81
+ );
82
+ link.close("handshake failed");
83
+ void host.close().catch(() => void 0);
84
+ throw error;
85
+ }
86
+ }
60
87
  async function connectAgent(endpoint, options = {}) {
61
88
  const context = options.context ?? BACKGROUND_CONTEXT;
62
89
  const link = new PortLink(endpoint);
63
90
  const serverId = await link.hello;
64
- const host = createHost({ name: `follower:${crypto.randomUUID()}`, kind: "client" });
65
- host.connect(link);
66
- const get = (contract) => host.get(contract, {}, { at: AGENT_HOST });
67
- const handles = [
91
+ const host = clientHost(link, "follower");
92
+ const handles = await holding(host, link, [
68
93
  AgentControl,
69
94
  AgentTranscript,
70
95
  AgentSettings,
71
96
  AgentRemoteService,
72
97
  AgentPagesService
73
- ].map(get);
98
+ ]).catch((error) => {
99
+ throw final(error);
100
+ });
74
101
  const [controlHandle, transcriptHandle, settingsHandle, remoteHandle, pagesHandle] = handles;
75
102
  const release = () => {
76
103
  for (const handle of handles) handle.release();
77
104
  };
78
- try {
79
- await Promise.race([Promise.all(handles.map((handle) => handle.ready)), ended(link)]);
80
- } catch (error) {
81
- release();
82
- link.close("handshake failed");
83
- throw final(error);
84
- }
85
105
  const root = (run, rootOptions) => host.root(run, rootOptions);
86
106
  const control = bound(controlHandle, root, CONTROL_METHODS, controlAnswer);
87
107
  const members = Object.fromEntries(
88
108
  TRANSCRIPT_STATES.map((name) => [name, transcriptHandle[name]])
89
109
  );
90
110
  const settingsState = settingsHandle.state;
91
- const keeping = [...Object.values(members), settingsState].map(kept);
92
- await Promise.race([Promise.all(keeping.map((keep) => keep.ready)), ended(link)]);
93
111
  const settings = {
94
112
  ...bound(settingsHandle, root, SETTINGS_METHODS, settingsAnswer),
95
113
  state: replicated(settingsState)
@@ -99,16 +117,15 @@ async function connectAgent(endpoint, options = {}) {
99
117
  const state = (name) => replicated(members[name]);
100
118
  const held = [];
101
119
  const hold = (state2) => {
102
- const kept2 = lasting(state2);
103
- held.push(kept2);
104
- return kept2.state;
120
+ const kept = lasting(state2);
121
+ held.push(kept);
122
+ return kept.state;
105
123
  };
106
124
  let open = true;
107
125
  const finish = () => {
108
126
  if (!open) return;
109
127
  open = false;
110
- for (const kept2 of held) kept2.freeze();
111
- for (const keep of keeping) keep.stop();
128
+ for (const kept of held) kept.freeze();
112
129
  release();
113
130
  };
114
131
  const closed = link.ended.then((reason) => {
@@ -117,7 +134,7 @@ async function connectAgent(endpoint, options = {}) {
117
134
  });
118
135
  return {
119
136
  serverId,
120
- readOnly: controlHandle.kind === "view",
137
+ readOnly: controlHandle.variant === "view",
121
138
  control,
122
139
  transcript: hold(state("state")),
123
140
  deliveries: hold(state("deliveries")),
@@ -134,8 +151,8 @@ async function connectAgent(endpoint, options = {}) {
134
151
  vault: hold(state("vault")),
135
152
  labels: hold(state("labels")),
136
153
  guests: hold(state("guests")),
137
- viewOnly: controlHandle.kind === "view",
138
- owner: pagesHandle.kind === "pages",
154
+ viewOnly: controlHandle.variant === "view",
155
+ owner: pagesHandle.variant === "pages",
139
156
  approvals: hold(state("approvals")),
140
157
  approvalPolicy: hold(state("approvalPolicy")),
141
158
  settings: lastingSettings(settings, hold),
@@ -165,9 +182,11 @@ async function connectAgent(endpoint, options = {}) {
165
182
  };
166
183
  }
167
184
  export {
168
- OTHER_VERSION,
185
+ clientHost,
169
186
  connectAgent,
187
+ ended,
170
188
  final,
189
+ holding,
171
190
  lasting
172
191
  };
173
192
  //# sourceMappingURL=client.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/client.ts"],
4
- "sourcesContent": ["import {\n type Context,\n createHost,\n isChordError,\n type Readable,\n type ReplicatedState,\n} from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport type { ConversationView } from '@earendil-works/pi-durable';\nimport type { ApprovalPolicy } from './approvals/policy.ts';\nimport type { Approval } from './approvals/record.ts';\nimport type { ChangesView } from './changes/git.ts';\nimport type { FrozenCone } from './freezer/index.ts';\nimport type { GuestSeat } from './guests/seats.ts';\nimport type { MemoryEntry, MemoryScope } from './memory/format.ts';\nimport { controlAnswer, pageAnswer, remoteAnswer, settingsAnswer } from './readonly.ts';\nimport type { AgentRemote } from './remote/host.ts';\nimport type { AgentPages } from './remote/pages.ts';\nimport type { VaultState } from './secrets/vault.ts';\nimport {\n AgentControl,\n AgentPagesService,\n AgentRemoteService,\n AgentSettings,\n type AgentsSummary,\n AgentTranscript,\n CONTROL_METHODS,\n type Command,\n type Delivered,\n type GuestProposal,\n type GuestShown,\n type MonitorFeed,\n PAGE_METHODS,\n type PrivateRevision,\n REMOTE_METHODS,\n SETTINGS_METHODS,\n type SendMode,\n type SettingsState,\n TRANSCRIPT_STATES,\n} from './services.ts';\nimport { AGENT_HOST, bound, kept, type Remote, type Root, replicated } from './serving.ts';\nimport type { Sprinkle } from './sprinkles/kind.ts';\nimport { type PortEndpoint, PortLink } from './wire.ts';\n\nexport interface AgentConnection {\n readonly serverId: string;\n readonly control: AgentControl;\n readonly transcript: ReplicatedState<ConversationView>;\n readonly deliveries: ReplicatedState<Record<string, Delivered>>;\n readonly agents?: ReplicatedState<AgentsSummary>;\n readonly views?: ReplicatedState<Record<string, ConversationView>>;\n readonly commands?: ReplicatedState<Command[]>;\n readonly privateRevision?: ReplicatedState<PrivateRevision>;\n readonly sprinkles?: ReplicatedState<Sprinkle[]>;\n readonly memories?: ReplicatedState<MemoryEntry[]>;\n readonly memoryScopes?: ReplicatedState<MemoryScope[]>;\n readonly frozen?: ReplicatedState<FrozenCone[]>;\n readonly changes?: ReplicatedState<ChangesView>;\n readonly monitor?: ReplicatedState<MonitorFeed>;\n readonly vault?: ReplicatedState<VaultState>;\n readonly labels?: ReplicatedState<Record<string, string>>;\n readonly guests?: ReplicatedState<GuestSeat[]>;\n readonly viewOnly?: boolean;\n readonly guest?: GuestShown;\n readonly proposals?: ReplicatedState<GuestProposal[]>;\n readonly owner?: boolean;\n readonly approvals?: ReplicatedState<Approval[]>;\n readonly approvalPolicy?: ReplicatedState<ApprovalPolicy>;\n readonly settings: AgentSettings;\n readonly remote?: AgentRemote;\n readonly pages?: AgentPages;\n readonly closed: Promise<Error | undefined>;\n readonly readOnly?: boolean;\n prompt(text: string, whenBusy?: SendMode): Promise<string>;\n webhook(\n name: string,\n delivery: { id?: string | null; headers?: Record<string, string>; body: string }\n ): Promise<{ delivered: boolean }>;\n close(): Promise<void>;\n}\n\nexport interface Lasting<T> {\n readonly state: ReplicatedState<T>;\n freeze(): void;\n}\n\nexport function lasting<T>(state: ReplicatedState<T>): Lasting<T> {\n let last: T | undefined;\n let frozen = false;\n const read = () => {\n if (frozen) return last;\n try {\n last = state.value;\n } catch {}\n return last;\n };\n return {\n state: {\n get value() {\n return read();\n },\n subscribe: (listener) => (frozen ? () => undefined : state.subscribe(listener)),\n },\n freeze() {\n read();\n frozen = true;\n },\n };\n}\n\nfunction lastingSettings(\n settings: AgentSettings,\n hold: <T>(state: ReplicatedState<T>) => ReplicatedState<T>\n): AgentSettings {\n return {\n state: hold(settings.state),\n connect: (providerId, secret, fields, context) =>\n settings.connect(providerId, secret, fields, context),\n disconnect: (providerId, context) => settings.disconnect(providerId, context),\n signIn: (providerId, context) => settings.signIn(providerId, context),\n dismissMoved: (context) => settings.dismissMoved(context),\n };\n}\n\nconst VERSIONS = new Set(['upgrade_required', 'contract_mismatch']);\n\nexport const OTHER_VERSION =\n \"That SLICC runs another version of the agent, so this tab can't follow it. Update both SLICCs, then reload this page.\";\n\nexport const final = (error: unknown): unknown =>\n isChordError(error) && VERSIONS.has(error.code)\n ? Object.assign(new Error(OTHER_VERSION, { cause: error }), { code: 'EVERSION' })\n : error;\n\nconst ended = (link: PortLink): Promise<never> =>\n link.ended.then((reason) => {\n throw reason ?? new Error('the agent closed the link');\n });\n\nexport async function connectAgent(\n endpoint: PortEndpoint,\n options: { context?: Context } = {}\n): Promise<AgentConnection> {\n const context = options.context ?? BACKGROUND_CONTEXT;\n const link = new PortLink(endpoint);\n const serverId = await link.hello;\n const host = createHost({ name: `follower:${crypto.randomUUID()}`, kind: 'client' });\n host.connect(link);\n const get = (contract: Parameters<typeof host.get>[0]) =>\n host.get(contract, {}, { at: AGENT_HOST }) as unknown as Remote;\n const handles = [\n AgentControl,\n AgentTranscript,\n AgentSettings,\n AgentRemoteService,\n AgentPagesService,\n ].map(get);\n const [controlHandle, transcriptHandle, settingsHandle, remoteHandle, pagesHandle] = handles as [\n Remote,\n Remote,\n Remote,\n Remote,\n Remote,\n ];\n const release = () => {\n for (const handle of handles) handle.release();\n };\n try {\n await Promise.race([Promise.all(handles.map((handle) => handle.ready)), ended(link)]);\n } catch (error) {\n release();\n link.close('handshake failed');\n throw final(error);\n }\n const root: Root = (run, rootOptions) => host.root(run, rootOptions);\n const control = bound<AgentControl>(controlHandle, root, CONTROL_METHODS, controlAnswer);\n const members = Object.fromEntries(\n TRANSCRIPT_STATES.map((name) => [name, transcriptHandle[name] as Readable<unknown>])\n ) as Record<(typeof TRANSCRIPT_STATES)[number], Readable<unknown>>;\n const settingsState = settingsHandle.state as Readable<unknown>;\n const keeping = [...Object.values(members), settingsState].map(kept);\n await Promise.race([Promise.all(keeping.map((keep) => keep.ready)), ended(link)]);\n const settings: AgentSettings = {\n ...bound<Omit<AgentSettings, 'state'>>(settingsHandle, root, SETTINGS_METHODS, settingsAnswer),\n state: replicated(settingsState as Readable<SettingsState | undefined>),\n };\n const remote = bound<AgentRemote>(remoteHandle, root, REMOTE_METHODS, remoteAnswer);\n const pages = bound<AgentPages>(pagesHandle, root, PAGE_METHODS, pageAnswer);\n const state = <K extends keyof typeof members>(name: K) =>\n replicated(members[name] as Readable<never>);\n const held: Lasting<unknown>[] = [];\n const hold = <T>(state: ReplicatedState<T>): ReplicatedState<T> => {\n const kept = lasting(state);\n held.push(kept as Lasting<unknown>);\n return kept.state;\n };\n let open = true;\n const finish = () => {\n if (!open) return;\n open = false;\n for (const kept of held) kept.freeze();\n for (const keep of keeping) keep.stop();\n release();\n };\n const closed = link.ended.then((reason) => {\n finish();\n return reason;\n });\n return {\n serverId,\n readOnly: controlHandle.kind === 'view',\n control,\n transcript: hold<ConversationView>(state('state')),\n deliveries: hold<Record<string, Delivered>>(state('deliveries')),\n agents: hold<AgentsSummary>(state('agents')),\n views: hold<Record<string, ConversationView>>(state('views')),\n commands: hold<Command[]>(state('commands')),\n privateRevision: hold<PrivateRevision>(state('privateRevision')),\n sprinkles: hold<Sprinkle[]>(state('sprinkles')),\n memories: hold<MemoryEntry[]>(state('memories')),\n memoryScopes: hold<MemoryScope[]>(state('memoryScopes')),\n frozen: hold<FrozenCone[]>(state('frozen')),\n changes: hold<ChangesView>(state('changes')),\n monitor: hold<MonitorFeed>(state('monitor')),\n vault: hold<VaultState>(state('vault')),\n labels: hold<Record<string, string>>(state('labels')),\n guests: hold<GuestSeat[]>(state('guests')),\n viewOnly: controlHandle.kind === 'view',\n owner: pagesHandle.kind === 'pages',\n approvals: hold<Approval[]>(state('approvals')),\n approvalPolicy: hold<ApprovalPolicy>(state('approvalPolicy')),\n settings: lastingSettings(settings, hold),\n remote,\n pages,\n closed,\n webhook(name, delivery) {\n return control.webhook(\n name,\n { id: delivery.id ?? null, headers: delivery.headers ?? {}, body: delivery.body },\n context\n );\n },\n async prompt(text, whenBusy = 'followUp') {\n const sent = await control.send({ text, whenBusy, requestId: null }, context);\n if (!sent.accepted) throw new Error(`${sent.error.code}: ${sent.error.message}`);\n const settled = await control.wait(sent.submissionId, context);\n if (settled.status !== 'done') throw new Error(`unanswered: ${settled.reason}`);\n return settled.text;\n },\n async close() {\n finish();\n link.close('closed');\n await closed;\n await host.close();\n },\n };\n}\n"],
5
- "mappings": "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",
6
- "names": ["state", "kept"]
4
+ "sourcesContent": ["import {\n type Context,\n createHost,\n type Host,\n isChordError,\n type Readable,\n type ReplicatedState,\n} from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport type { ConversationView } from '@earendil-works/pi-durable';\nimport type { ApprovalPolicy } from './approvals/policy.ts';\nimport type { Approval } from './approvals/record.ts';\nimport type { ChangesView } from './changes/git.ts';\nimport type { FrozenCone } from './freezer/index.ts';\nimport type { GuestSeat } from './guests/seats.ts';\nimport type { MemoryEntry, MemoryScope } from './memory/format.ts';\nimport { controlAnswer, pageAnswer, remoteAnswer, settingsAnswer } from './readonly.ts';\nimport type { AgentRemote } from './remote/host.ts';\nimport type { AgentPages } from './remote/pages.ts';\nimport type { VaultState } from './secrets/vault.ts';\nimport {\n AgentControl,\n AgentPagesService,\n AgentRemoteService,\n AgentSettings,\n type AgentsSummary,\n AgentTranscript,\n CONTROL_METHODS,\n type Command,\n type Delivered,\n type GuestProposal,\n type GuestShown,\n type MonitorFeed,\n PAGE_METHODS,\n type PrivateRevision,\n REMOTE_METHODS,\n SETTINGS_METHODS,\n type SendMode,\n type SettingsState,\n TRANSCRIPT_STATES,\n} from './services.ts';\nimport { type AgentPrincipal, bound, type Remote, type Root, replicated, SELF } from './serving.ts';\nimport type { Sprinkle } from './sprinkles/kind.ts';\nimport { otherVersion, type PortEndpoint, PortLink } from './wire.ts';\n\nexport interface AgentConnection {\n readonly serverId: string;\n readonly control: AgentControl;\n readonly transcript: ReplicatedState<ConversationView>;\n readonly deliveries: ReplicatedState<Record<string, Delivered>>;\n readonly agents?: ReplicatedState<AgentsSummary>;\n readonly views?: ReplicatedState<Record<string, ConversationView>>;\n readonly commands?: ReplicatedState<Command[]>;\n readonly privateRevision?: ReplicatedState<PrivateRevision>;\n readonly sprinkles?: ReplicatedState<Sprinkle[]>;\n readonly memories?: ReplicatedState<MemoryEntry[]>;\n readonly memoryScopes?: ReplicatedState<MemoryScope[]>;\n readonly frozen?: ReplicatedState<FrozenCone[]>;\n readonly changes?: ReplicatedState<ChangesView>;\n readonly monitor?: ReplicatedState<MonitorFeed>;\n readonly vault?: ReplicatedState<VaultState>;\n readonly labels?: ReplicatedState<Record<string, string>>;\n readonly guests?: ReplicatedState<GuestSeat[]>;\n readonly viewOnly?: boolean;\n readonly guest?: GuestShown;\n readonly proposals?: ReplicatedState<GuestProposal[]>;\n readonly owner?: boolean;\n readonly approvals?: ReplicatedState<Approval[]>;\n readonly approvalPolicy?: ReplicatedState<ApprovalPolicy>;\n readonly settings: AgentSettings;\n readonly remote?: AgentRemote;\n readonly pages?: AgentPages;\n readonly closed: Promise<Error | undefined>;\n readonly readOnly?: boolean;\n prompt(text: string, whenBusy?: SendMode): Promise<string>;\n webhook(\n name: string,\n delivery: { id?: string | null; headers?: Record<string, string>; body: string }\n ): Promise<{ delivered: boolean }>;\n close(): Promise<void>;\n}\n\nexport interface Lasting<T> {\n readonly state: ReplicatedState<T>;\n freeze(): void;\n}\n\nexport function lasting<T>(state: ReplicatedState<T>): Lasting<T> {\n let last: T | undefined;\n let frozen = false;\n const read = () => {\n if (frozen) return last;\n try {\n last = state.value;\n } catch {}\n return last;\n };\n return {\n state: {\n get value() {\n return read();\n },\n subscribe: (listener) => (frozen ? () => undefined : state.subscribe(listener)),\n },\n freeze() {\n read();\n frozen = true;\n },\n };\n}\n\nfunction lastingSettings(\n settings: AgentSettings,\n hold: <T>(state: ReplicatedState<T>) => ReplicatedState<T>\n): AgentSettings {\n return {\n state: hold(settings.state),\n connect: (providerId, secret, fields, context) =>\n settings.connect(providerId, secret, fields, context),\n disconnect: (providerId, context) => settings.disconnect(providerId, context),\n signIn: (providerId, context) => settings.signIn(providerId, context),\n dismissMoved: (context) => settings.dismissMoved(context),\n };\n}\n\nconst VERSIONS = new Set(['upgrade_required', 'contract_mismatch']);\n\nexport const final = (error: unknown): unknown =>\n isChordError(error) && VERSIONS.has(error.code) ? otherVersion(error) : error;\n\nexport const ended = (link: PortLink): Promise<never> =>\n link.ended.then((reason) => {\n throw reason ?? new Error('the agent closed the link');\n });\n\ntype Contract = Parameters<Host<AgentPrincipal>['get']>[0];\n\nexport function clientHost(link: PortLink, name: string): Host<AgentPrincipal> {\n const host = createHost<AgentPrincipal>({\n name: `${name}:${crypto.randomUUID()}`,\n kind: 'client',\n self: SELF,\n });\n host.connect(link, { principal: SELF, onProtocolMismatch: () => link.mismatched() });\n return host;\n}\n\nexport async function holding(\n host: Host<AgentPrincipal>,\n link: PortLink,\n contracts: readonly Contract[]\n): Promise<Remote[]> {\n const stop = new AbortController();\n const getting = contracts.map(\n (contract) =>\n host.get(contract, {}, { signal: stop.signal }) as Promise<unknown> as Promise<Remote>\n );\n try {\n return await Promise.race([Promise.all(getting), ended(link)]);\n } catch (error) {\n stop.abort();\n for (const got of getting)\n void got.then(\n (ref) => ref.release(),\n () => undefined\n );\n link.close('handshake failed');\n void host.close().catch(() => undefined);\n throw error;\n }\n}\n\nexport async function connectAgent(\n endpoint: PortEndpoint,\n options: { context?: Context } = {}\n): Promise<AgentConnection> {\n const context = options.context ?? BACKGROUND_CONTEXT;\n const link = new PortLink(endpoint);\n const serverId = await link.hello;\n const host = clientHost(link, 'follower');\n const handles = await holding(host, link, [\n AgentControl,\n AgentTranscript,\n AgentSettings,\n AgentRemoteService,\n AgentPagesService,\n ]).catch((error: unknown) => {\n throw final(error);\n });\n const [controlHandle, transcriptHandle, settingsHandle, remoteHandle, pagesHandle] = handles as [\n Remote,\n Remote,\n Remote,\n Remote,\n Remote,\n ];\n const release = () => {\n for (const handle of handles) handle.release();\n };\n const root: Root = (run, rootOptions) => host.root(run, rootOptions);\n const control = bound<AgentControl>(controlHandle, root, CONTROL_METHODS, controlAnswer);\n const members = Object.fromEntries(\n TRANSCRIPT_STATES.map((name) => [name, transcriptHandle[name] as Readable<unknown>])\n ) as Record<(typeof TRANSCRIPT_STATES)[number], Readable<unknown>>;\n const settingsState = settingsHandle.state as Readable<SettingsState>;\n const settings: AgentSettings = {\n ...bound<Omit<AgentSettings, 'state'>>(settingsHandle, root, SETTINGS_METHODS, settingsAnswer),\n state: replicated(settingsState),\n };\n const remote = bound<AgentRemote>(remoteHandle, root, REMOTE_METHODS, remoteAnswer);\n const pages = bound<AgentPages>(pagesHandle, root, PAGE_METHODS, pageAnswer);\n const state = <K extends keyof typeof members>(name: K) =>\n replicated(members[name] as Readable<never>);\n const held: Lasting<unknown>[] = [];\n const hold = <T>(state: ReplicatedState<T>): ReplicatedState<T> => {\n const kept = lasting(state);\n held.push(kept as Lasting<unknown>);\n return kept.state;\n };\n let open = true;\n const finish = () => {\n if (!open) return;\n open = false;\n for (const kept of held) kept.freeze();\n release();\n };\n const closed = link.ended.then((reason) => {\n finish();\n return reason;\n });\n return {\n serverId,\n readOnly: controlHandle.variant === 'view',\n control,\n transcript: hold<ConversationView>(state('state')),\n deliveries: hold<Record<string, Delivered>>(state('deliveries')),\n agents: hold<AgentsSummary>(state('agents')),\n views: hold<Record<string, ConversationView>>(state('views')),\n commands: hold<Command[]>(state('commands')),\n privateRevision: hold<PrivateRevision>(state('privateRevision')),\n sprinkles: hold<Sprinkle[]>(state('sprinkles')),\n memories: hold<MemoryEntry[]>(state('memories')),\n memoryScopes: hold<MemoryScope[]>(state('memoryScopes')),\n frozen: hold<FrozenCone[]>(state('frozen')),\n changes: hold<ChangesView>(state('changes')),\n monitor: hold<MonitorFeed>(state('monitor')),\n vault: hold<VaultState>(state('vault')),\n labels: hold<Record<string, string>>(state('labels')),\n guests: hold<GuestSeat[]>(state('guests')),\n viewOnly: controlHandle.variant === 'view',\n owner: pagesHandle.variant === 'pages',\n approvals: hold<Approval[]>(state('approvals')),\n approvalPolicy: hold<ApprovalPolicy>(state('approvalPolicy')),\n settings: lastingSettings(settings, hold),\n remote,\n pages,\n closed,\n webhook(name, delivery) {\n return control.webhook(\n name,\n { id: delivery.id ?? null, headers: delivery.headers ?? {}, body: delivery.body },\n context\n );\n },\n async prompt(text, whenBusy = 'followUp') {\n const sent = await control.send({ text, whenBusy, requestId: null }, context);\n if (!sent.accepted) throw new Error(`${sent.error.code}: ${sent.error.message}`);\n const settled = await control.wait(sent.submissionId, context);\n if (settled.status !== 'done') throw new Error(`unanswered: ${settled.reason}`);\n return settled.text;\n },\n async close() {\n finish();\n link.close('closed');\n await closed;\n await host.close();\n },\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["state"]
7
7
  }
@@ -1,10 +1,10 @@
1
- import { createHost, isChordError, replicatedState } from "@earendil-works/chord";
2
- import { final, lasting } from "../client.js";
1
+ import { isChordError, replicatedState } from "@earendil-works/chord";
2
+ import { clientHost, final, holding, lasting } from "../client.js";
3
3
  import {
4
4
  AgentGuestService,
5
5
  CONTROL_METHODS
6
6
  } from "../services.js";
7
- import { AGENT_HOST, kept, plain, replicated } from "../serving.js";
7
+ import { plain, replicated } from "../serving.js";
8
8
  import { PortLink } from "../wire.js";
9
9
  const GUEST_ONLY = "a guest can only watch and send messages";
10
10
  const SEAT_REFUSED = "This guest link isn't valid any more.";
@@ -26,33 +26,21 @@ async function connectGuest(endpoint) {
26
26
  } catch {
27
27
  throw refusal(SEAT_REFUSED);
28
28
  }
29
- const host = createHost({ name: `guest:${crypto.randomUUID()}`, kind: "client" });
30
- host.connect(link);
31
- const handle = host.get(AgentGuestService, {}, { at: AGENT_HOST });
32
- const fail = async (error) => {
29
+ const host = clientHost(link, "guest");
30
+ const [handle] = await holding(host, link, [AgentGuestService]).catch((error) => {
31
+ const code = error?.code;
32
+ if (code === "EVERSION") throw error;
33
+ throw isChordError(error) && error.code !== "forbidden" ? final(error) : refusal(SEAT_REFUSED);
34
+ });
35
+ if (handle.variant !== "guest") {
33
36
  handle.release();
34
37
  link.close("handshake failed");
35
38
  await host.close();
36
- throw error;
37
- };
38
- try {
39
- await Promise.race([
40
- handle.ready,
41
- link.ended.then(() => {
42
- throw refusal(SEAT_REFUSED);
43
- })
44
- ]);
45
- } catch (error) {
46
- return fail(
47
- isChordError(error) && error.code === "forbidden" ? refusal(SEAT_REFUSED) : final(error)
48
- );
39
+ throw refusal(SEAT_REFUSED);
49
40
  }
50
- if (handle.kind !== "guest") return fail(refusal(SEAT_REFUSED));
51
41
  const transcriptMember = handle.transcript;
52
42
  const seatMember = handle.seat;
53
43
  const proposalsMember = handle.proposals;
54
- const keeping = [transcriptMember, seatMember, proposalsMember].map(kept);
55
- await Promise.race([Promise.all(keeping.map((keep) => keep.ready)), link.ended]);
56
44
  const through = (name) => async (...args) => {
57
45
  const call = handle[name];
58
46
  try {
@@ -95,7 +83,6 @@ async function connectGuest(endpoint) {
95
83
  open = false;
96
84
  transcript.freeze();
97
85
  proposals.freeze();
98
- for (const keep of keeping) keep.stop();
99
86
  handle.release();
100
87
  };
101
88
  const closed = link.ended.then((reason) => {