eve-hub 0.8.1 → 0.8.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # eve-hub
2
2
 
3
+ ## 0.8.2
4
+
5
+ ### Patch Changes
6
+
7
+ - The per-bot mention gate is gone: the hub decides who a bot's message wakes (wake router), so the `hooks/mention-gate` hook is removed from the kit.
8
+
3
9
  ## 0.8.1
4
10
 
5
11
  ### Patch Changes
package/kit-modules.json CHANGED
@@ -2,7 +2,6 @@
2
2
  "channels/eve",
3
3
  "channels/eve-roster-user",
4
4
  "connections/vercel-kit",
5
- "hooks/mention-gate",
6
5
  "hooks/sandbox",
7
6
  "instructions/engineering",
8
7
  "instructions/main",
@@ -2,5 +2,4 @@ export type GateConfig = { enabled: boolean; model: string; api: 'chat' | 'decis
2
2
  export function gateConfig(env?: Record<string, string | undefined>, file?: unknown): GateConfig;
3
3
  export function opensWithHandle(text: string, agent: string): boolean;
4
4
  export function cleanReason(reason: unknown): string;
5
- export function screenInbound(input: { text: string; sessionId: string; agent?: string; registryUrl: string; credential: (opts?: { refresh?: boolean }) => Promise<{ token: string; teamId?: string | null } | null>; instructions?: string; fetchImpl?: typeof fetch; log?: ((line: string) => void) | null }): Promise<{ cancel: boolean; result?: unknown }>;
6
5
  export function hubCredentialLoader(fetchHub: (path: string, init: { headers?: Record<string, string> }) => Promise<Response>): (opts?: { refresh?: boolean }) => Promise<{ ok: boolean; token?: string; error?: string }>;
package/mention-gate.mjs CHANGED
@@ -1,19 +1,7 @@
1
- // Mention gate: when a bot's turn is woken by an @-mention or a DM, the bot
2
- // itself (its message.received hook, hooks/mention-gate.ts) asks a cheap
3
- // model, through AI Gateway, whether a reply is needed, before its main model
4
- // runs. The model reads the bot's own instructions and the delivery the bot
5
- // just received. A handle inside code (codeRanges) is dropped by
6
- // parseMentions before it joins or wakes anyone; the gate catches the other
7
- // incidental ones: a quote or a recap, or an ack that asks nothing.
8
- //
9
- // The hub decides which wakes are gated (gatedWake, registry/server.mjs) and
10
- // owns the outcome: the hook asks it first (POST /api/mention-gate/check),
11
- // then reports the decision (POST /api/mention-gate/report); on a skip the
12
- // hub posts the system line and records the usage, and the hook cancels the
13
- // turn. Biased to answering: the model only classifies the message (how the
14
- // handle is used, what it asks of the bot); skipFor decides, and any error,
15
- // timeout or unreadable verdict answers. Only bots' messages are gated: the
16
- // human's always wake.
1
+ // The decision-model helpers the registry's mention gate (registry/server.mjs, frozen until its removal) and the hosted hub's wake
2
+ // router and closing check share: the gateway config (gateConfig), the Mac bot credential loader and the response parsing. The bot-side
3
+ // hook is gone; the hosted hub decides who a message wakes (hosted-hub/src/store/wake-router.ts). A handle inside code (codeRanges)
4
+ // is dropped by parseMentions before it joins or wakes anyone.
17
5
  //
18
6
  // Config (the hub's, sent to the hook with each check): `mentionGate` in
19
7
  // hub.workspace.json ({ enabled, model, reasoningEffort, timeoutMs }),
@@ -21,7 +9,6 @@
21
9
  // HUB_MENTION_GATE_REASONING, HUB_MENTION_GATE_TIMEOUT_MS and
22
10
  // HUB_MENTION_GATE_URL (tests).
23
11
  import { codeRanges } from "./mention-tokens.mjs";
24
- import { hubHeaders } from "./registry-fetch.mjs";
25
12
  import { createHash } from "node:crypto";
26
13
 
27
14
  export { codeRanges };
@@ -395,45 +382,6 @@ export function credentialCache(load, { ttlMs = 10 * 60e3, failMs = 60e3, now =
395
382
  return get;
396
383
  }
397
384
 
398
- // --- The bot's side (hooks/mention-gate.ts).
399
-
400
- const HUB_CALL_MS = 2000;
401
-
402
- async function hubCall(fetchImpl, url, body) {
403
- const res = await fetchImpl(url, { method: "POST", headers: await hubHeaders({ "content-type": "application/json" }), body: JSON.stringify(body), signal: AbortSignal.timeout(HUB_CALL_MS) });
404
- if (!res.ok) throw new Error(`hub ${res.status}`);
405
- return res.json();
406
- }
407
-
408
- // One inbound delivery, screened. Never throws; { cancel: true } only when the
409
- // hub accepted a skip (it posted the system line and expects the turn to
410
- // end cancelled). Anything that fails on the way answers.
411
- // text: the delivery (message.received's data.message)
412
- // credential: ({ refresh }) → { token, teamId? } | null (credentialCache)
413
- // `agent` is optional: left out, the hub names the bot from its session and sends its instructions.
414
- export async function screenInbound({ text, sessionId, agent, registryUrl, instructions = "", credential, fetchImpl = fetch, log = console.warn }) {
415
- const messageId = triggerId(text);
416
- if (!registryUrl || !messageId) return { cancel: false };
417
- let check;
418
- try {
419
- check = await hubCall(fetchImpl, `${registryUrl}/api/mention-gate/check`, { ...(agent ? { agent } : {}), messageId, sessionId });
420
- } catch (error) {
421
- log?.(`[mention-gate] check for ${messageId} failed, answering: ${error?.message ?? error}`);
422
- return { cancel: false };
423
- }
424
- if (!check?.gate || !check.config) return { cancel: false };
425
- agent ??= check.agent;
426
- instructions ||= check.instructions ?? "";
427
- const r = await decide({ config: check.config, credential, prompt: promptFor(check.config, { agent, human: check.humanName ?? "the human", instructions, envelope: text, broadcast: Boolean(check.broadcast) }), addressed: Boolean(check.addressed), fetchImpl });
428
- try {
429
- const ack = await hubCall(fetchImpl, `${registryUrl}/api/mention-gate/report`, { ...(agent ? { agent } : {}), messageId, sessionId, skip: r.skip, reason: r.reason, verdict: r.verdict ?? null, usage: r.usage ?? null, ms: r.ms, error: r.error ?? null });
430
- return { cancel: Boolean(r.skip && ack?.skip), result: r };
431
- } catch (error) {
432
- log?.(`[mention-gate] report for ${messageId} failed, answering: ${error?.message ?? error}`);
433
- return { cancel: false, result: r };
434
- }
435
- }
436
-
437
385
  // A Mac bot's AI Gateway credential: the hub mints an OIDC token for the workspace's bots project, asked for with the bot's HUB_TOKEN.
438
386
  // `fetchHub(path, init)` is registryFetch. On a refresh it names the token AI Gateway refused, as eve's served credential does.
439
387
  export function hubCredentialLoader(fetchHub) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eve-hub",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Bot-side code shared by every hub agent: registry tools, the roster hook, the eve channel, the host sandbox and browser, and model helpers.",
6
6
  "type": "module",
package/stubs.mjs CHANGED
@@ -4,7 +4,6 @@
4
4
  export const KIT_FILES = {
5
5
  "channels/eve.ts": ["Queued eve channel that resolves the registry's human as a user.", "channels/eve-roster-user.ts"],
6
6
  "hooks/sandbox.ts": ["Migrates legacy Docker sessions to the host sandbox."],
7
- "hooks/mention-gate.ts": ["Mention gate: a cheap model decides whether an inbound @-mention or DM needs a turn."],
8
7
  "tools/bash.ts": ["eve's bash with a default time limit (kills the process group) and the workspace's code location."],
9
8
  "tools/react.ts": ["React to a message with an emoji."],
10
9
  "tools/show.ts": ["Move the human's screen to a thread, DM, or message."],
@@ -52,7 +51,6 @@ export const MAIN_KIT_FILES = {
52
51
  // main's dash schedule stays: off the hub's machine it has no PR data (its lookup runs `gh`).
53
52
  export const DEPLOYED_KIT_FILES = new Set([
54
53
  "channels/eve.ts",
55
- "hooks/mention-gate.ts",
56
54
  "schedules/dash.ts",
57
55
  "skills/onboarding-profile.ts",
58
56
  "tools/onboarding_scripts.ts",
package/world.mjs CHANGED
@@ -21397,6 +21397,12 @@ async function afterAppend(append, routed, fetchFn, env = process.env) {
21397
21397
  // ../../hosted-hub/bots/world.ts
21398
21398
  var import_oidc = __toESM(require_oidc(), 1);
21399
21399
 
21400
+ // ../../hosted-hub/src/store/db.ts
21401
+ import { AsyncLocalStorage } from "node:async_hooks";
21402
+ import { createClient, LibsqlError } from "@libsql/client";
21403
+ var s = (sql, ...args) => ({ sql, args });
21404
+ var running = new AsyncLocalStorage();
21405
+
21400
21406
  // mention-tokens.mjs
21401
21407
  function codeRanges(text) {
21402
21408
  const s2 = String(text ?? "");
@@ -21498,90 +21504,144 @@ function gateConfig(env = process.env, file2 = {}) {
21498
21504
  };
21499
21505
  }
21500
21506
 
21501
- // ../../hosted-hub/src/store/db.ts
21502
- import { AsyncLocalStorage } from "node:async_hooks";
21503
- import { createClient, LibsqlError } from "@libsql/client";
21504
- var s = (sql, ...args) => ({ sql, args });
21505
- var running = new AsyncLocalStorage();
21506
-
21507
21507
  // ../../hosted-hub/src/store/schema.ts
21508
21508
  var OPEN_TURN_STATUSES = "('streaming','cancelling')";
21509
21509
  var UNSETTLED_DELIVERIES = "unsettled AS MATERIALIZED (SELECT rowid AS rid, * FROM deliveries WHERE accepted_at IS NULL AND failed_at IS NULL AND revoked_at IS NULL)";
21510
21510
 
21511
- // ../../hosted-hub/src/domain/ids.ts
21512
- import { createHash, randomBytes } from "node:crypto";
21513
- var alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
21514
- function base32(value) {
21515
- let out = "";
21516
- for (let i = 0; i < 26; i++) {
21517
- out = alphabet[Number(value & 31n)] + out;
21518
- value >>= 5n;
21519
- }
21520
- return out;
21511
+ // ../../hosted-hub/src/store/wake-router.ts
21512
+ var GATE = gateConfig(process.env);
21513
+ var WAKE_AT = 0.15;
21514
+ var STEER_AT = 0.5;
21515
+ var CLIP = 700;
21516
+ var clip = (t, n = CLIP) => t.length > n ? `${t.slice(0, n)} \u2026` : t;
21517
+ var BOTS_BUSY = `SELECT b.name, b.role, EXISTS (SELECT 1 FROM turns t WHERE t.thread_id = ? AND t.bot = b.name AND t.status IN ${OPEN_TURN_STATUSES}) AS busy FROM bots b`;
21518
+ var WAKE_QUESTION = {
21519
+ named: (bot) => `The message @-mentions ${bot}. Decide whether ${bot} has something to do after reading it. Yes if it asks ${bot} for anything, hands ${bot} work or a result to review, land or build on, or gives ${bot} a fix, correction or answer ${bot} was waiting on. No if it only thanks or acknowledges, says it reacted or changed nothing, says nothing is needed from ${bot} or that the matter is already done and nobody has to act, says to ignore it, or only names ${bot} in a quote, a recap or an example.`,
21520
+ all: (bot) => `The message is addressed to everyone with @all. Does it need ${bot} to act, given ${bot}'s role? Yes if it asks for something ${bot} can give, or announces something ${bot} must act on. No if it is news, or for other bots' roles. A missed request stalls the thread: when unsure, yes.`,
21521
+ reply: (bot) => `The message does not @-mention ${bot}, but replies to ${bot}'s earlier request. Does it need ${bot} to act? Yes if it hands back an answer, result or approval ${bot} asked for, or asks it something. No if it only thanks or acknowledges, says it reacted, posted nothing new or has nothing to do, says the matter is already handled or nothing is needed from ${bot}, or only points back to messages ${bot} has already seen. A missed answer stalls the thread: when unsure, yes.`
21522
+ };
21523
+ var isPick = (bots) => bots.length > 0 && bots.every((b) => b.relation === "pick");
21524
+ function pickPrompt({ author, text, tail, bots }) {
21525
+ const input2 = [
21526
+ `${author} wrote a message in a team chat thread with the bots below.`,
21527
+ "",
21528
+ "Bots in the thread (name: role):",
21529
+ "<bots>",
21530
+ bots.map((b, i) => `- ${b.name}: ${clip(b.role, 160)}${i === 0 ? " (the default)" : ""}`).join("\n"),
21531
+ "</bots>",
21532
+ "",
21533
+ "The thread so far (latest last):",
21534
+ "<thread>",
21535
+ tail.map((m) => `${m.author}: ${clip(m.text)}`).join("\n") || "(none)",
21536
+ "</thread>",
21537
+ "",
21538
+ `The message from ${author}:`,
21539
+ "<message>",
21540
+ clip(text, 6e3),
21541
+ "</message>"
21542
+ ].join("\n");
21543
+ const questions = [{ type: "choice", name: "who", instructions: `Who is this message for? ${author} writes names loosely: a misspelled or shortened name ("@arc" for archer), a name without @ ("archer, wdyt"), or a reply to what the bot who spoke last just said. Pick the one bot ${author} is addressing or answering. If it is for nobody in particular (a general question, "sitrep", a request for the status), pick ${bots[0].name}, the default.`, choices: bots.map((b) => ({ value: b.name, description: clip(b.role, 160) })) }];
21544
+ return { input: input2, questions };
21521
21545
  }
21522
- function ulid(now = Date.now()) {
21523
- return base32(BigInt(now) << 80n | BigInt(`0x${randomBytes(10).toString("hex")}`));
21546
+ function routePrompt(request) {
21547
+ if (isPick(request.bots)) return pickPrompt(request);
21548
+ const { author, text, tail, bots, asked } = request;
21549
+ const roster = bots.map((b) => `- ${b.name}: ${clip(b.role, 160)}${b.busy ? " [busy: working on another turn in this thread]" : ""}${b.relation === "named" ? " [@-mentioned in the message]" : b.relation === "all" ? " [addressed by @all]" : ""}`).join("\n");
21550
+ const input2 = [
21551
+ `A message was just posted in a team chat thread of bots and people by ${author}. For each bot below, decide whether the message needs that bot to act. A bot that acts wakes up and spends a full turn; one that isn't needed stays asleep.`,
21552
+ "",
21553
+ "Bots that could wake (name: role):",
21554
+ "<bots>",
21555
+ roster,
21556
+ "</bots>",
21557
+ "",
21558
+ "The thread so far (latest last):",
21559
+ "<thread>",
21560
+ tail.map((m) => `${m.author}: ${clip(m.text)}`).join("\n") || "(none)",
21561
+ "</thread>",
21562
+ ...asked?.length ? ["", `${author} wrote the message below as the end of a turn that answered these messages:`, "<answered>", asked.map((m) => `${m.author}: ${clip(m.text)}`).join("\n"), "</answered>"] : [],
21563
+ "",
21564
+ `The message from ${author}:`,
21565
+ "<message>",
21566
+ clip(text, 6e3),
21567
+ "</message>"
21568
+ ].join("\n");
21569
+ const questions = bots.flatMap((b) => [
21570
+ { type: "predicate", name: `wake-${b.name}`, instructions: WAKE_QUESTION[b.relation](b.name) },
21571
+ ...b.busy ? [{ type: "predicate", name: `steer-${b.name}`, instructions: `${b.name} is in the middle of a turn. Is this message urgent enough that ${b.name} must see it now, mid-turn: it changes or cancels what ${b.name} is doing, or ${b.name} can't finish without it? No if it can wait for the turn to end.` }] : []
21572
+ ]);
21573
+ return { input: input2, questions };
21524
21574
  }
21525
- var eventId = () => `evt_${ulid()}`;
21526
- function derivedMessageId(...parts) {
21527
- return `msg_${base32(BigInt(`0x${createHash("sha256").update(parts.join("\0")).digest("hex").slice(0, 33)}`) >> 2n)}`;
21575
+ var choice = (body, bots) => {
21576
+ const a = body?.answers?.find((x) => x?.name === "who");
21577
+ const bot = bots.find((b) => b.name === a?.choice)?.name, score = a?.probabilities?.find((p) => p?.value === bot)?.probability;
21578
+ return bot && typeof score === "number" ? { bot, score } : null;
21579
+ };
21580
+ var probabilities = (body) => new Map((body?.answers ?? []).flatMap((a) => typeof a?.name === "string" && typeof a.probability === "number" ? [[a.name, a.probability]] : []));
21581
+ function decisionRouter(credential, fetchImpl, config3) {
21582
+ const asked = /* @__PURE__ */ new Map();
21583
+ return (request) => {
21584
+ const key = JSON.stringify([request.messageId, request.bots.map((b) => [b.name, b.relation, b.busy])]);
21585
+ let call = asked.get(key);
21586
+ if (!call) {
21587
+ call = ask(request, credential, fetchImpl, config3);
21588
+ asked.set(key, call);
21589
+ if (asked.size > 200) asked.delete(asked.keys().next().value);
21590
+ }
21591
+ return call;
21592
+ };
21528
21593
  }
21529
- var sessionId = () => `wrun_${ulid()}`;
21530
- var deliveryId = (message, bot) => `dlv_${message}_${bot}`;
21531
- var messageOfDelivery = (delivery, bot) => delivery.slice("dlv_".length, -`_${bot}`.length);
21532
- var turnId = (session, chunkIndex) => `trn_${session}_${chunkIndex}`;
21533
-
21534
- // ../../hosted-hub/src/store/events.ts
21535
- function eventStatements(threadId2, event, text, at, id = eventId(), only) {
21536
- const expr = (v) => typeof v === "string" ? { sql: "?", args: [v] } : "sql" in v && "args" in v ? v : { sql: "?", args: [JSON.stringify(v)] };
21537
- const [t, e] = [expr(text), expr(event)];
21538
- return [
21539
- s(
21540
- `INSERT OR IGNORE INTO messages (id, thread_id, author, author_kind, text, created_at, event) SELECT ?, ?, 'system', 'event', ${t.sql}, ?, ${e.sql} WHERE ${only?.sql ?? "1"}`,
21541
- id,
21542
- threadId2,
21543
- ...t.args,
21544
- at,
21545
- ...e.args,
21546
- ...only?.args ?? []
21547
- ),
21548
- s(`INSERT INTO log (thread_id,type,data,at) SELECT ?, 'message.created', ?, ? WHERE changes() > 0`, threadId2, JSON.stringify({ threadId: threadId2, messageId: id, author: "system", authorKind: "event" }), at)
21549
- ];
21594
+ async function ask(request, credential, fetchImpl, config3) {
21595
+ const started = Date.now();
21596
+ const failed = (error63) => ({ wake: (isPick(request.bots) ? request.bots.slice(0, 1) : request.bots).map((b) => ({ bot: b.name, steer: false })), ms: Date.now() - started, error: error63 });
21597
+ if (!config3.enabled || config3.api !== "decisions") return failed("routing needs a decision model");
21598
+ const signal = AbortSignal.timeout(config3.timeoutMs);
21599
+ try {
21600
+ const token = await Promise.race([credential(), new Promise((_, reject) => signal.addEventListener("abort", () => reject(signal.reason), { once: true }))]);
21601
+ const res = await fetchImpl(config3.url, {
21602
+ method: "POST",
21603
+ signal,
21604
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
21605
+ body: JSON.stringify({ model: config3.model, ...routePrompt(request) })
21606
+ });
21607
+ if (!res.ok) return failed(`gateway ${res.status}`);
21608
+ const answer = await res.json();
21609
+ if (isPick(request.bots)) {
21610
+ const picked = choice(answer, request.bots);
21611
+ return picked ? { wake: [{ bot: picked.bot, steer: false }], pick: picked, ms: Date.now() - started } : failed("unreadable decision");
21612
+ }
21613
+ const p = probabilities(answer);
21614
+ if (request.bots.some((b) => !p.has(`wake-${b.name}`))) return failed("unreadable decision");
21615
+ return {
21616
+ wake: request.bots.filter((b) => p.get(`wake-${b.name}`) >= WAKE_AT).map((b) => ({ bot: b.name, steer: b.busy && (p.get(`steer-${b.name}`) ?? 0) >= STEER_AT })),
21617
+ ms: Date.now() - started
21618
+ };
21619
+ } catch (e) {
21620
+ return failed(String(e?.message ?? e));
21621
+ }
21550
21622
  }
21551
-
21552
- // ../../lib/usage.mjs
21553
- var emptyBucket = () => ({ steps: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, costUsd: null, unpricedSteps: 0 });
21554
- var TOKENS = ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens"];
21555
- var num = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0;
21556
- var round = (v) => Math.round(v * 1e9) / 1e9;
21557
- function addUsage(bucket, usage = {}) {
21558
- bucket.steps += 1;
21559
- for (const key of TOKENS) bucket[key] += num(usage?.[key]);
21560
- const cost = usage?.costUsd;
21561
- if (typeof cost === "number" && Number.isFinite(cost) && cost >= 0) bucket.costUsd = round((bucket.costUsd ?? 0) + cost);
21562
- else bucket.unpricedSteps += 1;
21563
- return bucket;
21623
+ function routeRequest({ messageId: messageId2, author, text, bots, facts }) {
21624
+ return { messageId: messageId2, author, text, tail: facts.tail, bots: bots.map((b) => ({ ...b, role: facts.roles.get(b.name)?.role ?? "", busy: facts.roles.get(b.name)?.busy ?? false })) };
21564
21625
  }
21565
- var HOURS_KEPT_MS = 35 * 24 * 36e5;
21566
-
21567
- // ../../hosted-hub/src/store/usage.ts
21568
- var STEP_INSERT = "INSERT OR IGNORE INTO usage_steps (session_id, sequence, step_index, at, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, cost_usd, turn_id, model)";
21569
- function stepValues(sessionId2, step) {
21570
- const b = addUsage(emptyBucket(), step.usage);
21571
- return [sessionId2, step.sequence, step.stepIndex, Date.parse(step.at), b.inputTokens, b.outputTokens, b.cacheReadTokens, b.cacheWriteTokens, b.costUsd];
21626
+ function applyRouting({ forced, bots }, routing) {
21627
+ const picked = new Map(routing.wake.map((w) => [w.bot, w]));
21628
+ const wake = bots.filter((b) => forced.has(b.name) || picked.has(b.name)).map((b) => ({ bot: b.name, steer: !forced.has(b.name) && Boolean(picked.get(b.name)?.steer) }));
21629
+ const woken = new Set(wake.map((w) => w.bot));
21630
+ return { wake, skipped: bots.filter((b) => !woken.has(b.name)).map((b) => b.name) };
21572
21631
  }
21573
-
21574
- // ../../hosted-hub/src/store/mention-gate.ts
21575
- var GATE = gateConfig(process.env);
21632
+ function logRouting(request, routed, routing, gatherMs) {
21633
+ console.log(JSON.stringify({ at: "route", message: request.messageId, author: request.author, gatherMs, wake: routed.wake, skipped: routed.skipped, modelMs: routing.ms, ...routing.pick ? { pick: routing.pick } : {}, ...routing.error ? { error: routing.error } : {} }));
21634
+ }
21635
+ var ROUTE_BOTS_COLUMN = `(SELECT json_group_array(json_object('name', b.name, 'role', b.role, 'busy', EXISTS (SELECT 1 FROM turns t WHERE t.thread_id = sess.thread_id AND t.bot = b.name AND t.status IN ${OPEN_TURN_STATUSES}))) FROM bots b, sess) AS route_bots`;
21576
21636
 
21577
21637
  // ../../hosted-hub/src/store/closing-check.ts
21578
21638
  var TAIL_COLUMN = `(SELECT json_group_array(json_object('author', author, 'text', text)) FROM (SELECT author, text FROM messages
21579
21639
  WHERE thread_id = (SELECT thread_id FROM sess) AND author_kind IN ('human', 'agent') AND status IS NULL ORDER BY created_at DESC LIMIT 6)) AS tail`;
21580
21640
  var DROP_BELOW = 0.05;
21581
- var CLIP = 600;
21582
- var clip = (t, n = CLIP) => t.length > n ? `${t.slice(0, n)} \u2026` : t;
21641
+ var CLIP2 = 600;
21642
+ var clip2 = (t, n = CLIP2) => t.length > n ? `${t.slice(0, n)} \u2026` : t;
21583
21643
  function closingPrompt({ bot, text, tail, posted, asked }) {
21584
- const lines = (items) => items.map((m) => `${m.author ? `${m.author}: ` : "- "}${clip(m.text)}`).join("\n") || "(none)";
21644
+ const lines = (items) => items.map((m) => `${m.author ? `${m.author}: ` : "- "}${clip2(m.text)}`).join("\n") || "(none)";
21585
21645
  const input2 = [
21586
21646
  `${bot} is a bot in a team chat thread of bots and people. Its turn just ended with the closing text below, which will post to the thread as a message unless it adds nothing.`,
21587
21647
  "",
@@ -21622,14 +21682,14 @@ function decisionJudge(credential, fetchImpl = fetch, config3) {
21622
21682
  const key = `${request.turnId}:${request.text.length}`;
21623
21683
  let call = asked.get(key);
21624
21684
  if (!call) {
21625
- call = ask(request, credential, fetchImpl, config3);
21685
+ call = ask2(request, credential, fetchImpl, config3);
21626
21686
  asked.set(key, call);
21627
21687
  if (asked.size > 200) asked.delete(asked.keys().next().value);
21628
21688
  }
21629
21689
  return call;
21630
21690
  };
21631
21691
  }
21632
- async function ask(request, credential, fetchImpl, config3) {
21692
+ async function ask2(request, credential, fetchImpl, config3) {
21633
21693
  const started = Date.now();
21634
21694
  const done = (c) => {
21635
21695
  const closing = { ...c, ms: Date.now() - started };
@@ -22274,7 +22334,7 @@ __export(external_exports, {
22274
22334
  tuple: () => tuple,
22275
22335
  uint32: () => uint32,
22276
22336
  uint64: () => uint64,
22277
- ulid: () => ulid3,
22337
+ ulid: () => ulid2,
22278
22338
  undefined: () => _undefined3,
22279
22339
  union: () => union,
22280
22340
  unknown: () => unknown,
@@ -23998,7 +24058,7 @@ __export(regexes_exports, {
23998
24058
  sha512_hex: () => sha512_hex,
23999
24059
  string: () => string,
24000
24060
  time: () => time,
24001
- ulid: () => ulid2,
24061
+ ulid: () => ulid,
24002
24062
  undefined: () => _undefined,
24003
24063
  unicodeEmail: () => unicodeEmail,
24004
24064
  uppercase: () => uppercase,
@@ -24010,7 +24070,7 @@ __export(regexes_exports, {
24010
24070
  });
24011
24071
  var cuid = /^[cC][0-9a-z]{6,}$/;
24012
24072
  var cuid2 = /^[0-9a-z]+$/;
24013
- var ulid2 = /^[0-7][0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{25}$/;
24073
+ var ulid = /^[0-7][0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{25}$/;
24014
24074
  var xid = /^[0-9a-vA-V]{20}$/;
24015
24075
  var ksuid = /^[A-Za-z0-9]{27}$/;
24016
24076
  var nanoid = /^[a-zA-Z0-9_-]{21}$/;
@@ -24949,7 +25009,7 @@ var $ZodCUID2 = /* @__PURE__ */ $constructor("$ZodCUID2", (inst, def) => {
24949
25009
  $ZodStringFormat.init(inst, def);
24950
25010
  });
24951
25011
  var $ZodULID = /* @__PURE__ */ $constructor("$ZodULID", (inst, def) => {
24952
- def.pattern ?? (def.pattern = ulid2);
25012
+ def.pattern ?? (def.pattern = ulid);
24953
25013
  $ZodStringFormat.init(inst, def);
24954
25014
  });
24955
25015
  var $ZodXID = /* @__PURE__ */ $constructor("$ZodXID", (inst, def) => {
@@ -39167,7 +39227,7 @@ __export(schemas_exports2, {
39167
39227
  tuple: () => tuple,
39168
39228
  uint32: () => uint32,
39169
39229
  uint64: () => uint64,
39170
- ulid: () => ulid3,
39230
+ ulid: () => ulid2,
39171
39231
  undefined: () => _undefined3,
39172
39232
  union: () => union,
39173
39233
  unknown: () => unknown,
@@ -39701,7 +39761,7 @@ var ZodULID = /* @__PURE__ */ $constructor("ZodULID", (inst, def) => {
39701
39761
  $ZodULID.init(inst, def);
39702
39762
  ZodStringFormat.init(inst, def);
39703
39763
  });
39704
- function ulid3(params) {
39764
+ function ulid2(params) {
39705
39765
  return _ulid(ZodULID, params);
39706
39766
  }
39707
39767
  var ZodXID = /* @__PURE__ */ $constructor("ZodXID", (inst, def) => {
@@ -41747,6 +41807,47 @@ function date4(params) {
41747
41807
  return _coercedDate(ZodDate, params);
41748
41808
  }
41749
41809
 
41810
+ // ../../hosted-hub/src/domain/ids.ts
41811
+ import { createHash, randomBytes } from "node:crypto";
41812
+ var alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
41813
+ function base32(value) {
41814
+ let out = "";
41815
+ for (let i = 0; i < 26; i++) {
41816
+ out = alphabet[Number(value & 31n)] + out;
41817
+ value >>= 5n;
41818
+ }
41819
+ return out;
41820
+ }
41821
+ function ulid3(now = Date.now()) {
41822
+ return base32(BigInt(now) << 80n | BigInt(`0x${randomBytes(10).toString("hex")}`));
41823
+ }
41824
+ var eventId = () => `evt_${ulid3()}`;
41825
+ function derivedMessageId(...parts) {
41826
+ return `msg_${base32(BigInt(`0x${createHash("sha256").update(parts.join("\0")).digest("hex").slice(0, 33)}`) >> 2n)}`;
41827
+ }
41828
+ var sessionId = () => `wrun_${ulid3()}`;
41829
+ var deliveryId = (message, bot) => `dlv_${message}_${bot}`;
41830
+ var messageOfDelivery = (delivery, bot) => delivery.slice("dlv_".length, -`_${bot}`.length);
41831
+ var turnId = (session, chunkIndex) => `trn_${session}_${chunkIndex}`;
41832
+
41833
+ // ../../hosted-hub/src/store/events.ts
41834
+ function eventStatements(threadId2, event, text, at, id = eventId(), only) {
41835
+ const expr = (v) => typeof v === "string" ? { sql: "?", args: [v] } : "sql" in v && "args" in v ? v : { sql: "?", args: [JSON.stringify(v)] };
41836
+ const [t, e] = [expr(text), expr(event)];
41837
+ return [
41838
+ s(
41839
+ `INSERT OR IGNORE INTO messages (id, thread_id, author, author_kind, text, created_at, event) SELECT ?, ?, 'system', 'event', ${t.sql}, ?, ${e.sql} WHERE ${only?.sql ?? "1"}`,
41840
+ id,
41841
+ threadId2,
41842
+ ...t.args,
41843
+ at,
41844
+ ...e.args,
41845
+ ...only?.args ?? []
41846
+ ),
41847
+ s(`INSERT INTO log (thread_id,type,data,at) SELECT ?, 'message.created', ?, ? WHERE changes() > 0`, threadId2, JSON.stringify({ threadId: threadId2, messageId: id, author: "system", authorKind: "event" }), at)
41848
+ ];
41849
+ }
41850
+
41750
41851
  // ../../hosted-hub/src/store/routing.ts
41751
41852
  var PARTICIPANTS = (thread, except = "''") => `SELECT b.name AS bot FROM bots b, (SELECT ${thread} AS t, ${except} AS x) p WHERE MAX(
41752
41853
  COALESCE((SELECT created_at FROM sessions WHERE thread_id = p.t AND bot = b.name ORDER BY created_at DESC LIMIT 1), -2),
@@ -41782,7 +41883,7 @@ function routingStatements(messageId2, threadId2, bots, now, { by, joins }, held
41782
41883
  bot,
41783
41884
  messageId2
41784
41885
  ),
41785
- ...steer ? [s("UPDATE deliveries SET steer = 1 WHERE id = ? AND accepted_at IS NULL", deliveryId(messageId2, bot))] : [],
41886
+ ...steer === true || steer && steer.has(bot) ? [s("UPDATE deliveries SET steer = 1 WHERE id = ? AND accepted_at IS NULL", deliveryId(messageId2, bot))] : [],
41786
41887
  ...held[bot] ? [s("UPDATE deliveries SET held_at = ?, held_scope = ? WHERE id = ? AND accepted_at IS NULL", now, held[bot], deliveryId(messageId2, bot))] : []
41787
41888
  ])
41788
41889
  ];
@@ -41804,13 +41905,6 @@ var HOP_LIMIT = "(SELECT json_extract(metadata, '$.workspace.hopLimit') FROM wor
41804
41905
  var heldByHopLimit = (m) => `(${m}.author_kind = 'agent' AND ${HOP_LIMIT} IS NOT NULL AND ${m}.hop >= ${HOP_LIMIT})`;
41805
41906
  var ToolInfo = external_exports.object({ tools: external_exports.object({ static: external_exports.array(external_exports.object({ name: external_exports.string().min(1) })) }) });
41806
41907
 
41807
- // ../../hosted-hub/src/store/mentionable.ts
41808
- var NOW_MS = `(CAST(strftime('%s', 'now') AS INTEGER) * 1000)`;
41809
- var inRoster = `NOT (runs_on = 'local' AND EXISTS (SELECT 1 FROM local_runner l, workspace_repo w
41810
- WHERE l.id = 1 AND w.id = 1 AND l.setup_failed_commit = json_extract(w.metadata, '$.commit') AND l.setup_failed_until > ${NOW_MS}))`;
41811
- var LEASE_LEFT = `SELECT l.setup_failed_until - ${NOW_MS} AS ms FROM local_runner l, workspace_repo w
41812
- WHERE l.id = 1 AND w.id = 1 AND l.setup_failed_commit = json_extract(w.metadata, '$.commit') AND l.setup_failed_until > ${NOW_MS}`;
41813
-
41814
41908
  // ../../hosted-hub/src/domain/wake.ts
41815
41909
  var mentioned = (roster, text) => {
41816
41910
  const tokens = new Set(mentionTokens(text));
@@ -41827,6 +41921,58 @@ function recipients(roster, text, from, threadId2 = "", participants = []) {
41827
41921
  return named.length ? named : roster.slice(0, 1);
41828
41922
  }
41829
41923
 
41924
+ // ../../hosted-hub/src/store/fold-route.ts
41925
+ var relationsOf = (roster, text, me, bots, replyTo) => {
41926
+ const named = new Set(mentioned(roster, text).filter((b) => b !== me));
41927
+ return bots.map((name) => ({ name, relation: named.has(name) ? "named" : hasAll(text) && !replyTo.has(name) ? "all" : "reply" }));
41928
+ };
41929
+ function routedWake(t, messageId2, text, bots, answers, asks) {
41930
+ const all = bots.map((b) => b.name), none = { to: all, steer: /* @__PURE__ */ new Set(), stmts: [] };
41931
+ if (!t.facts || !bots.length || t.thread.startsWith("dm-")) return none;
41932
+ const answer = answers?.get(messageId2);
41933
+ if (!answer) {
41934
+ asks.push({ messageId: messageId2, request: { ...routeRequest({ messageId: messageId2, author: t.me, text, bots, facts: { ...t.facts, tail: t.tail } }), asked: t.asked.map(({ author, text: text2 }) => ({ author, text: text2 })) } });
41935
+ return none;
41936
+ }
41937
+ const routed = applyRouting({ forced: /* @__PURE__ */ new Set(), bots }, answer);
41938
+ logRouting({ messageId: messageId2, author: t.me }, routed, answer, t.facts.gatherMs);
41939
+ return {
41940
+ to: routed.wake.map((w) => w.bot),
41941
+ steer: new Set(routed.wake.filter((w) => w.steer).map((w) => w.bot)),
41942
+ stmts: routed.skipped.flatMap((bot) => eventStatements(t.thread, { kind: "mention_skip", agent: bot, by: t.me, message: messageId2 }, `${bot} skipped: needs nothing from it`, t.at, derivedMessageId(messageId2, "skip", bot), s("EXISTS (SELECT 1 FROM messages WHERE id = ?)", messageId2)))
41943
+ };
41944
+ }
41945
+ var wakeStatements = (t, messageId2, to, steer) => routingStatements(messageId2, t.thread, [...to], t.at, { by: t.me, joins: true }, {}, steer);
41946
+
41947
+ // ../../hosted-hub/src/store/mentionable.ts
41948
+ var NOW_MS = `(CAST(strftime('%s', 'now') AS INTEGER) * 1000)`;
41949
+ var inRoster = `NOT (runs_on = 'local' AND EXISTS (SELECT 1 FROM local_runner l, workspace_repo w
41950
+ WHERE l.id = 1 AND w.id = 1 AND l.setup_failed_commit = json_extract(w.metadata, '$.commit') AND l.setup_failed_until > ${NOW_MS}))`;
41951
+ var LEASE_LEFT = `SELECT l.setup_failed_until - ${NOW_MS} AS ms FROM local_runner l, workspace_repo w
41952
+ WHERE l.id = 1 AND w.id = 1 AND l.setup_failed_commit = json_extract(w.metadata, '$.commit') AND l.setup_failed_until > ${NOW_MS}`;
41953
+
41954
+ // ../../lib/usage.mjs
41955
+ var emptyBucket = () => ({ steps: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, costUsd: null, unpricedSteps: 0 });
41956
+ var TOKENS = ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens"];
41957
+ var num = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0;
41958
+ var round = (v) => Math.round(v * 1e9) / 1e9;
41959
+ function addUsage(bucket, usage = {}) {
41960
+ bucket.steps += 1;
41961
+ for (const key of TOKENS) bucket[key] += num(usage?.[key]);
41962
+ const cost = usage?.costUsd;
41963
+ if (typeof cost === "number" && Number.isFinite(cost) && cost >= 0) bucket.costUsd = round((bucket.costUsd ?? 0) + cost);
41964
+ else bucket.unpricedSteps += 1;
41965
+ return bucket;
41966
+ }
41967
+ var HOURS_KEPT_MS = 35 * 24 * 36e5;
41968
+
41969
+ // ../../hosted-hub/src/store/usage.ts
41970
+ var STEP_INSERT = "INSERT OR IGNORE INTO usage_steps (session_id, sequence, step_index, at, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, cost_usd, turn_id, model)";
41971
+ function stepValues(sessionId2, step) {
41972
+ const b = addUsage(emptyBucket(), step.usage);
41973
+ return [sessionId2, step.sequence, step.stepIndex, Date.parse(step.at), b.inputTokens, b.outputTokens, b.cacheReadTokens, b.cacheWriteTokens, b.costUsd];
41974
+ }
41975
+
41830
41976
  // ../../hosted-hub/src/store/notifications.ts
41831
41977
  var NOTIFICATION_READS = s("SELECT person, notification_id FROM notification_reads");
41832
41978
  var NOTIFICATIONS = s("SELECT message_id, person FROM notifications");
@@ -48729,9 +48875,9 @@ var TURN_END_SQL = `SELECT * FROM (WITH sess AS (SELECT * FROM sessions WHERE id
48729
48875
  AND (EXISTS (SELECT 1 FROM turn_deliveries td WHERE td.delivery_id = dd.id AND td.turn_id = (SELECT id FROM open_turn)) OR NOT EXISTS (SELECT 1 FROM turn_deliveries td WHERE td.delivery_id = dd.id))) AS asked,
48730
48876
  (SELECT json_group_array(p.text) FROM messages p WHERE p.turn_id = (SELECT id FROM open_turn)
48731
48877
  AND p.author = (SELECT bot FROM sess) AND p.author_kind = 'agent' AND p.status IS NULL) AS posted,
48732
- (SELECT p.id FROM messages p WHERE p.turn_id = (SELECT id FROM open_turn)
48878
+ (SELECT json_object('id', p.id, 'text', p.text) FROM messages p WHERE p.turn_id = (SELECT id FROM open_turn)
48733
48879
  AND p.author = (SELECT bot FROM sess) AND p.author_kind = 'agent' AND p.status IS NULL ORDER BY p.created_at DESC, p.rowid DESC LIMIT 1) AS last_post,
48734
- ${TAIL_COLUMN})`;
48880
+ ${TAIL_COLUMN}, ${ROUTE_BOTS_COLUMN})`;
48735
48881
  var namedDeliveries = (chunks) => [...new Set(chunks.flatMap((c) => c.events.flatMap((e) => e.meta?.deliveryIds ?? [])))];
48736
48882
  var completes = (chunks) => chunks.some((c) => c.events.some((e) => e.type === "turn.completed"));
48737
48883
  async function readFoldState(get, sessionId2, chunks) {
@@ -48749,7 +48895,8 @@ function foldState(row) {
48749
48895
  asked: JSON.parse(String(row.asked ?? "[]")),
48750
48896
  posted: JSON.parse(String(row.posted ?? "[]")),
48751
48897
  tail: row.tail == null ? [] : JSON.parse(String(row.tail)).reverse(),
48752
- lastPost: row.last_post == null ? null : String(row.last_post),
48898
+ ...row.route_bots == null ? {} : { facts: { roles: new Map(JSON.parse(String(row.route_bots)).map((b) => [b.name, { role: b.role, busy: Number(b.busy) === 1 }])), tail: [] } },
48899
+ lastPost: row.last_post == null ? null : JSON.parse(String(row.last_post)),
48753
48900
  notify: row.notify ? JSON.parse(String(row.notify)) : null,
48754
48901
  foldedIndex: Number(row.folded_index),
48755
48902
  open: row.open_turn ? JSON.parse(String(row.open_turn)) : null
@@ -48760,13 +48907,13 @@ var REVOKED_TURN = `EXISTS (SELECT 1 FROM turn_deliveries td JOIN deliveries d O
48760
48907
  AND turns.id IS NOT (SELECT archived_turn FROM thread_meta WHERE thread_id = turns.thread_id)`;
48761
48908
  var IF_INSERTED = "WHERE changes() > 0";
48762
48909
  var lastActWasComms = (ls) => !!ls && Object.keys(ls.calls).length > 0 && Object.values(ls.calls).every((own3) => own3 === true);
48763
- function fold(state, chunks, now = Date.now(), verdicts = /* @__PURE__ */ new Map()) {
48910
+ function fold(state, chunks, now = Date.now(), answers = {}) {
48764
48911
  const { session } = state;
48765
48912
  const stmts = [], touched = /* @__PURE__ */ new Map(), named = /* @__PURE__ */ new Set();
48766
48913
  const log = (type, data, when = "") => stmts.push(s(`INSERT INTO log (thread_id, type, data, at) SELECT ?, ?, ?, ? ${when}`, session.thread, type, JSON.stringify({ threadId: session.thread, agent: session.bot, ...data }), at));
48767
48914
  let open3 = state.open && { ...state.open, trigger: state.open.trigger ?? null, lastStep: state.open.lastStep ?? null, status: state.open.status, endedAt: null, error: null };
48768
48915
  const deliver = [];
48769
- const closings = [];
48916
+ const closings = [], routeAsks = [];
48770
48917
  let last = state.foldedIndex;
48771
48918
  let at = now;
48772
48919
  for (const { index, events } of chunks) {
@@ -48922,7 +49069,7 @@ function fold(state, chunks, now = Date.now(), verdicts = /* @__PURE__ */ new Ma
48922
49069
  flush();
48923
49070
  let text = lastActWasComms(open3.lastStep) ? "" : open3.text.trim();
48924
49071
  if (text && open3.status !== "cancelling") {
48925
- const drop = verdicts.get(open3.id);
49072
+ const drop = answers.closing?.get(open3.id);
48926
49073
  if (drop === void 0) closings.push({ turnId: open3.id, bot: session.bot, text, tail: state.tail ?? [], posted: state.posted ?? [], asked: (state.asked ?? []).map(({ author, text: text2 }) => ({ author, text: text2 })) });
48927
49074
  else if (drop) {
48928
49075
  stmts.push(...eventStatements(session.thread, { kind: "closing_skip", agent: session.bot, by: session.bot }, `${session.bot} skipped its closing text: nothing new`, at, derivedMessageId(open3.id, "closing-skip"), s("EXISTS (SELECT 1 FROM turns WHERE id = ? AND status NOT IN ('cancelling','cancelled'))", open3.id)));
@@ -48939,13 +49086,16 @@ function fold(state, chunks, now = Date.now(), verdicts = /* @__PURE__ */ new Ma
48939
49086
  stmts.push(...notificationFor(messageId2, "agent", text, state.notify ?? void 0));
48940
49087
  stmts.push(s(`INSERT OR IGNORE INTO message_attachments (message_id, position, upload_id, name, size)
48941
49088
  SELECT ?, position, upload_id, name, size FROM turn_attachments WHERE turn_id = ? AND EXISTS (SELECT 1 FROM messages WHERE id = ?) ORDER BY position`, messageId2, open3.id, messageId2));
48942
- const onPost = text === "" ? state.lastPost : null;
48943
- const to = [.../* @__PURE__ */ new Set([...recipients(state.roster, text, { name: session.bot, kind: "agent" }, session.thread, state.participants), ...onPost ? [] : handedBack(state, text, named)])];
48944
- stmts.push(...routingStatements(messageId2, session.thread, to, at, { by: session.bot, joins: true }));
49089
+ const me = session.bot, turn = { me, thread: session.thread, roster: state.roster, facts: state.facts && { ...state.facts, tail: state.tail ?? [] }, tail: state.tail ?? [], asked: state.asked ?? [], at };
49090
+ const onPost = text === "" ? state.lastPost : null, replyTo = new Set((state.asked ?? []).map((a) => a.author));
49091
+ const base = [.../* @__PURE__ */ new Set([...recipients(state.roster, text, { name: me, kind: "agent" }, session.thread, state.participants), ...onPost ? [] : handedBack(state, text, named)])];
49092
+ const first = text ? routedWake(turn, messageId2, text, relationsOf(state.roster, text, me, base, replyTo), answers.routes, routeAsks) : { to: base, steer: /* @__PURE__ */ new Set(), stmts: [] };
49093
+ stmts.push(...first.stmts, ...wakeStatements(turn, messageId2, first.to, first.steer));
48945
49094
  deliver.push(messageId2);
48946
49095
  if (onPost) {
48947
- stmts.push(...routingStatements(onPost, session.thread, handedBack(state, "", named), at, { by: session.bot, joins: true }));
48948
- deliver.push(onPost);
49096
+ const back = routedWake(turn, onPost.id, onPost.text, handedBack(state, "", named).map((name) => ({ name, relation: "reply" })), answers.routes, routeAsks);
49097
+ stmts.push(...back.stmts, ...wakeStatements(turn, onPost.id, back.to, back.steer));
49098
+ deliver.push(onPost.id);
48949
49099
  }
48950
49100
  }
48951
49101
  end("done");
@@ -49000,17 +49150,20 @@ function fold(state, chunks, now = Date.now(), verdicts = /* @__PURE__ */ new Ma
49000
49150
  stmts.push(s(`INSERT INTO session_folds (session_id, folded_index, folded_at) VALUES (?, ?, ?)
49001
49151
  ON CONFLICT (session_id) DO UPDATE SET folded_index = excluded.folded_index, folded_at = excluded.folded_at`, session.id, last, now));
49002
49152
  }
49003
- return { statements: stmts, deliver, ...closings.length ? { closings } : {} };
49153
+ return { statements: stmts, deliver, ...closings.length ? { closings } : {}, ...routeAsks.length ? { routes: routeAsks } : {} };
49004
49154
  }
49005
- async function foldJudged(state, chunks, now, judge2) {
49155
+ async function foldJudged(state, chunks, now, judges2 = {}) {
49006
49156
  const first = fold(structuredClone(state), chunks, now);
49007
- if (!judge2 || !first.closings) return first;
49008
- const verdicts = await Promise.all(first.closings.map(async (c) => [c.turnId, (await judge2(c)).drop]));
49009
- return fold(state, chunks, now, new Map(verdicts));
49010
- }
49011
- async function foldInTx(tx, sessionId2, chunks, judge2) {
49157
+ const [closing, routes] = await Promise.all([
49158
+ Promise.all((judges2.closing ? first.closings ?? [] : []).map(async (c) => [c.turnId, (await judges2.closing(c)).drop])),
49159
+ Promise.all((judges2.router ? first.routes ?? [] : []).map(async (r) => [r.messageId, await judges2.router(r.request)]))
49160
+ ]);
49161
+ if (!closing.length && !routes.length) return first;
49162
+ return fold(state, chunks, now, { closing: new Map(closing), routes: new Map(routes) });
49163
+ }
49164
+ async function foldInTx(tx, sessionId2, chunks, judges2) {
49012
49165
  const state = await readFoldState((sql, ...args) => tx.get(sql, ...args), sessionId2, chunks);
49013
- const result = state ? await foldJudged(state, chunks, Date.now(), judge2) : foldSubagent(subagentState(sessionId2, await tx.get(SUBAGENT_STATE_SQL, sessionId2)), chunks);
49166
+ const result = state ? await foldJudged(state, chunks, Date.now(), judges2) : foldSubagent(subagentState(sessionId2, await tx.get(SUBAGENT_STATE_SQL, sessionId2)), chunks);
49014
49167
  for (const stmt of result.statements) await tx.write(stmt.sql, ...stmt.args);
49015
49168
  return result.deliver;
49016
49169
  }
@@ -49048,11 +49201,11 @@ function foldSubagent(state, chunks, now = Date.now()) {
49048
49201
  }
49049
49202
 
49050
49203
  // ../../hosted-hub/bots/world.ts
49051
- var judge = decisionJudge(import_oidc.getVercelOidcToken, fetch, GATE);
49204
+ var judges = { closing: decisionJudge(import_oidc.getVercelOidcToken, fetch, GATE), router: decisionRouter(import_oidc.getVercelOidcToken, fetch, GATE) };
49052
49205
  async function foldAppend(tx, { runId, name, chunks, firstIndex }) {
49053
49206
  if (name !== sessionCodec.streamName(runId)) return;
49054
49207
  const decoded = await Promise.all(chunks.map(async (c, k2) => ({ index: firstIndex + k2, events: await sessionCodec.decode(c) })));
49055
- return foldInTx(tx, runId, decoded, judge);
49208
+ return foldInTx(tx, runId, decoded, judges);
49056
49209
  }
49057
49210
  async function createWorld() {
49058
49211
  const url2 = process.env.TURSO_DATABASE_URL;
@@ -1,2 +0,0 @@
1
- declare const _default: import("eve/hooks").HookDefinition<"message.received">;
2
- export default _default;
@@ -1,31 +0,0 @@
1
- import { getVercelOidcToken } from "@vercel/oidc";
2
- import { defineHook } from "eve/hooks";
3
- import { registryBase, registryFetch } from "../registry-fetch.mjs";
4
- import { credentialCache, hubCredentialLoader, screenInbound } from "../mention-gate.mjs";
5
- // The mention gate, on the bot's side: before the main model runs a turn an
6
- // @-mention or a DM woke, a cheap model decides from this bot's instructions
7
- // and the delivery whether a reply is needed. On a skip the hub posts
8
- // "<bot> skipped: <reason>" and the turn is cancelled before any model call.
9
- // Any failure answers. The hub names the bot from its session and serves its
10
- // instructions, so a deployed bot and a Mac bot run the same code. Its AI Gateway
11
- // credential is an OIDC token for the workspace's bots project: a deployed bot
12
- // carries its own, and a Mac bot has none, so it asks the hub for one.
13
- // See packages/agent-kit/mention-gate.mjs.
14
- const deploymentToken = async () => ({ ok: true, token: await getVercelOidcToken() });
15
- const credential = credentialCache(process.env.VERCEL ? deploymentToken : hubCredentialLoader(registryFetch));
16
- export default defineHook({
17
- events: {
18
- async "message.received"(event, ctx) {
19
- if (ctx.session.parent || !(process.env.VERCEL || process.env.HUB_TOKEN))
20
- return; // subagents are never gated; nor is a bot with no hosted hub
21
- const { cancel } = await screenInbound({
22
- text: event.data.message,
23
- sessionId: ctx.session.id,
24
- registryUrl: registryBase(),
25
- credential,
26
- });
27
- if (cancel)
28
- ctx.cancel();
29
- },
30
- },
31
- });