eve-hub 0.6.39 → 0.6.42

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,23 @@
1
1
  # eve-hub
2
2
 
3
+ ## 0.6.42
4
+
5
+ ### Patch Changes
6
+
7
+ - The state frame carries each bot's latest finished turn per thread (`lastTurn`), so the app marks a thread failed from that turn.
8
+
9
+ ## 0.6.41
10
+
11
+ ### Patch Changes
12
+
13
+ - A model call that sends no data for 30 s ends with a timeout instead of waiting 5 minutes per attempt; the clock restarts on every chunk, and the AI SDK's two retries stay, so a brief hang recovers without a hand retry.
14
+
15
+ ## 0.6.40
16
+
17
+ ### Patch Changes
18
+
19
+ - The hub records the size of each input a bot receives and each compaction, from eve's own stream, so the team-model work has a measured before.
20
+
3
21
  ## 0.6.39
4
22
 
5
23
  ### Patch Changes
@@ -0,0 +1,79 @@
1
+ // A model call that gets no response fails fast. Node's fetch waits 300 s for
2
+ // response headers and for each body chunk (undici's default headersTimeout /
3
+ // bodyTimeout), and eve hands the AI SDK no timeout of its own, so one hung
4
+ // gateway request stalled a turn for 5 minutes (15 with retries). Every
5
+ // streaming gateway request now gets an idle limit instead: no headers, or no
6
+ // chunk while a read is waiting, within MODEL_CALL_TIMEOUT_MS ends it with a
7
+ // TimeoutError; the clock restarts on every chunk, so an answer that is still
8
+ // streaming is never cut off. The AI SDK retries that twice (3 x 30 s plus its
9
+ // backoff of 2 s and 4 s, about 1 min 36 s), then eve parks the turn as recoverable, so a transient hang
10
+ // recovers without a hand retry.
11
+ //
12
+ // Streaming calls only: a non-streaming call (eve's compaction `generateText`)
13
+ // legitimately waits for the whole generation, so a hung compaction still waits
14
+ // the 300 s default. Remove this module when eve forwards `timeout`/`maxRetries`
15
+ // to the AI SDK (vercel/eve#3984).
16
+ export const MODEL_CALL_TIMEOUT_MS = 30_000;
17
+
18
+ const INSTALLED = Symbol.for("hub.modelCallTimeout");
19
+ const GATEWAY_HOST = "ai-gateway.vercel.sh";
20
+
21
+ function isStreamingGatewayCall(input, init) {
22
+ let url;
23
+ try {
24
+ url = new URL(typeof input === "string" || input instanceof URL ? input : input.url);
25
+ } catch {
26
+ return false;
27
+ }
28
+ if (url.hostname !== GATEWAY_HOST) return false;
29
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
30
+ return headers.get("ai-language-model-streaming") === "true";
31
+ }
32
+
33
+ export function withModelCallTimeout(baseFetch, ms) {
34
+ return async function fetchWithModelCallTimeout(input, init) {
35
+ if (!isStreamingGatewayCall(input, init)) return baseFetch(input, init);
36
+ const ctrl = new AbortController();
37
+ const caller = init?.signal ?? (input instanceof Request ? input.signal : undefined);
38
+ const signal = caller ? AbortSignal.any([caller, ctrl.signal]) : ctrl.signal;
39
+ let timer;
40
+ const arm = () => {
41
+ timer = setTimeout(() => ctrl.abort(new DOMException(`model call got no response for ${ms} ms`, "TimeoutError")), ms);
42
+ };
43
+ arm();
44
+ let res;
45
+ try {
46
+ res = await baseFetch(input, { ...init, signal });
47
+ } finally {
48
+ clearTimeout(timer);
49
+ }
50
+ if (!res.body) return res;
51
+ const reader = res.body.getReader();
52
+ const body = new ReadableStream({
53
+ async pull(controller) {
54
+ arm();
55
+ try {
56
+ const { done, value } = await reader.read();
57
+ if (done) controller.close();
58
+ else controller.enqueue(value);
59
+ } catch (error) {
60
+ controller.error(error);
61
+ } finally {
62
+ clearTimeout(timer);
63
+ }
64
+ },
65
+ cancel(reason) {
66
+ clearTimeout(timer);
67
+ return reader.cancel(reason);
68
+ },
69
+ });
70
+ return new Response(body, { status: res.status, statusText: res.statusText, headers: res.headers });
71
+ };
72
+ }
73
+
74
+ export function installModelCallTimeout() {
75
+ if (globalThis.fetch[INSTALLED]) return;
76
+ const wrapped = withModelCallTimeout(globalThis.fetch.bind(globalThis), MODEL_CALL_TIMEOUT_MS);
77
+ wrapped[INSTALLED] = true;
78
+ globalThis.fetch = wrapped;
79
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eve-hub",
3
- "version": "0.6.39",
3
+ "version": "0.6.42",
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/roster-model.mjs CHANGED
@@ -16,8 +16,10 @@ import { defineDynamic } from "eve";
16
16
  import { CONTEXT_WINDOW, selectModel } from "./bot-model.mjs";
17
17
  import { DEFAULT_SUBAGENT_CAP } from "./subagent-cap.mjs";
18
18
  import { sessionLimits } from "./session-limits.mjs";
19
+ import { installModelCallTimeout } from "./model-timeout.mjs";
19
20
  import { findWorkspaceRoot, loadWorkspace, parseWorkspace } from "./workspace.mjs";
20
21
  const real = process.env.ROSTER_MODELS !== "mock";
22
+ installModelCallTimeout();
21
23
 
22
24
  export function createRosterModel({ agents, subagentCap }) {
23
25
  return function rosterModel(name, { mock, copyMock } = {}) {
package/world.mjs CHANGED
@@ -626,6 +626,9 @@ var names = (text, { person, names: names2 }) => {
626
626
  var record = (id, person) => s("INSERT OR IGNORE INTO notifications (message_id, person) SELECT ?, ? WHERE EXISTS (SELECT 1 FROM messages WHERE id = ?)", id, person, id);
627
627
  var notificationFor = (messageId2, kind, text, notify) => notify && kind === "agent" && names(text, notify) ? [record(messageId2, notify.person)] : [];
628
628
 
629
+ // ../../hosted-hub/src/dispatch/bot.ts
630
+ var MESSAGE_LINE = "\n[thread ";
631
+
629
632
  // ../../hosted-hub/src/store/subagents.ts
630
633
  var UP = `up(session_id, parent) AS (
631
634
  SELECT session_id, parent FROM subagent_links WHERE session_id = ?
@@ -26782,6 +26785,8 @@ var HubThread = object2({
26782
26785
  status: maybe(map2("ThreadStatus", string5())),
26783
26786
  busySince: maybe(map2("ThreadBusySince", string5())),
26784
26787
  turnCounts: maybe(map2("ThreadTurnCounts", int2())),
26788
+ /** Each bot's latest finished turn here: done | failed | cancelled. */
26789
+ lastTurn: maybe(map2("ThreadLastTurn", string5())),
26785
26790
  archived: maybe(Archived),
26786
26791
  source: maybe(ThreadSource),
26787
26792
  tags: maybe(ThreadTags),
@@ -27323,7 +27328,35 @@ function fold(state, chunks, now = Date.now()) {
27323
27328
  SELECT ?, ?, ?, ?, ?, 'streaming', ? WHERE EXISTS (SELECT 1 FROM sessions WHERE id = ? AND ended_at IS NULL)`, open3.id, session.thread, session.id, data.turnId, session.bot, at, session.id));
27324
27329
  log("turn.status", { turnId: open3.id, status: "streaming" });
27325
27330
  }
27331
+ if (event.type === "compaction.completed")
27332
+ stmts.push(s(
27333
+ "INSERT OR IGNORE INTO compaction_events (session_id, chunk, position, sequence, step_index, at, thread_id, bot) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
27334
+ session.id,
27335
+ index,
27336
+ eventIndex,
27337
+ data.sequence ?? null,
27338
+ data.stepIndex ?? null,
27339
+ at,
27340
+ session.thread,
27341
+ session.bot
27342
+ ));
27326
27343
  if (!open3) continue;
27344
+ if (event.type === "message.received" && typeof data.message === "string") {
27345
+ const cut = data.message.indexOf(MESSAGE_LINE);
27346
+ const state2 = cut < 0 ? 0 : cut;
27347
+ stmts.push(s(
27348
+ "INSERT OR IGNORE INTO inputs (session_id, chunk, position, turn_id, at, thread_id, bot, state_chars, message_chars) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
27349
+ session.id,
27350
+ index,
27351
+ eventIndex,
27352
+ open3.id,
27353
+ at,
27354
+ session.thread,
27355
+ session.bot,
27356
+ state2,
27357
+ data.message.length - state2
27358
+ ));
27359
+ }
27327
27360
  for (const deliveryId2 of event.meta?.deliveryIds ?? []) {
27328
27361
  open3.trigger ??= messageOfDelivery(deliveryId2, session.bot);
27329
27362
  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));