eve-hub 0.6.34 → 0.6.36

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.36
4
+
5
+ ### Patch Changes
6
+
7
+ - The bots' /hub/manifest reports the workspace's `hopLimit`, so the hosted hub holds bot-to-bot chains at it.
8
+
9
+ ## 0.6.35
10
+
11
+ ### Patch Changes
12
+
13
+ - A bot's attach_image carries its images into its reply with their sizes; an oversized upload answers 413 naming both limits.
14
+
3
15
  ## 0.6.34
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eve-hub",
3
- "version": "0.6.34",
3
+ "version": "0.6.36",
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/prepare.mjs CHANGED
@@ -63,8 +63,8 @@ import workspace from "../../../../hub.manifest.json" with { type: "json" };
63
63
  // syncs by; a deployment with none (\`vercel deploy\` uploads no git metadata)
64
64
  // has no manifest to give.
65
65
  const sha = process.env.VERCEL_GIT_COMMIT_SHA;
66
- const { agents, humans, subagentCap } = parseWorkspace(workspace);
67
- const manifest = { sha, kit: ${JSON.stringify(KIT_VERSION)}, agents, humans, subagentCap };
66
+ const { agents, humans, subagentCap, hopLimit } = parseWorkspace(workspace);
67
+ const manifest = { sha, kit: ${JSON.stringify(KIT_VERSION)}, agents, humans, subagentCap, ...(hopLimit === undefined ? {} : { hopLimit }) };
68
68
 
69
69
  export default defineChannel({
70
70
  routes: [
package/world.mjs CHANGED
@@ -437,6 +437,7 @@ function derivedMessageId(...parts) {
437
437
  }
438
438
  var sessionId = () => `wrun_${ulid()}`;
439
439
  var deliveryId = (message, bot) => `dlv_${message}_${bot}`;
440
+ var messageOfDelivery = (delivery, bot) => delivery.slice("dlv_".length, -`_${bot}`.length);
440
441
  var turnId = (session, chunkIndex) => `trn_${session}_${chunkIndex}`;
441
442
 
442
443
  // ../../hosted-hub/src/store/events.ts
@@ -495,6 +496,9 @@ function routingStatements(messageId2, threadId2, bots, now, { by, joins }, held
495
496
  ])
496
497
  ];
497
498
  }
499
+ var hopAfter = (session) => s(`(SELECT 1 + COALESCE(MAX(m.hop), 0) FROM deliveries d JOIN messages m ON m.id = d.message_id WHERE d.session_id = ? AND (
500
+ EXISTS (SELECT 1 FROM turn_deliveries td WHERE td.delivery_id = d.id AND td.turn_id = (SELECT id FROM turns WHERE session_id = ? ORDER BY started_at DESC, rowid DESC LIMIT 1))
501
+ OR (d.accepted_at IS NOT NULL AND NOT EXISTS (SELECT 1 FROM turn_deliveries td WHERE td.delivery_id = d.id))))`, session, session);
498
502
 
499
503
  // subagent-cap.mjs
500
504
  var DEFAULT_SUBAGENT_CAP = Object.freeze({
@@ -505,6 +509,8 @@ var DEFAULT_SUBAGENT_CAP = Object.freeze({
505
509
 
506
510
  // ../../hosted-hub/src/store/roster.ts
507
511
  var onLocal = (bot) => `EXISTS (SELECT 1 FROM bots WHERE name = ${bot} AND runs_on = 'local')`;
512
+ var HOP_LIMIT = "(SELECT json_extract(metadata, '$.workspace.hopLimit') FROM workspace_repo WHERE id = 1)";
513
+ var heldByHopLimit = (m) => `(${m}.author_kind = 'agent' AND ${HOP_LIMIT} IS NOT NULL AND ${m}.hop >= ${HOP_LIMIT})`;
508
514
 
509
515
  // ../../hosted-hub/src/store/schema.ts
510
516
  var OPEN_TURN_STATUSES = "('streaming','cancelling')";
@@ -635,6 +641,29 @@ function linkStatements(sessionId2, parent, callId) {
635
641
  ];
636
642
  }
637
643
 
644
+ // ../../hosted-hub/src/store/failures.ts
645
+ var STATE = "json_extract(event, '$.state')";
646
+ var failureId = (trigger, bot) => `evt_fail_${trigger}_${bot}`;
647
+ var cardLog = (idSql, now, ...args) => s(`INSERT INTO log (thread_id, type, data, at) SELECT thread_id, 'message.updated', json_object('threadId', thread_id, 'messageId', id), ? FROM messages WHERE id = (${idSql}) AND changes() > 0`, now, ...args);
648
+ function failureStatements(t, now) {
649
+ if (!t.trigger) return [];
650
+ const id = failureId(t.trigger, t.bot), detail = t.error ?? "unknown error";
651
+ const turn = (status) => s("EXISTS (SELECT 1 FROM turns WHERE id = ? AND status = ?)", t.id, status);
652
+ const failed = turn("failed"), settled = (status, next) => {
653
+ const when = turn(status);
654
+ return [s(`UPDATE messages SET event = json_set(event, '$.state', ?) WHERE id = ? AND ${STATE} = 'retrying' AND ${when.sql}`, next, id, ...when.args), cardLog("?", now, id)];
655
+ };
656
+ return [
657
+ ...eventStatements(t.threadId, { kind: "turn_failed", agent: t.bot, triggerId: t.trigger, state: "failed", retries: 0, turnId: t.id, detail }, `${t.bot}'s turn failed: ${detail}`, now, id, failed),
658
+ s(`UPDATE messages SET text = ?, event = json_set(event, '$.state', 'failed', '$.detail', ?, '$.turnId', ?, '$.retries', json_extract(event, '$.retries') + 1)
659
+ WHERE id = ? AND ${STATE} = 'retrying' AND json_extract(event, '$.turnId') <> ? AND ${failed.sql}`, `${t.bot}'s turn failed: ${detail}`, detail, t.id, id, t.id, ...failed.args),
660
+ cardLog("?", now, id),
661
+ ...settled("done", "resolved"),
662
+ ...settled("cancelled", "failed")
663
+ ];
664
+ }
665
+ var triggerOf = (t) => `(SELECT d.message_id FROM turn_deliveries td JOIN deliveries d ON d.id = td.delivery_id WHERE td.turn_id = ${t}.id ORDER BY d.rowid LIMIT 1)`;
666
+
638
667
  // ../../lib/tags.mjs
639
668
  var TAG_FIELDS = ["project", "issueType"];
640
669
  var LABEL_FIELDS = [...TAG_FIELDS, "path"];
@@ -670,6 +699,8 @@ var PENDING_REQUESTS_READ = s(`SELECT r.id, r.session_id, r.thread_id, r.bot, r.
670
699
  var LIVE_TURNS_READ = s(`SELECT thread_id,bot,started_at,reply_text,activity FROM turns WHERE status IN ${OPEN_TURN_STATUSES} ORDER BY started_at`);
671
700
 
672
701
  // ../../hosted-hub/src/store/attachments.ts
702
+ var MAX_ATTACHMENTS = 10;
703
+ var OPEN_TURN = `(SELECT id FROM turns WHERE session_id = ? AND status IN ${OPEN_TURN_STATUSES} ORDER BY started_at DESC LIMIT 1)`;
673
704
  var ATTACHMENTS_READ = s("SELECT message_id, upload_id, name FROM message_attachments ORDER BY message_id, position");
674
705
  var unseenBy = (bot, thread, mid) => {
675
706
  const end = `COALESCE((SELECT rowid FROM messages WHERE id = ${mid}), 9223372036854775807)`;
@@ -5746,7 +5777,7 @@ var $ZodCyclicError = class extends Error {
5746
5777
  this.name = "ZodCyclicError";
5747
5778
  }
5748
5779
  };
5749
- var STATE = "~memo";
5780
+ var STATE2 = "~memo";
5750
5781
  var NO_ISSUES = [];
5751
5782
  function isRef(value) {
5752
5783
  return value !== null && typeof value === "object";
@@ -5960,10 +5991,10 @@ var memo = {
5960
5991
  const input2 = payload.value;
5961
5992
  if (!isRef(input2))
5962
5993
  return base(payload, ctx);
5963
- let state = ctx[STATE];
5994
+ let state = ctx[STATE2];
5964
5995
  if (!state) {
5965
5996
  state = { buckets: /* @__PURE__ */ new WeakMap(), backEdges: void 0 };
5966
- ctx[STATE] = state;
5997
+ ctx[STATE2] = state;
5967
5998
  }
5968
5999
  let bucket;
5969
6000
  if (lastCtx === ctx) {
@@ -6012,7 +6043,7 @@ function memoizer() {
6012
6043
  return memo;
6013
6044
  }
6014
6045
  function isBackEdge(ctx, value) {
6015
- const backEdges = ctx[STATE]?.backEdges;
6046
+ const backEdges = ctx[STATE2]?.backEdges;
6016
6047
  return backEdges !== void 0 && isRef(value) && backEdges.has(value);
6017
6048
  }
6018
6049
 
@@ -23600,7 +23631,7 @@ var $ZodCyclicError2 = class extends Error {
23600
23631
  this.name = "ZodCyclicError";
23601
23632
  }
23602
23633
  };
23603
- var STATE2 = "~memo";
23634
+ var STATE3 = "~memo";
23604
23635
  var NO_ISSUES2 = [];
23605
23636
  function cloneIssues2(issues) {
23606
23637
  return issues.map((iss) => iss.path ? { ...iss, path: iss.path.slice() } : { ...iss });
@@ -23775,10 +23806,10 @@ var memo2 = {
23775
23806
  const input2 = payload.value;
23776
23807
  if (input2 === null || typeof input2 !== "object")
23777
23808
  return base(payload, ctx);
23778
- let state = ctx[STATE2];
23809
+ let state = ctx[STATE3];
23779
23810
  if (!state) {
23780
23811
  state = { buckets: /* @__PURE__ */ new Map(), backEdges: void 0 };
23781
- ctx[STATE2] = state;
23812
+ ctx[STATE3] = state;
23782
23813
  }
23783
23814
  let bucket;
23784
23815
  if (lastCtx === ctx) {
@@ -23827,7 +23858,7 @@ function memoizer2() {
23827
23858
  return memo2;
23828
23859
  }
23829
23860
  function isBackEdge2(ctx, value) {
23830
- const backEdges = ctx[STATE2]?.backEdges;
23861
+ const backEdges = ctx[STATE3]?.backEdges;
23831
23862
  return backEdges !== void 0 && value !== null && typeof value === "object" && backEdges.has(value);
23832
23863
  }
23833
23864
 
@@ -27052,7 +27083,8 @@ var Cap = Requested.extend({ models: external_exports.array(external_exports.str
27052
27083
  var DeployedWorkspace = external_exports.object({
27053
27084
  agents: external_exports.array(external_exports.looseObject({ name: external_exports.string(), role: external_exports.string(), color: external_exports.string(), model: external_exports.string(), runsOn: external_exports.enum(["hosted", "local"]).optional(), subagents: external_exports.record(external_exports.string(), Requested).optional(), subagentCap: Cap.optional() })),
27054
27085
  humans: external_exports.array(external_exports.looseObject({ name: external_exports.string(), color: external_exports.string().optional() })).optional(),
27055
- subagentCap: Cap.optional()
27086
+ subagentCap: Cap.optional(),
27087
+ hopLimit: external_exports.int().min(1).nullable().optional()
27056
27088
  });
27057
27089
  var Upgrade = external_exports.object({
27058
27090
  to: external_exports.string(),
@@ -27085,6 +27117,7 @@ var PENDING_FROM = `SELECT d.id, d.message_id, d.session_id, d.bot, m.thread_id,
27085
27117
  s.ended_at IS NOT NULL AS session_ended, ${onLocal("d.bot")} AS on_local,
27086
27118
  (SELECT json_group_array(json_object('id', a.upload_id, 'name', a.name, 'size', a.size)) FROM (SELECT * FROM message_attachments WHERE message_id = m.id ORDER BY position) a) AS attachments
27087
27119
  FROM deliveries d JOIN messages m ON m.id = d.message_id JOIN threads t ON t.id = m.thread_id JOIN sessions s ON s.id = d.session_id`;
27120
+ var UNSETTLED = `d.accepted_at IS NULL AND d.failed_at IS NULL AND d.revoked_at IS NULL AND d.held_at IS NULL AND NOT ${heldByHopLimit("m")}`;
27088
27121
  var TASK_CARD = `(SELECT CASE WHEN json_array_length(json_extract(t.activity, '$.taskCard.tasks')) > 0 THEN json_extract(t.activity, '$.taskCard') END
27089
27122
  FROM turns t WHERE t.id = m.turn_id AND m.author_kind = 'agent' AND t.status NOT IN ${OPEN_TURN_STATUSES}
27090
27123
  AND m.rowid = (SELECT MAX(x.rowid) FROM messages x WHERE x.turn_id = m.turn_id))`;
@@ -27095,7 +27128,7 @@ var DELIVERY_STATUS = `CASE WHEN d.accepted_at IS NOT NULL OR ${LINKED} THEN COA
27095
27128
  WHERE r.session_id = d.session_id AND r.status = 'pending' AND rs.ended_at IS NULL
27096
27129
  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),
27097
27130
  CASE WHEN ${LINKED} THEN 'seen' ELSE 'delivered' END)
27098
- WHEN d.failed_at IS NOT NULL THEN 'failed' WHEN d.revoked_at IS NOT NULL THEN 'skipped'
27131
+ WHEN d.failed_at IS NOT NULL THEN 'failed' WHEN d.revoked_at IS NOT NULL THEN 'skipped' WHEN EXISTS (SELECT 1 FROM messages hm WHERE hm.id = d.message_id AND ${heldByHopLimit("hm")}) THEN 'blocked: hop limit'
27099
27132
  WHEN d.held_at IS NOT NULL THEN CASE d.held_scope WHEN 'bot' THEN 'queued until free' ELSE 'queued' END
27100
27133
  WHEN ${onLocal("d.bot")} AND EXISTS (SELECT 1 FROM local_runner WHERE id = 1 AND device IS NOT NULL AND attachments = 0)
27101
27134
  AND (EXISTS (SELECT 1 FROM message_attachments o WHERE o.message_id = d.message_id)
@@ -27103,12 +27136,18 @@ var DELIVERY_STATUS = `CASE WHEN d.accepted_at IS NOT NULL OR ${LINKED} THEN COA
27103
27136
  THEN 'waiting: the Mac app running this bot is too old to carry images; update it'
27104
27137
  ELSE 'pending' END`;
27105
27138
 
27139
+ // ../../hosted-hub/src/store/local-runner.ts
27140
+ var RESTARTED_TURNS_READ = s(`SELECT t.thread_id, t.bot, r.claimed_at FROM turns t, local_runner r
27141
+ WHERE r.id = 1 AND t.status IN ${OPEN_TURN_STATUSES} AND ${onLocal("t.bot")} AND t.started_at < r.claimed_at
27142
+ AND NOT EXISTS (SELECT 1 FROM session_folds f WHERE f.session_id = t.session_id AND f.folded_at >= r.claimed_at)`);
27143
+
27106
27144
  // ../../hosted-hub/src/store/fold.ts
27107
27145
  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 ORDER BY position)) AS roster,
27108
27146
  (SELECT json_group_array(bot) FROM (${PARTICIPANTS("s.thread_id")})) AS participants,
27109
27147
  (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,
27110
27148
  (SELECT json_object('person', person, 'names', json(names)) FROM human_notify) AS notify,
27111
- (SELECT json_object('status', t.status, 'id', t.id, 'text', t.reply_text, 'lastStep', json(t.last_step), 'activity', json(t.activity)) FROM turns t WHERE t.session_id = s.id AND t.status IN ${OPEN_TURN_STATUSES}
27149
+ (SELECT json_object('status', t.status, 'id', t.id, 'text', t.reply_text, 'lastStep', json(t.last_step), 'activity', json(t.activity),
27150
+ 'trigger', ${triggerOf("t")}) FROM turns t WHERE t.session_id = s.id AND t.status IN ${OPEN_TURN_STATUSES}
27112
27151
  ORDER BY t.started_at DESC LIMIT 1) AS open_turn
27113
27152
  FROM sessions s LEFT JOIN session_folds f ON f.session_id = s.id WHERE s.id = ?`;
27114
27153
  function foldState(row) {
@@ -27132,7 +27171,7 @@ function fold(state, chunks, now = Date.now()) {
27132
27171
  const { session } = state;
27133
27172
  const stmts = [], touched = /* @__PURE__ */ new Map();
27134
27173
  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));
27135
- let open3 = state.open && { ...state.open, lastStep: state.open.lastStep ?? null, status: state.open.status, endedAt: null, error: null };
27174
+ let open3 = state.open && { ...state.open, trigger: state.open.trigger ?? null, lastStep: state.open.lastStep ?? null, status: state.open.status, endedAt: null, error: null };
27136
27175
  const deliver = [];
27137
27176
  let last = state.foldedIndex;
27138
27177
  let at = now;
@@ -27156,6 +27195,7 @@ function fold(state, chunks, now = Date.now()) {
27156
27195
  reduceActivity(open3.activity, { type: "turn.cancelled" }, at, cutOff(open3, "turn.cancelled"));
27157
27196
  Object.assign(open3, { status: open3.status === "cancelling" ? "cancelled" : status, endedAt: at, error: error63 });
27158
27197
  touched.set(open3.id, open3);
27198
+ stmts.push(s("DELETE FROM turn_attachments WHERE turn_id = ?", open3.id));
27159
27199
  log("turn.status", { turnId: open3.id, status: open3.status });
27160
27200
  open3 = null;
27161
27201
  };
@@ -27180,15 +27220,17 @@ function fold(state, chunks, now = Date.now()) {
27180
27220
  if (!data.turnId) throw new Error("turn.started must name eve turnId");
27181
27221
  flush();
27182
27222
  end("cancelled", "superseded by a new turn");
27183
- open3 = { activity: initActivity(), id: turnId(session.id, index), text: "", lastStep: null, status: "streaming", endedAt: null, error: null };
27223
+ open3 = { activity: initActivity(), id: turnId(session.id, index), trigger: null, text: "", lastStep: null, status: "streaming", endedAt: null, error: null };
27184
27224
  touched.set(open3.id, open3);
27185
27225
  stmts.push(s(`INSERT OR IGNORE INTO turns (id, thread_id, session_id, eve_turn_id, bot, status, started_at)
27186
- VALUES (?, ?, ?, ?, ?, 'streaming', ?)`, open3.id, session.thread, session.id, data.turnId, session.bot, at));
27226
+ 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));
27187
27227
  log("turn.status", { turnId: open3.id, status: "streaming" });
27188
27228
  }
27189
27229
  if (!open3) continue;
27190
- for (const deliveryId2 of event.meta?.deliveryIds ?? [])
27230
+ for (const deliveryId2 of event.meta?.deliveryIds ?? []) {
27231
+ open3.trigger ??= messageOfDelivery(deliveryId2, session.bot);
27191
27232
  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));
27233
+ }
27192
27234
  if (event.meta?.deliveryIds?.length) touched.set(open3.id, open3);
27193
27235
  const activityEvent = event.type === "message.completed" ? { ...event, data: { ...data, message: data.message ?? open3.text } } : event;
27194
27236
  if (reduceActivity(open3.activity, activityEvent, at, cutOff(open3, event.type, data.turnId))) {
@@ -27229,6 +27271,10 @@ function fold(state, chunks, now = Date.now()) {
27229
27271
  break;
27230
27272
  }
27231
27273
  case "action.result": {
27274
+ const attachment = data.result?.output?.attachment;
27275
+ if (data.result?.toolName === "attach_image" && data.status === "completed" && typeof attachment?.id === "string" && typeof attachment.name === "string" && typeof attachment.size === "number")
27276
+ stmts.push(s(`INSERT OR IGNORE INTO turn_attachments (turn_id, position, upload_id, name, size)
27277
+ SELECT ?, n, ?, ?, ? FROM (SELECT count(*) AS n FROM turn_attachments WHERE turn_id = ?) WHERE n < ?`, open3.id, attachment.id, attachment.name, attachment.size, open3.id, MAX_ATTACHMENTS));
27232
27278
  const ls = open3.lastStep, callId = data.result?.callId;
27233
27279
  if (!ls || !callId || ls.calls[callId] !== null) break;
27234
27280
  const postOutput = data.result?.output;
@@ -27242,14 +27288,17 @@ function fold(state, chunks, now = Date.now()) {
27242
27288
  break;
27243
27289
  case "turn.completed": {
27244
27290
  flush();
27245
- const text = open3.text.trim();
27246
- if (open3.status !== "cancelling" && !lastActWasOwnPost(open3.lastStep)) {
27291
+ const text = lastActWasOwnPost(open3.lastStep) ? "" : open3.text.trim();
27292
+ if (open3.status !== "cancelling") {
27247
27293
  const messageId2 = derivedMessageId(open3.id);
27248
- stmts.push(s(`INSERT OR IGNORE INTO messages (id, thread_id, author, author_kind, text, turn_id, created_at)
27249
- SELECT ?, ?, ?, 'agent', ?, ?, ? WHERE ? <> ''
27250
- AND EXISTS (SELECT 1 FROM turns WHERE id = ? AND status NOT IN ('cancelling','cancelled') AND NOT (${REVOKED_TURN}))`, messageId2, session.thread, session.bot, text, open3.id, at, text, open3.id));
27294
+ const hop = hopAfter(session.id);
27295
+ stmts.push(s(`INSERT OR IGNORE INTO messages (id, thread_id, author, author_kind, text, turn_id, created_at, hop)
27296
+ SELECT ?, ?, ?, 'agent', ?, ?, ?, ${hop.sql} WHERE (? <> '' OR EXISTS (SELECT 1 FROM turn_attachments WHERE turn_id = ?))
27297
+ AND EXISTS (SELECT 1 FROM turns WHERE id = ? AND status NOT IN ('cancelling','cancelled') AND NOT (${REVOKED_TURN}))`, messageId2, session.thread, session.bot, text, open3.id, at, ...hop.args, text, open3.id, open3.id));
27251
27298
  log("message.created", { messageId: messageId2, author: session.bot, authorKind: "agent" }, IF_INSERTED);
27252
27299
  stmts.push(...notificationFor(messageId2, "agent", text, state.notify ?? void 0));
27300
+ stmts.push(s(`INSERT OR IGNORE INTO message_attachments (message_id, position, upload_id, name, size)
27301
+ 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));
27253
27302
  const to = recipients(state.roster, text, { name: session.bot, kind: "agent" }, session.thread, state.participants);
27254
27303
  stmts.push(...routingStatements(messageId2, session.thread, to, at, { by: session.bot, joins: true }));
27255
27304
  deliver.push(messageId2);
@@ -27297,10 +27346,15 @@ function fold(state, chunks, now = Date.now()) {
27297
27346
  JSON.stringify(t.activity),
27298
27347
  t.id
27299
27348
  ));
27349
+ if (t.endedAt !== null) stmts.push(...failureStatements({ id: t.id, threadId: session.thread, bot: session.bot, trigger: t.trigger, status: t.status, error: t.error }, t.endedAt));
27350
+ }
27351
+ if (last > state.foldedIndex) {
27352
+ stmts.push(s(`INSERT INTO log (thread_id, type, data, at) SELECT t.thread_id, 'turn.status', json_object('turnId', t.id, 'status', t.status), ?
27353
+ FROM turns t, local_runner r WHERE t.session_id = ? AND r.id = 1 AND t.status IN ${OPEN_TURN_STATUSES} AND t.started_at < r.claimed_at AND ${onLocal("t.bot")}
27354
+ AND NOT EXISTS (SELECT 1 FROM session_folds f WHERE f.session_id = t.session_id AND f.folded_at >= r.claimed_at)`, now, session.id));
27355
+ stmts.push(s(`INSERT INTO session_folds (session_id, folded_index, folded_at) VALUES (?, ?, ?)
27356
+ ON CONFLICT (session_id) DO UPDATE SET folded_index = excluded.folded_index, folded_at = excluded.folded_at`, session.id, last, now));
27300
27357
  }
27301
- if (last > state.foldedIndex)
27302
- stmts.push(s(`INSERT INTO session_folds (session_id, folded_index) VALUES (?, ?)
27303
- ON CONFLICT (session_id) DO UPDATE SET folded_index = excluded.folded_index`, session.id, last));
27304
27358
  return { statements: stmts, deliver };
27305
27359
  }
27306
27360
  async function foldInTx(tx, sessionId2, chunks) {