@knpkv/relay 0.1.0 → 0.2.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/README.md CHANGED
@@ -50,7 +50,10 @@ const RelayLive = Layer.unwrap(
50
50
  the run that answers it. `backend` switches the session from its next turn on (`RelayBackendNotConfigured`
51
51
  otherwise). `context` is what the person attached, as the product renders it (`{ label, body }`): it is
52
52
  placed just before the message for the model, and never appears in the transcript or the Snapshot.
53
- - `events(ref)`: a `Snapshot` first, with each message's `id` and the `runIds` of a run in flight. Then:
53
+ - `events(ref)`: a `Snapshot` first, with each message's `id`, the `runIds` of a run in flight and the
54
+ requestIds still `queued`. Then:
55
+ - `MessageQueued { requestId }`, then `MessagePlaced { id, requestId, text }` when a run takes it, or
56
+ `MessageWithdrawn { requestId }` when it is cancelled first.
54
57
  - `RunStarted { runIds }` and `TextDelta`.
55
58
  - `ToolStarted` and `ToolFinished`, each with a server-built, display-safe `summary`. A finished call carries
56
59
  its citations, and a write whose product projects one carries a `receipt { summary, providerId, link? }`
@@ -67,14 +70,31 @@ const RelayLive = Layer.unwrap(
67
70
  `NotInstalled`, `SignedOut`, `Misconfigured` or `NoCapability` and a one-line fix. Observed, never persisted.
68
71
  `SignedOut` comes from an `AuthenticationError` turn failure.
69
72
 
73
+ ## In the browser: `@knpkv/relay/wire`
74
+
75
+ The contract a product's `/…/relay` routes speak, as a separate browser bundle that imports only
76
+ `effect` and `@knpkv/capability`. It holds the harness's event and session schemas, plus:
77
+
78
+ - `RelayStreamFrame`: one `data:` frame of `GET events`, a `RelayEvent`, `Unauthorized` or `StreamFailed`;
79
+ - `RelayMessageRequest`, `RelayCancelRequest` and `RelayDecisionRequest`, the write bodies, and `RelayMessageAccepted`;
80
+ - the typed refusals: `RelayUnavailableError` (503, with the fix), `RelayConflictError` (409, with the
81
+ `RelayConflictState` found) and `RelayBadRequestError` (400).
82
+
83
+ A product narrows `ref` to its own objects and may add a typed `context` to a message. The build fails if
84
+ the bundle reaches anything else, and `pnpm test:pack` checks the packed `dist/wire.js` again.
85
+ `@knpkv/relay-product/client` reads a conversation through it.
86
+
70
87
  ## Security boundaries
71
88
 
72
89
  - The session store holds conversation content. Its directory is created `0700` and the database `0600`;
73
90
  a store directory, database or SQLite sidecar (`-wal`, `-shm`) that is a symbolic link, even a dangling one, fails with `RelayStoreLinked` before anything is
74
91
  re-permissioned or written;
75
92
  it is never logged, never sent to telemetry, and never served on unauthenticated routes.
76
- - One process owns a store: the connection runs in SQLite's exclusive locking mode, so a second owner fails
77
- with `RelayStoreLocked`, and the lock is released by the OS if the owner dies.
93
+ - One process owns a store: it holds an exclusive SQLite lock on `<store>.lock` (owner-only), so a second
94
+ owner fails with `RelayStoreLocked`. Closing the harness frees the store at once, so the same process can
95
+ open it again; the OS releases the lock if the owner dies. The lock is the runtime's own SQLite
96
+ (`node:sqlite` on Node, `bun:sqlite` on Bun), loaded when a store opens and used only through `exec`,
97
+ because libsql keeps a closed connection, and its locks, until its statements are garbage-collected.
78
98
  - Provider credentials stay with the user's CLI. Relay only spawns `claude`/`codex` with all of their tools
79
99
  withheld.
80
100
 
package/dist/harness.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Context, Crypto, Effect, FileSystem, Layer, Path, Schema, Stream } from "effect";
1
+ import { Context, Crypto, Effect, FileSystem, Layer, Path, Schema, Scope, Stream } from "effect";
2
2
  import type { LanguageModel } from "effect/ai";
3
3
  import { RelayBackendId } from "./model.js";
4
4
  import type { BackendStatus, ObjectRef, RelayEvent, SessionInfo, SessionTool } from "./model.js";
@@ -120,9 +120,13 @@ export interface RelayHarnessService {
120
120
  declare const RelayHarness_base: Context.ServiceClass<RelayHarness, "@knpkv/relay/RelayHarness", RelayHarnessService>;
121
121
  export declare class RelayHarness extends RelayHarness_base {
122
122
  }
123
- /** Build the harness for one product. */
124
- export declare const make: <Requirements>(options: RelayHarnessOptions<Requirements>) => Effect.Effect<RelayHarnessService, RelayStoreFailed | RelayStoreLinked | RelayStoreLocked, Requirements | Crypto.Crypto | FileSystem.FileSystem | Path.Path | import("effect/Scope").Scope>;
123
+ /**
124
+ * Build the harness for one product. One attempt's resources (the store lock, the store, the harness) live in
125
+ * their own scope: an attempt that fails releases them at once, so a retry in the same scope can take the
126
+ * store; one that succeeds keeps them until the caller's scope closes.
127
+ */
128
+ export declare const make: <Requirements>(options: RelayHarnessOptions<Requirements>) => Effect.Effect<RelayHarnessService, RelayStoreFailed | RelayStoreLinked | RelayStoreLocked, Crypto.Crypto | FileSystem.FileSystem | Path.Path | Scope.Scope | Exclude<Requirements, Scope.Scope>>;
125
129
  /** Build the harness as a scoped layer: the store closes, and the lock is released, with the scope. */
126
- export declare const layer: <Requirements>(options: RelayHarnessOptions<Requirements>) => Layer.Layer<RelayHarness, RelayStoreFailed | RelayStoreLinked | RelayStoreLocked, Crypto.Crypto | FileSystem.FileSystem | Path.Path | Exclude<Requirements, import("effect/Scope").Scope>>;
130
+ export declare const layer: <Requirements>(options: RelayHarnessOptions<Requirements>) => Layer.Layer<RelayHarness, RelayStoreFailed | RelayStoreLinked | RelayStoreLocked, Crypto.Crypto | FileSystem.FileSystem | Path.Path | Exclude<Exclude<Requirements, Scope.Scope>, Scope.Scope>>;
127
131
  export {};
128
132
  //# sourceMappingURL=harness.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../src/harness.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../src/harness.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -4757,10 +4757,10 @@ async function finalResult(runtime, call, result, reported, context) {
4757
4757
  harness.push(truncated(bounded, reported.limits.retain));
4758
4758
  return { ...final, content: bounded.content, diagnostics: [...final.diagnostics ?? [], ...harness] };
4759
4759
  }
4760
- async function settle(runtime, call, ending, build, context) {
4760
+ async function settle(runtime, call, ending, build2, context) {
4761
4761
  await runtime.commit(async (tx) => {
4762
4762
  const slot = toolSlot(await tx.doc(LiveDoc, runtime.conversationId), runtime.taskId);
4763
- const result = build(slot);
4763
+ const result = build2(slot);
4764
4764
  const entry = await appendToolResult(tx, runtime.conversationId, call, result, runtime.now());
4765
4765
  if (slot !== void 0)
4766
4766
  finishSlot(slot, entry.id);
@@ -9257,11 +9257,11 @@ var HarnessImpl = class extends SessionImpl {
9257
9257
  }
9258
9258
  /** Build a conversation's environment from its current `cwd`; `undefined` without an `env` option. */
9259
9259
  async buildEnv(id, context) {
9260
- const build = this.#options.env;
9261
- if (build === void 0)
9260
+ const build2 = this.#options.env;
9261
+ if (build2 === void 0)
9262
9262
  return void 0;
9263
9263
  const cwd = (await this.snapshot(AgentDoc, id, context))?.cwd;
9264
- return build({ conversationId: id, ...cwd === void 0 ? {} : { cwd }, read: this }, context);
9264
+ return build2({ conversationId: id, ...cwd === void 0 ? {} : { cwd }, read: this }, context);
9265
9265
  }
9266
9266
  /** Reconcile surviving `running` tasks to `pending`; part of open. */
9267
9267
  openTasks(context) {
@@ -10117,8 +10117,9 @@ var SqliteStorage = class _SqliteStorage {
10117
10117
  // dist/harness.js
10118
10118
  import * as Capability2 from "@knpkv/capability";
10119
10119
  import { createClient } from "@libsql/client";
10120
- import { Clock, Context, Crypto, Deferred, Effect as Effect3, Exit, FileSystem, Layer, Path, Predicate as Predicate3, PubSub, Queue, Ref, Schema as Schema5, Semaphore, Stream } from "effect";
10120
+ import { Clock, Context, Crypto, Deferred, Effect as Effect3, Exit, FileSystem, Layer, Path, Predicate as Predicate4, PubSub, Queue, Ref, Schema as Schema5, Scope, Semaphore, Stream } from "effect";
10121
10121
  import { AiError } from "effect/ai";
10122
+ import { createRequire } from "node:module";
10122
10123
 
10123
10124
  // dist/libsqlDatabase.js
10124
10125
  import { Predicate } from "effect";
@@ -10174,6 +10175,48 @@ var libsqlDatabase = (client) => {
10174
10175
  };
10175
10176
  };
10176
10177
 
10178
+ // dist/messages.js
10179
+ import { Predicate as Predicate2 } from "effect";
10180
+ var entryText = (messages) => (messages ?? []).flatMap((message) => message.role === "user" || message.role === "assistant" ? Predicate2.isString(message.content) ? [message.content] : message.content.flatMap((block) => block.type === "text" && block.text !== void 0 ? [block.text] : []) : []).join("\n");
10181
+ var makeMessageTracker = () => {
10182
+ const texts = /* @__PURE__ */ new Map();
10183
+ const requests = /* @__PURE__ */ new Map();
10184
+ const announced = /* @__PURE__ */ new Set();
10185
+ const placed = (entry) => {
10186
+ const text = texts.get(entry);
10187
+ const requestId = requests.get(entry);
10188
+ if (text === void 0 || requestId === void 0 || announced.has(entry))
10189
+ return [];
10190
+ announced.add(entry);
10191
+ texts.delete(entry);
10192
+ requests.delete(entry);
10193
+ return [{ _tag: "MessagePlaced", id: entry, requestId, text }];
10194
+ };
10195
+ return {
10196
+ /** A committed entry; only a person's message can place one. */
10197
+ entry: (entry) => {
10198
+ if (entry.kind !== "pi.user" || announced.has(String(entry.id)))
10199
+ return [];
10200
+ texts.set(String(entry.id), entryText(entry.model));
10201
+ return placed(String(entry.id));
10202
+ },
10203
+ /** A submission record; Relay submits every input with the dock's requestId, its id is the fallback. */
10204
+ record: (record) => {
10205
+ if (record.type !== "input")
10206
+ return [];
10207
+ const requestId = record.requestId ?? String(record.id);
10208
+ if (record.entry === void 0) {
10209
+ return [{ _tag: record.status === "queued" ? "MessageQueued" : "MessageWithdrawn", requestId }];
10210
+ }
10211
+ const entry = String(record.entry);
10212
+ if (announced.has(entry))
10213
+ return [];
10214
+ requests.set(entry, requestId);
10215
+ return placed(entry);
10216
+ }
10217
+ };
10218
+ };
10219
+
10177
10220
  // dist/model.js
10178
10221
  import * as Capability from "@knpkv/capability";
10179
10222
  import { Schema } from "effect";
@@ -10228,8 +10271,19 @@ var RelayEvent = Schema.TaggedUnion({
10228
10271
  ...eventFields,
10229
10272
  messages: Schema.Array(Schema.Struct({ id: Name, role: Schema.Literals(["user", "relay"]), text: Schema.String })),
10230
10273
  /** The run in flight, empty when idle. */
10231
- runIds: RunIds
10274
+ runIds: RunIds,
10275
+ /** Messages accepted but not yet in the transcript, oldest first: they wait for the run in flight. */
10276
+ queued: RunIds
10232
10277
  },
10278
+ /**
10279
+ * A message the server accepted that waits for the run in flight. It joins the transcript with its own
10280
+ * `MessagePlaced`, or leaves with `MessageWithdrawn`.
10281
+ */
10282
+ MessageQueued: { ...eventFields, requestId: Name },
10283
+ /** A message the server placed in the transcript; `id` is its transcript id, as a later Snapshot names it. */
10284
+ MessagePlaced: { ...eventFields, id: Name, requestId: Name, text: Schema.String },
10285
+ /** A queued message the person withdrew before any run took it. */
10286
+ MessageWithdrawn: { ...eventFields, requestId: Name },
10233
10287
  RunStarted: { ...eventFields, runIds: RunIds },
10234
10288
  TextDelta: { ...eventFields, text: Schema.String },
10235
10289
  /** `summary` is a server-built, display-safe line; `input` is the call's typed arguments. */
@@ -11309,11 +11363,11 @@ function createProvider(input) {
11309
11363
  }
11310
11364
 
11311
11365
  // dist/piProvider.js
11312
- import { Predicate as Predicate2, Schema as Schema2 } from "effect";
11366
+ import { Predicate as Predicate3, Schema as Schema2 } from "effect";
11313
11367
  var API = "relay-cli-turn";
11314
11368
  var decodeJson = Schema2.decodeUnknownOption(Schema2.Json);
11315
11369
  var MODEL_ID = "default";
11316
- var textOf = (content) => Predicate2.isString(content) ? content : content.flatMap((block) => block.type === "text" && block.text !== void 0 ? [block.text] : []).join("\n");
11370
+ var textOf = (content) => Predicate3.isString(content) ? content : content.flatMap((block) => block.type === "text" && block.text !== void 0 ? [block.text] : []).join("\n");
11317
11371
  var turnRequestOf = (messages) => {
11318
11372
  const instructions = [];
11319
11373
  const tools = /* @__PURE__ */ new Map();
@@ -11636,7 +11690,48 @@ var promise = (operation, run2) => Effect3.tryPromise({
11636
11690
  try: run2,
11637
11691
  catch: (cause) => new RelayStoreFailed({ operation, message: `${operation} failed: ${String(cause)}` })
11638
11692
  });
11639
- var notALink = (error) => error.reason._tag === "NotFound" || error.reason._tag !== "BadArgument" && Predicate3.hasProperty(error.reason.cause, "code") && error.reason.cause.code === "EINVAL";
11693
+ var notALink = (error) => error.reason._tag === "NotFound" || error.reason._tag !== "BadArgument" && Predicate4.hasProperty(error.reason.cause, "code") && error.reason.cause.code === "EINVAL";
11694
+ var lockDriver = Effect3.tryPromise({
11695
+ try: async () => {
11696
+ try {
11697
+ const { DatabaseSync } = await import("node:sqlite");
11698
+ return (path) => new DatabaseSync(path);
11699
+ } catch (nodeFailure) {
11700
+ try {
11701
+ const bun = createRequire(import.meta.url)("bun:sqlite");
11702
+ return (path) => new bun.Database(path);
11703
+ } catch (bunFailure) {
11704
+ throw new AggregateError([nodeFailure, bunFailure], `neither node:sqlite (${String(nodeFailure)}) nor bun:sqlite (${String(bunFailure)}) could load`, { cause: bunFailure });
11705
+ }
11706
+ }
11707
+ },
11708
+ catch: (cause) => new RelayStoreFailed({
11709
+ operation: "load SQLite for the Relay store lock",
11710
+ message: `load SQLite for the Relay store lock failed: ${String(cause)}`
11711
+ })
11712
+ });
11713
+ var isBusy = (cause) => Predicate4.hasProperty(cause, "errcode") && cause.errcode === 5 || Predicate4.hasProperty(cause, "errno") && cause.errno === 5 || Predicate4.hasProperty(cause, "code") && cause.code === "SQLITE_BUSY";
11714
+ var acquireStoreLock = (path) => Effect3.acquireRelease(Effect3.flatMap(lockDriver, (open) => Effect3.try({
11715
+ try: () => {
11716
+ const lock = open(path);
11717
+ try {
11718
+ lock.exec("PRAGMA locking_mode = EXCLUSIVE");
11719
+ lock.exec("BEGIN IMMEDIATE");
11720
+ lock.exec("COMMIT");
11721
+ return lock;
11722
+ } catch (failure) {
11723
+ lock.close();
11724
+ throw failure;
11725
+ }
11726
+ },
11727
+ catch: (cause) => isBusy(cause) ? new RelayStoreLocked({
11728
+ path,
11729
+ message: `Another process owns the Relay store at ${path.replace(/\.lock$/u, "")}. Stop it, then retry.`
11730
+ }) : new RelayStoreFailed({
11731
+ operation: "lock the Relay store",
11732
+ message: `lock the Relay store failed: ${String(cause)}`
11733
+ })
11734
+ })), (lock) => Effect3.sync(() => lock.close()));
11640
11735
  var openStore = (path) => Effect3.acquireRelease(Effect3.gen(function* () {
11641
11736
  const fs = yield* FileSystem.FileSystem;
11642
11737
  const paths = yield* Path.Path;
@@ -11654,17 +11749,17 @@ var openStore = (path) => Effect3.acquireRelease(Effect3.gen(function* () {
11654
11749
  }));
11655
11750
  yield* refuseLink(directory, "store directory");
11656
11751
  yield* fs.makeDirectory(directory, { recursive: true, mode: 448 }).pipe(ownerOnly("create the Relay store directory"));
11657
- yield* Effect3.forEach([path, `${path}-wal`, `${path}-shm`], (file) => refuseLink(file, "store"), {
11752
+ yield* Effect3.forEach([path, `${path}-wal`, `${path}-shm`, `${path}.lock`], (file) => refuseLink(file, "store"), {
11658
11753
  discard: true
11659
11754
  });
11660
11755
  yield* fs.chmod(directory, 448).pipe(ownerOnly("restrict the Relay store directory"));
11756
+ yield* acquireStoreLock(`${path}.lock`);
11757
+ yield* fs.chmod(`${path}.lock`, 384).pipe(ownerOnly("restrict the Relay store lock"));
11661
11758
  const client = createClient({ url: `file:${path}` });
11662
11759
  const database = libsqlDatabase(client);
11663
11760
  yield* promise("open the Relay store", async () => {
11664
- await database.exec("PRAGMA locking_mode = EXCLUSIVE");
11665
11761
  await database.exec("PRAGMA journal_mode = WAL");
11666
11762
  await database.exec("PRAGMA synchronous = NORMAL");
11667
- await database.exec("BEGIN IMMEDIATE; COMMIT");
11668
11763
  }).pipe(Effect3.andThen(fs.chmod(path, 384).pipe(ownerOnly("restrict the Relay store"))), Effect3.catchTag("RelayStoreFailed", (failure) => /SQLITE_BUSY|database is locked/u.test(failure.message) ? Effect3.fail(new RelayStoreLocked({
11669
11764
  path,
11670
11765
  message: `Another process owns the Relay store at ${path}. Stop it, then retry.`
@@ -11672,6 +11767,11 @@ var openStore = (path) => Effect3.acquireRelease(Effect3.gen(function* () {
11672
11767
  return database;
11673
11768
  }), (database) => Effect3.promise(() => database.close()));
11674
11769
  var make = Effect3.fn("RelayHarness.make")(function* (options) {
11770
+ const owner = yield* Scope.Scope;
11771
+ const attempt = yield* Scope.fork(owner, "sequential");
11772
+ return yield* build(options).pipe(Scope.provide(attempt), Effect3.onExit((exit) => Exit.isFailure(exit) ? Scope.close(attempt, exit) : Effect3.void));
11773
+ });
11774
+ var build = Effect3.fn("RelayHarness.build")(function* (options) {
11675
11775
  const services = yield* Effect3.context();
11676
11776
  const runEffect = (effect, signal) => Effect3.runPromiseExitWith(services)(effect, signal === void 0 ? void 0 : { signal });
11677
11777
  const runPromise = Effect3.runPromiseWith(services);
@@ -11861,7 +11961,6 @@ ${item.body}`,
11861
11961
  });
11862
11962
  });
11863
11963
  var layer = (options) => Layer.effect(RelayHarness, make(options));
11864
- var entryText = (messages) => (messages ?? []).flatMap((message) => message.role === "user" || message.role === "assistant" ? Predicate3.isString(message.content) ? [message.content] : message.content.flatMap((block) => block.type === "text" ? [block.text] : []) : []).join("\n");
11865
11964
  var decodeDetails = Schema5.decodeUnknownOption(Schema5.Struct({ cites: Schema5.Array(Capability2.ObjectRef), receipt: Schema5.optionalKey(WriteReceipt) }));
11866
11965
  var sessionEvents = (harness, conversation, confirmations, pendingFor, display) => {
11867
11966
  const summarize = (capability, args) => display(capability)?.summarize(args) ?? capability;
@@ -11905,7 +12004,9 @@ var sessionEvents = (harness, conversation, confirmations, pendingFor, display)
11905
12004
  }]);
11906
12005
  return [...partial, ...running];
11907
12006
  };
11908
- const toRelay = (event, runIds) => {
12007
+ const messages = makeMessageTracker();
12008
+ const fromMessages = (events) => events.map((event) => ({ ...event, session }));
12009
+ const toRelay = (event, runIds, queued2 = []) => {
11909
12010
  switch (event.type) {
11910
12011
  case "snapshot":
11911
12012
  return [
@@ -11913,6 +12014,7 @@ var sessionEvents = (harness, conversation, confirmations, pendingFor, display)
11913
12014
  _tag: "Snapshot",
11914
12015
  session,
11915
12016
  runIds,
12017
+ queued: queued2,
11916
12018
  messages: event.entries.flatMap((entry) => entry.kind === "pi.user" ? [{ id: String(entry.id), role: "user", text: entryText(entry.model) }] : entry.kind === "pi.assistant" ? [{ id: String(entry.id), role: "relay", text: entryText(entry.model) }] : [])
11917
12019
  },
11918
12020
  ...inFlight(event),
@@ -11921,6 +12023,11 @@ var sessionEvents = (harness, conversation, confirmations, pendingFor, display)
11921
12023
  case "message_start":
11922
12024
  sent.clear();
11923
12025
  return event.message.role === "assistant" ? event.message.content.flatMap((block, index) => block.type === "text" ? textFrom(index, block.text) : []) : [];
12026
+ case "message_end":
12027
+ case "entry_appended":
12028
+ return fromMessages(messages.entry(event.entry));
12029
+ case "submission":
12030
+ return fromMessages(messages.record(event.record));
11924
12031
  case "message_update":
11925
12032
  return event.changes.flatMap((change) => {
11926
12033
  if (change.type === "text_delta") {
@@ -11979,6 +12086,10 @@ var sessionEvents = (harness, conversation, confirmations, pendingFor, display)
11979
12086
  const runIdsOf = (inputs, read2) => inputs.map((id, index) => read2[index]?.requestId ?? String(id));
11980
12087
  let active = [];
11981
12088
  const track2 = (inputs) => (inputs.length === 0 ? Effect3.succeed([]) : promise("read the run in flight", async () => runIdsOf(inputs, await records(inputs)))).pipe(Effect3.tap((runIds) => Effect3.sync(() => active = runIds)));
12089
+ const queuedOf = (inbox) => {
12090
+ const inputs = inbox.map(({ id }) => id);
12091
+ return inputs.length === 0 ? Effect3.succeed([]) : promise("read the queued messages", async () => runIdsOf(inputs, await records(inputs)));
12092
+ };
11982
12093
  const runEnded = (inputs) => promise("read how the run ended", async () => {
11983
12094
  const read2 = await records(inputs);
11984
12095
  const runIds = runIdsOf(inputs, read2);
@@ -11991,13 +12102,13 @@ var sessionEvents = (harness, conversation, confirmations, pendingFor, display)
11991
12102
  session,
11992
12103
  runIds,
11993
12104
  // `reason` is Pi's code (`model_error`); `detail` carries the backend's own message when there is one.
11994
- cause: Predicate3.isString(unanswered.detail) ? unanswered.detail : unanswered.reason,
12105
+ cause: Predicate4.isString(unanswered.detail) ? unanswered.detail : unanswered.reason,
11995
12106
  fix: "Check the backend in Relay setup (installed and signed in), then send the message again."
11996
12107
  };
11997
12108
  });
11998
12109
  const gate = Stream.fromPubSub(confirmations).pipe(Stream.filter(({ conversationId }) => conversationId === session), Stream.map(({ event }) => event));
11999
12110
  let seq = 0;
12000
- const relayed = Stream.flatMap(fromPi, (event) => event.type === "run_end" ? Stream.fromEffect(runEnded(event.inputs).pipe(Effect3.tap(() => Effect3.sync(() => active = [])))) : event.type === "run_start" ? Stream.fromEffect(track2(event.inputs)).pipe(Stream.map((runIds) => ({ _tag: "RunStarted", session, runIds }))) : event.type === "snapshot" ? Stream.fromEffect(track2(event.run?.inputs ?? [])).pipe(Stream.flatMap((runIds) => Stream.fromIterable(toRelay(event, runIds)))) : Stream.fromIterable(toRelay(event, active)));
12111
+ const relayed = Stream.flatMap(fromPi, (event) => event.type === "run_end" ? Stream.fromEffect(runEnded(event.inputs).pipe(Effect3.tap(() => Effect3.sync(() => active = [])))) : event.type === "run_start" ? Stream.fromEffect(track2(event.inputs)).pipe(Stream.map((runIds) => ({ _tag: "RunStarted", session, runIds }))) : event.type === "snapshot" ? Stream.fromEffect(Effect3.all([track2(event.run?.inputs ?? []), queuedOf(event.inbox)])).pipe(Stream.flatMap(([runIds, queued2]) => Stream.fromIterable(toRelay(event, runIds, queued2)))) : Stream.fromIterable(toRelay(event, active)));
12001
12112
  return Stream.merge(relayed, gate).pipe(Stream.map((event) => ({ ...event, seq: seq++ })));
12002
12113
  };
12003
12114