eve-hub 0.6.51 → 0.6.52

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.6.52
4
+
5
+ ### Patch Changes
6
+
7
+ - The mention gate runs on deployed bots, not only Mac bots: the hook ships in the deployed kit, the hub names the bot from its session and sends its role, and the model credential comes from the hub. It reads the message id of a hosted hub's delivery, so a mention that asks nothing of the bot is skipped.
8
+
3
9
  ## 0.6.51
4
10
 
5
11
  ### Patch Changes
@@ -1,29 +1,23 @@
1
1
  import { defineHook } from "eve/hooks";
2
- import { registryBase } from "../registry-fetch.mjs";
3
- import { hubPath } from "../hub-path.mjs";
4
- import { botInstructions, credentialCache, gatewayCredentialLoader, screenInbound } from "../mention-gate.mjs";
2
+ import { registryBase, registryFetch } from "../registry-fetch.mjs";
3
+ import { credentialCache, hubCredentialLoader, screenInbound } from "../mention-gate.mjs";
5
4
  // The mention gate, on the bot's side: before the main model runs a turn an
6
5
  // @-mention or a DM woke, a cheap model decides from this bot's instructions
7
6
  // and the delivery whether a reply is needed. On a skip the hub posts
8
7
  // "<bot> skipped: <reason>" and the turn is cancelled before any model call.
9
- // Any failure answers. See packages/agent-kit/mention-gate.mjs.
10
- const credential = credentialCache(async () => {
11
- const resolver = hubPath("node_modules/eve/dist/src/internal/model-auth/gateway-credential.js", { probe: "" });
12
- return gatewayCredentialLoader(resolver)();
13
- });
8
+ // Any failure answers. The hub names the bot from its session and serves its
9
+ // instructions and the model credential, so a deployed bot and a Mac bot run
10
+ // the same code. See packages/agent-kit/mention-gate.mjs.
11
+ const credential = credentialCache(hubCredentialLoader(registryFetch));
14
12
  export default defineHook({
15
13
  events: {
16
14
  async "message.received"(event, ctx) {
17
- const agent = process.env.HUB_AGENT;
18
- if (!agent || ctx.session.parent)
19
- return; // subagents are never gated
15
+ if (ctx.session.parent || !process.env.HUB_TOKEN)
16
+ return; // subagents are never gated; nor is a bot with no hosted hub
20
17
  const { cancel } = await screenInbound({
21
18
  text: event.data.message,
22
19
  sessionId: ctx.session.id,
23
- turnId: event.data.turnId,
24
- agent,
25
20
  registryUrl: registryBase(),
26
- instructions: botInstructions(agent),
27
21
  credential,
28
22
  });
29
23
  if (cancel)
@@ -0,0 +1,6 @@
1
+ export type GateConfig = { enabled: boolean; model: string; api: 'chat' | 'decisions'; reasoningEffort: string; timeoutMs: number; threshold: number; incidentalThreshold: number; url: string };
2
+ export function gateConfig(env?: Record<string, string | undefined>, file?: unknown): GateConfig;
3
+ export function opensWithHandle(text: string, agent: string): boolean;
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
+ export function hubCredentialLoader(fetchHub: (path: string, init: { headers?: Record<string, string> }) => Promise<Response>): (opts?: { refresh?: boolean }) => Promise<{ ok: boolean; token?: string; teamId?: string | null; error?: string }>;
package/mention-gate.mjs CHANGED
@@ -22,9 +22,7 @@
22
22
  // HUB_MENTION_GATE_URL (tests).
23
23
  import { codeRanges } from "./mention-tokens.mjs";
24
24
  import { registryHeaders } from "./registry-fetch.mjs";
25
- import { existsSync, readFileSync } from "node:fs";
26
- import { join } from "node:path";
27
- import { pathToFileURL } from "node:url";
25
+ import { createHash } from "node:crypto";
28
26
 
29
27
  export { codeRanges };
30
28
 
@@ -122,8 +120,10 @@ const clip = (text, n) => {
122
120
  };
123
121
 
124
122
  // The id of the message marked → in a delivery (registry envelope()).
123
+ // A hosted hub's delivery is one message: `[thread <id> "<title>"] [<message id>] <author>: …`.
125
124
  export function triggerId(envelope) {
126
- return String(envelope ?? "").match(/^→ \[([^\]\s]+)\]/m)?.[1] ?? null;
125
+ const text = String(envelope ?? "");
126
+ return text.match(/^→ \[([^\]\s]+)\]/m)?.[1] ?? text.match(/^\[thread \S+ "(?:[^"\\]|\\.)*"\] \[([^\]\s]+)\] /m)?.[1] ?? null;
127
127
  }
128
128
 
129
129
  // The delivery's closing address line ("[you were addressed by @x. … @x
@@ -380,7 +380,7 @@ export function credentialCache(load, { ttlMs = 10 * 60e3, failMs = 60e3, now =
380
380
  const get = async ({ refresh = false } = {}) => {
381
381
  if (cached && !refresh && now() < cached.until) return cached.value;
382
382
  inflight ??= Promise.resolve()
383
- .then(load)
383
+ .then(() => load({ refresh }))
384
384
  .catch((error) => ({ ok: false, error: String(error?.message ?? error) }))
385
385
  .then((r) => {
386
386
  const value = r?.ok && r.token ? { token: r.token, teamId: r.teamId ?? null } : null;
@@ -410,20 +410,23 @@ async function hubCall(fetchImpl, url, body) {
410
410
  // end cancelled). Anything that fails on the way answers.
411
411
  // text: the delivery (message.received's data.message)
412
412
  // credential: ({ refresh }) → { token, teamId? } | null (credentialCache)
413
- export async function screenInbound({ text, sessionId, turnId, agent, registryUrl, instructions = "", credential, fetchImpl = fetch, log = console.warn }) {
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 }) {
414
415
  const messageId = triggerId(text);
415
- if (!agent || !registryUrl || !messageId) return { cancel: false };
416
+ if (!registryUrl || !messageId) return { cancel: false };
416
417
  let check;
417
418
  try {
418
- check = await hubCall(fetchImpl, `${registryUrl}/api/mention-gate/check`, { agent, messageId, sessionId });
419
+ check = await hubCall(fetchImpl, `${registryUrl}/api/mention-gate/check`, { ...(agent ? { agent } : {}), messageId, sessionId });
419
420
  } catch (error) {
420
421
  log?.(`[mention-gate] check for ${messageId} failed, answering: ${error?.message ?? error}`);
421
422
  return { cancel: false };
422
423
  }
423
424
  if (!check?.gate || !check.config) return { cancel: false };
425
+ agent ??= check.agent;
426
+ instructions ||= check.instructions ?? "";
424
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 });
425
428
  try {
426
- const ack = await hubCall(fetchImpl, `${registryUrl}/api/mention-gate/report`, { agent, messageId, sessionId, turnId, skip: r.skip, reason: r.reason, verdict: r.verdict ?? null, usage: r.usage ?? null, ms: r.ms, error: r.error ?? null });
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 });
427
430
  return { cancel: Boolean(r.skip && ack?.skip), result: r };
428
431
  } catch (error) {
429
432
  log?.(`[mention-gate] report for ${messageId} failed, answering: ${error?.message ?? error}`);
@@ -431,31 +434,15 @@ export async function screenInbound({ text, sessionId, turnId, agent, registryUr
431
434
  }
432
435
  }
433
436
 
434
- // The bot's own instructions: agents/<name>/agent/instructions.md in the
435
- // workspace (role and what it owns), else its role line. Read once.
436
- const instructionsCache = new Map();
437
- export function botInstructions(agent, { workspace = process.env.HUB_WORKSPACE } = {}) {
438
- if (instructionsCache.has(agent)) return instructionsCache.get(agent);
439
- let text = "";
440
- try {
441
- const file = join(workspace, "agents", agent, "agent", "instructions.md");
442
- if (existsSync(file)) text = readFileSync(file, "utf8");
443
- if (!text.trim()) {
444
- const data = JSON.parse(readFileSync(join(workspace, "hub.workspace.json"), "utf8"));
445
- const role = data?.agents?.find?.((a) => a.name === agent)?.role;
446
- if (role) text = `You are **${agent}**. ${role}`;
447
- }
448
- } catch {}
449
- instructionsCache.set(agent, text);
450
- return text;
451
- }
452
-
453
- // The AI Gateway credential the bot's own model calls use: eve's resolver
454
- // (the bot process already loaded the workspace's env files), cached.
455
- export function gatewayCredentialLoader(resolverFile) {
456
- return async () => {
457
- const mod = await import(pathToFileURL(resolverFile).href);
458
- const c = await mod.resolveGatewayModelCredential();
459
- return { ok: true, token: c.token, teamId: c.teamId ?? null };
437
+ // A deployed bot's AI Gateway credential: the hub's own (the workspace owner's sign-in, kept fresh there), asked for with the bot's HUB_TOKEN.
438
+ // `fetchHub(path, init)` is registryFetch. On a refresh it names the token AI Gateway refused, as eve's served credential does.
439
+ export function hubCredentialLoader(fetchHub) {
440
+ let last = null;
441
+ return async ({ refresh = false } = {}) => {
442
+ const res = await fetchHub("/api/model-credential", { headers: refresh && last ? { "x-eve-model-rejected-token-sha256": createHash("sha256").update(last).digest("hex") } : {} });
443
+ if (!res.ok) return { ok: false, error: `model credential ${res.status}` };
444
+ const c = await res.json();
445
+ last = c.token ?? null;
446
+ return { ok: Boolean(c.token), token: c.token, teamId: c.teamId ?? null };
460
447
  };
461
448
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eve-hub",
3
- "version": "0.6.51",
3
+ "version": "0.6.52",
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
@@ -50,12 +50,12 @@ export const MAIN_KIT_FILES = {
50
50
  // sandbox that resolves /workspace as itself, which a Vercel Sandbox does),
51
51
  // the kit's bash (HostSandbox's root, the daemon's env; eve's own bash runs
52
52
  // instead), the browser and the screenshot paths of attach_image/view_image,
53
- // the mention gate (the
54
- // daemon's per-process HUB_AGENT and patched model credential), restart_hub (the hub's own code), memory (the workspace's files), and main's prune schedule (it cleans the hub machine's disk).
53
+ // restart_hub (the hub's own code), memory (the workspace's files), and main's prune schedule (it cleans the hub machine's disk).
55
54
  // main's compact and dash schedules stay: off the hub's machine compact finds no session to compact
56
55
  // (hosted sessions carry no last-model-call time), and dash has no PR data (its lookup runs `gh`).
57
56
  export const DEPLOYED_KIT_FILES = new Set([
58
57
  "channels/eve.ts",
58
+ "hooks/mention-gate.ts",
59
59
  "schedules/compact.ts",
60
60
  "schedules/dash.ts",
61
61
  "skills/onboarding-profile.ts",
package/world.mjs CHANGED
@@ -20242,42 +20242,6 @@ var LEASE_LEFT = `SELECT l.setup_failed_until - ${NOW_MS} AS ms FROM local_runne
20242
20242
  // ../../hosted-hub/src/store/schema.ts
20243
20243
  var OPEN_TURN_STATUSES = "('streaming','cancelling')";
20244
20244
 
20245
- // ../../hosted-hub/src/domain/wake.ts
20246
- var escape = (s2) => s2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
20247
- var mentioned = (roster, text) => roster.filter((bot) => new RegExp(`(^|[^\\w@])@${escape(bot)}(?![\\w-])`, "i").test(text));
20248
- var hasAll = (text) => /(^|[^\w@])@all(?![\w-])/i.test(text);
20249
- function recipients(roster, text, from, threadId2 = "", participants = []) {
20250
- if (threadId2.startsWith("dm-")) {
20251
- const bot = threadId2.slice(3);
20252
- return bot !== from.name && roster.includes(bot) ? [bot] : [];
20253
- }
20254
- const named = [.../* @__PURE__ */ new Set([...mentioned(roster, text), ...hasAll(text) ? participants.filter((b) => roster.includes(b)) : []])].filter((bot) => bot !== from.name);
20255
- if (from.kind === "agent") return named;
20256
- return named.length ? named : roster.slice(0, 1);
20257
- }
20258
-
20259
- // ../../lib/usage.mjs
20260
- var emptyBucket = () => ({ steps: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, costUsd: null, unpricedSteps: 0 });
20261
- var TOKENS = ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens"];
20262
- var num = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0;
20263
- var round = (v) => Math.round(v * 1e9) / 1e9;
20264
- function addUsage(bucket, usage = {}) {
20265
- bucket.steps += 1;
20266
- for (const key of TOKENS) bucket[key] += num(usage?.[key]);
20267
- const cost = usage?.costUsd;
20268
- if (typeof cost === "number" && Number.isFinite(cost) && cost >= 0) bucket.costUsd = round((bucket.costUsd ?? 0) + cost);
20269
- else bucket.unpricedSteps += 1;
20270
- return bucket;
20271
- }
20272
- var HOURS_KEPT_MS = 35 * 24 * 36e5;
20273
-
20274
- // ../../hosted-hub/src/store/usage.ts
20275
- 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)";
20276
- function stepValues(sessionId2, step) {
20277
- const b = addUsage(emptyBucket(), step.usage);
20278
- return [sessionId2, step.sequence, step.stepIndex, Date.parse(step.at), b.inputTokens, b.outputTokens, b.cacheReadTokens, b.cacheWriteTokens, b.costUsd];
20279
- }
20280
-
20281
20245
  // mention-tokens.mjs
20282
20246
  function codeRanges(text) {
20283
20247
  const s2 = String(text ?? "");
@@ -20339,6 +20303,44 @@ function mentionTokens(text) {
20339
20303
  return out;
20340
20304
  }
20341
20305
 
20306
+ // ../../hosted-hub/src/domain/wake.ts
20307
+ var mentioned = (roster, text) => {
20308
+ const tokens = new Set(mentionTokens(text));
20309
+ return roster.filter((bot) => tokens.has(bot.toLowerCase()));
20310
+ };
20311
+ var hasAll = (text) => mentionTokens(text).includes("all");
20312
+ function recipients(roster, text, from, threadId2 = "", participants = []) {
20313
+ if (threadId2.startsWith("dm-")) {
20314
+ const bot = threadId2.slice(3);
20315
+ return bot !== from.name && roster.includes(bot) ? [bot] : [];
20316
+ }
20317
+ const named = [.../* @__PURE__ */ new Set([...mentioned(roster, text), ...hasAll(text) ? participants.filter((b) => roster.includes(b)) : []])].filter((bot) => bot !== from.name);
20318
+ if (from.kind === "agent") return named;
20319
+ return named.length ? named : roster.slice(0, 1);
20320
+ }
20321
+
20322
+ // ../../lib/usage.mjs
20323
+ var emptyBucket = () => ({ steps: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, costUsd: null, unpricedSteps: 0 });
20324
+ var TOKENS = ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens"];
20325
+ var num = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0;
20326
+ var round = (v) => Math.round(v * 1e9) / 1e9;
20327
+ function addUsage(bucket, usage = {}) {
20328
+ bucket.steps += 1;
20329
+ for (const key of TOKENS) bucket[key] += num(usage?.[key]);
20330
+ const cost = usage?.costUsd;
20331
+ if (typeof cost === "number" && Number.isFinite(cost) && cost >= 0) bucket.costUsd = round((bucket.costUsd ?? 0) + cost);
20332
+ else bucket.unpricedSteps += 1;
20333
+ return bucket;
20334
+ }
20335
+ var HOURS_KEPT_MS = 35 * 24 * 36e5;
20336
+
20337
+ // ../../hosted-hub/src/store/usage.ts
20338
+ 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)";
20339
+ function stepValues(sessionId2, step) {
20340
+ const b = addUsage(emptyBucket(), step.usage);
20341
+ return [sessionId2, step.sequence, step.stepIndex, Date.parse(step.at), b.inputTokens, b.outputTokens, b.cacheReadTokens, b.cacheWriteTokens, b.costUsd];
20342
+ }
20343
+
20342
20344
  // ../../hosted-hub/src/store/notifications.ts
20343
20345
  var NOTIFICATION_READS = s("SELECT person, notification_id FROM notification_reads");
20344
20346
  var NOTIFICATIONS = s("SELECT message_id, person FROM notifications");
@@ -27226,7 +27228,8 @@ var TASK_CARD = `(SELECT CASE WHEN json_array_length(json_extract(t.activity, '$
27226
27228
  AND m.rowid = (SELECT MAX(x.rowid) FROM messages x WHERE x.turn_id = m.turn_id))`;
27227
27229
  var FACT_COLUMNS = `status, status_from, metadata, verdict, decisions, tested, ${TASK_CARD} AS task_card, event`;
27228
27230
  var LINKED = "EXISTS (SELECT 1 FROM turn_deliveries td WHERE td.delivery_id = d.id)";
27229
- var DELIVERY_STATUS = `CASE WHEN d.accepted_at IS NOT NULL OR ${LINKED} THEN COALESCE((SELECT 'waiting: on your answer to its ' || r.kind || ' request'
27231
+ var DELIVERY_STATUS = `CASE WHEN json_extract(d.gate, '$.decision') = 'skip' THEN 'skipped: ' || json_extract(d.gate, '$.reason')
27232
+ WHEN d.accepted_at IS NOT NULL OR ${LINKED} THEN COALESCE((SELECT 'waiting: on your answer to its ' || r.kind || ' request'
27230
27233
  FROM input_requests r JOIN sessions rs ON rs.id = r.session_id
27231
27234
  WHERE r.session_id = d.session_id AND r.status = 'pending' AND rs.ended_at IS NULL
27232
27235
  AND r.created_at <= COALESCE((SELECT t.started_at FROM turn_deliveries td JOIN turns t ON t.id = td.turn_id WHERE td.delivery_id = d.id), d.accepted_at) LIMIT 1),
@@ -27245,9 +27248,22 @@ var RESTARTED_TURNS_READ = s(`SELECT t.thread_id, t.bot, r.claimed_at FROM turns
27245
27248
  AND NOT EXISTS (SELECT 1 FROM session_folds f WHERE f.session_id = t.session_id AND f.folded_at >= r.claimed_at)`);
27246
27249
 
27247
27250
  // ../../hosted-hub/src/store/fold.ts
27251
+ function handedBack({ session, roster, asked = [], posted = [], notify }, text, named) {
27252
+ if (session.thread.startsWith("dm-")) return [];
27253
+ const known = new Set([...roster, ...notify ? [notify.person, ...notify.names] : [], "all"].map((n) => n.toLowerCase())), me = session.bot.toLowerCase();
27254
+ if ([text, ...posted].some((t) => mentionTokens(t).some((name) => name !== me && known.has(name)))) return [];
27255
+ return [...new Set(asked.filter((a) => (a.linked || named.has(a.id)) && a.author !== session.bot && roster.includes(a.author) && mentionTokens(a.text).includes(me)).map((a) => a.author))];
27256
+ }
27248
27257
  var FOLD_STATE_SQL = `SELECT s.id, s.thread_id, s.bot, COALESCE(f.folded_index, -1) AS folded_index, (SELECT json_group_array(name) FROM (SELECT name FROM bots WHERE ${inRoster} ORDER BY position)) AS roster,
27249
27258
  (SELECT json_group_array(bot) FROM (${PARTICIPANTS("s.thread_id")})) AS participants,
27250
27259
  (SELECT d.message_id FROM deliveries d WHERE d.bot = s.bot AND d.held_at IS NOT NULL AND d.held_paused IS NOT 1 AND d.accepted_at IS NULL AND d.failed_at IS NULL AND d.revoked_at IS NULL ORDER BY d.rowid LIMIT 1) AS held,
27260
+ (SELECT json_group_array(json_object('id', dd.id, 'author', m.author, 'text', m.text, 'linked', EXISTS (SELECT 1 FROM turn_deliveries td JOIN turns t ON t.id = td.turn_id WHERE td.delivery_id = dd.id AND t.status IN ${OPEN_TURN_STATUSES})))
27261
+ FROM deliveries dd JOIN messages m ON m.id = dd.message_id WHERE dd.session_id = s.id AND m.author_kind = 'agent'
27262
+ AND (EXISTS (SELECT 1 FROM turn_deliveries td JOIN turns t ON t.id = td.turn_id WHERE td.delivery_id = dd.id AND t.status IN ${OPEN_TURN_STATUSES}) OR NOT EXISTS (SELECT 1 FROM turn_deliveries td WHERE td.delivery_id = dd.id))) AS asked,
27263
+ (SELECT json_group_array(p.text) FROM messages p WHERE p.turn_id = (SELECT t.id FROM turns t WHERE t.session_id = s.id AND t.status IN ${OPEN_TURN_STATUSES} ORDER BY t.started_at DESC LIMIT 1)
27264
+ AND p.author = s.bot AND p.author_kind = 'agent' AND p.status IS NULL) AS posted,
27265
+ (SELECT p.id FROM messages p WHERE p.turn_id = (SELECT t.id FROM turns t WHERE t.session_id = s.id AND t.status IN ${OPEN_TURN_STATUSES} ORDER BY t.started_at DESC LIMIT 1)
27266
+ AND p.author = s.bot AND p.author_kind = 'agent' AND p.status IS NULL ORDER BY p.created_at DESC, p.rowid DESC LIMIT 1) AS last_post,
27251
27267
  (SELECT json_object('person', person, 'names', json(names)) FROM human_notify) AS notify,
27252
27268
  (SELECT json_object('status', t.status, 'id', t.id, 'text', t.reply_text, 'lastStep', json(t.last_step), 'activity', json(t.activity),
27253
27269
  'trigger', ${triggerOf("t")}) FROM turns t WHERE t.session_id = s.id AND t.status IN ${OPEN_TURN_STATUSES}
@@ -27262,6 +27278,9 @@ function foldState(row) {
27262
27278
  roster: JSON.parse(String(row.roster)),
27263
27279
  participants: JSON.parse(String(row.participants)),
27264
27280
  held: row.held == null ? null : String(row.held),
27281
+ asked: JSON.parse(String(row.asked ?? "[]")),
27282
+ posted: JSON.parse(String(row.posted ?? "[]")),
27283
+ lastPost: row.last_post == null ? null : String(row.last_post),
27265
27284
  notify: row.notify ? JSON.parse(String(row.notify)) : null,
27266
27285
  foldedIndex: Number(row.folded_index),
27267
27286
  open: row.open_turn ? JSON.parse(String(row.open_turn)) : null,
@@ -27275,7 +27294,7 @@ var IF_INSERTED = "WHERE changes() > 0";
27275
27294
  var lastActWasOwnPost = (ls) => !!ls && Object.keys(ls.calls).length > 0 && Object.values(ls.calls).every((own3) => own3 === true);
27276
27295
  function fold(state, chunks, now = Date.now()) {
27277
27296
  const { session } = state;
27278
- const stmts = [], touched = /* @__PURE__ */ new Map();
27297
+ const stmts = [], touched = /* @__PURE__ */ new Map(), named = /* @__PURE__ */ new Set();
27279
27298
  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));
27280
27299
  let open3 = state.open && { ...state.open, trigger: state.open.trigger ?? null, lastStep: state.open.lastStep ?? null, status: state.open.status, endedAt: null, error: null };
27281
27300
  const deliver = [];
@@ -27363,7 +27382,9 @@ function fold(state, chunks, now = Date.now()) {
27363
27382
  data.message.length - state2
27364
27383
  ));
27365
27384
  }
27385
+ if (event.type === "turn.started") named.clear();
27366
27386
  for (const deliveryId2 of event.meta?.deliveryIds ?? []) {
27387
+ named.add(deliveryId2);
27367
27388
  open3.trigger ??= messageOfDelivery(deliveryId2, session.bot);
27368
27389
  stmts.push(s("INSERT OR IGNORE INTO turn_deliveries (delivery_id, turn_id) SELECT id, ? FROM deliveries WHERE id = ? AND session_id = ?", open3.id, deliveryId2, session.id));
27369
27390
  }
@@ -27441,9 +27462,14 @@ function fold(state, chunks, now = Date.now()) {
27441
27462
  stmts.push(...notificationFor(messageId2, "agent", text, state.notify ?? void 0));
27442
27463
  stmts.push(s(`INSERT OR IGNORE INTO message_attachments (message_id, position, upload_id, name, size)
27443
27464
  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));
27444
- const to = recipients(state.roster, text, { name: session.bot, kind: "agent" }, session.thread, state.participants);
27465
+ const onPost = text === "" ? state.lastPost : null;
27466
+ const to = [.../* @__PURE__ */ new Set([...recipients(state.roster, text, { name: session.bot, kind: "agent" }, session.thread, state.participants), ...onPost ? [] : handedBack(state, text, named)])];
27445
27467
  stmts.push(...routingStatements(messageId2, session.thread, to, at, { by: session.bot, joins: true }));
27446
27468
  deliver.push(messageId2);
27469
+ if (onPost) {
27470
+ stmts.push(...routingStatements(onPost, session.thread, handedBack(state, "", named), at, { by: session.bot, joins: true }));
27471
+ deliver.push(onPost);
27472
+ }
27447
27473
  }
27448
27474
  end("done");
27449
27475
  break;