eve-hub 0.6.51 → 0.6.53

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,17 @@
1
1
  # eve-hub
2
2
 
3
+ ## 0.6.53
4
+
5
+ ### Patch Changes
6
+
7
+ - The Update card never offers an older release than the workspace runs, and the upgrade route answers a workspace still being set up as an error.
8
+
9
+ ## 0.6.52
10
+
11
+ ### Patch Changes
12
+
13
+ - 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.
14
+
3
15
  ## 0.6.51
4
16
 
5
17
  ### 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.53",
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");
@@ -27040,6 +27042,8 @@ var HostedUpdate = object2({
27040
27042
  commit: maybe(string5()),
27041
27043
  /** `updated`: the Vercel page of the deployment that made the move; absent for a move recorded before it was kept. */
27042
27044
  deployment: maybe(string5()),
27045
+ /** `updated`: the move was a commit that is not an update to the hub's release (a memory or instructions edit); `commit` is it. */
27046
+ plain: boolean5().optional(),
27043
27047
  /**
27044
27048
  * `kind`: sign-in (the owner signs in again) | build (`url` is the failed build) | other. A failed build also carries
27045
27049
  * what Vercel says of it once the hub has read it: the `step` that failed, the `sha` it built, and the `log`'s last lines.
@@ -27205,7 +27209,7 @@ var StoredHosted = HostedWorkspace.omit({ eveHub: true, commits: true, update: t
27205
27209
  workspace: DeployedWorkspace.nullable().default(null),
27206
27210
  attempt: Attempt.nullable().default(null),
27207
27211
  upgrade: Upgrade.nullable().default(null),
27208
- updated: external_exports.object({ to: external_exports.string(), at: external_exports.string(), commit: external_exports.string().nullable().default(null), deployment: external_exports.string().nullable().default(null) }).nullable().default(null)
27212
+ updated: external_exports.object({ to: external_exports.string(), at: external_exports.string(), commit: external_exports.string().nullable().default(null), deployment: external_exports.string().nullable().default(null), plain: external_exports.boolean().default(false) }).nullable().default(null)
27209
27213
  });
27210
27214
  var REPO_READ = s("SELECT metadata FROM workspace_repo WHERE id = 1");
27211
27215
 
@@ -27226,7 +27230,8 @@ var TASK_CARD = `(SELECT CASE WHEN json_array_length(json_extract(t.activity, '$
27226
27230
  AND m.rowid = (SELECT MAX(x.rowid) FROM messages x WHERE x.turn_id = m.turn_id))`;
27227
27231
  var FACT_COLUMNS = `status, status_from, metadata, verdict, decisions, tested, ${TASK_CARD} AS task_card, event`;
27228
27232
  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'
27233
+ var DELIVERY_STATUS = `CASE WHEN json_extract(d.gate, '$.decision') = 'skip' THEN 'skipped: ' || json_extract(d.gate, '$.reason')
27234
+ WHEN d.accepted_at IS NOT NULL OR ${LINKED} THEN COALESCE((SELECT 'waiting: on your answer to its ' || r.kind || ' request'
27230
27235
  FROM input_requests r JOIN sessions rs ON rs.id = r.session_id
27231
27236
  WHERE r.session_id = d.session_id AND r.status = 'pending' AND rs.ended_at IS NULL
27232
27237
  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 +27250,22 @@ var RESTARTED_TURNS_READ = s(`SELECT t.thread_id, t.bot, r.claimed_at FROM turns
27245
27250
  AND NOT EXISTS (SELECT 1 FROM session_folds f WHERE f.session_id = t.session_id AND f.folded_at >= r.claimed_at)`);
27246
27251
 
27247
27252
  // ../../hosted-hub/src/store/fold.ts
27253
+ function handedBack({ session, roster, asked = [], posted = [], notify }, text, named) {
27254
+ if (session.thread.startsWith("dm-")) return [];
27255
+ const known = new Set([...roster, ...notify ? [notify.person, ...notify.names] : [], "all"].map((n) => n.toLowerCase())), me = session.bot.toLowerCase();
27256
+ if ([text, ...posted].some((t) => mentionTokens(t).some((name) => name !== me && known.has(name)))) return [];
27257
+ 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))];
27258
+ }
27248
27259
  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
27260
  (SELECT json_group_array(bot) FROM (${PARTICIPANTS("s.thread_id")})) AS participants,
27250
27261
  (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,
27262
+ (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})))
27263
+ FROM deliveries dd JOIN messages m ON m.id = dd.message_id WHERE dd.session_id = s.id AND m.author_kind = 'agent'
27264
+ 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,
27265
+ (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)
27266
+ AND p.author = s.bot AND p.author_kind = 'agent' AND p.status IS NULL) AS posted,
27267
+ (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)
27268
+ 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
27269
  (SELECT json_object('person', person, 'names', json(names)) FROM human_notify) AS notify,
27252
27270
  (SELECT json_object('status', t.status, 'id', t.id, 'text', t.reply_text, 'lastStep', json(t.last_step), 'activity', json(t.activity),
27253
27271
  'trigger', ${triggerOf("t")}) FROM turns t WHERE t.session_id = s.id AND t.status IN ${OPEN_TURN_STATUSES}
@@ -27262,6 +27280,9 @@ function foldState(row) {
27262
27280
  roster: JSON.parse(String(row.roster)),
27263
27281
  participants: JSON.parse(String(row.participants)),
27264
27282
  held: row.held == null ? null : String(row.held),
27283
+ asked: JSON.parse(String(row.asked ?? "[]")),
27284
+ posted: JSON.parse(String(row.posted ?? "[]")),
27285
+ lastPost: row.last_post == null ? null : String(row.last_post),
27265
27286
  notify: row.notify ? JSON.parse(String(row.notify)) : null,
27266
27287
  foldedIndex: Number(row.folded_index),
27267
27288
  open: row.open_turn ? JSON.parse(String(row.open_turn)) : null,
@@ -27275,7 +27296,7 @@ var IF_INSERTED = "WHERE changes() > 0";
27275
27296
  var lastActWasOwnPost = (ls) => !!ls && Object.keys(ls.calls).length > 0 && Object.values(ls.calls).every((own3) => own3 === true);
27276
27297
  function fold(state, chunks, now = Date.now()) {
27277
27298
  const { session } = state;
27278
- const stmts = [], touched = /* @__PURE__ */ new Map();
27299
+ const stmts = [], touched = /* @__PURE__ */ new Map(), named = /* @__PURE__ */ new Set();
27279
27300
  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
27301
  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
27302
  const deliver = [];
@@ -27363,7 +27384,9 @@ function fold(state, chunks, now = Date.now()) {
27363
27384
  data.message.length - state2
27364
27385
  ));
27365
27386
  }
27387
+ if (event.type === "turn.started") named.clear();
27366
27388
  for (const deliveryId2 of event.meta?.deliveryIds ?? []) {
27389
+ named.add(deliveryId2);
27367
27390
  open3.trigger ??= messageOfDelivery(deliveryId2, session.bot);
27368
27391
  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
27392
  }
@@ -27441,9 +27464,14 @@ function fold(state, chunks, now = Date.now()) {
27441
27464
  stmts.push(...notificationFor(messageId2, "agent", text, state.notify ?? void 0));
27442
27465
  stmts.push(s(`INSERT OR IGNORE INTO message_attachments (message_id, position, upload_id, name, size)
27443
27466
  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);
27467
+ const onPost = text === "" ? state.lastPost : null;
27468
+ const to = [.../* @__PURE__ */ new Set([...recipients(state.roster, text, { name: session.bot, kind: "agent" }, session.thread, state.participants), ...onPost ? [] : handedBack(state, text, named)])];
27445
27469
  stmts.push(...routingStatements(messageId2, session.thread, to, at, { by: session.bot, joins: true }));
27446
27470
  deliver.push(messageId2);
27471
+ if (onPost) {
27472
+ stmts.push(...routingStatements(onPost, session.thread, handedBack(state, "", named), at, { by: session.bot, joins: true }));
27473
+ deliver.push(onPost);
27474
+ }
27447
27475
  }
27448
27476
  end("done");
27449
27477
  break;