@goodfish/jarvy 0.3.0 → 0.4.0

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.
@@ -11,7 +11,7 @@ import { createServer } from "node:http";
11
11
  import { getRequestListener } from "@hono/node-server";
12
12
 
13
13
  // src/server/api/app.ts
14
- import { Hono as Hono9 } from "hono";
14
+ import { Hono as Hono10 } from "hono";
15
15
  import { HTTPException as HTTPException9 } from "hono/http-exception";
16
16
  import { serveStatic } from "@hono/node-server/serve-static";
17
17
 
@@ -432,6 +432,26 @@ function botRoutes(bots) {
432
432
  // src/server/api/routes/chats.ts
433
433
  import { Hono as Hono5 } from "hono";
434
434
  import { HTTPException as HTTPException7 } from "hono/http-exception";
435
+
436
+ // src/server/api/views/download.ts
437
+ var MEDIA_TYPES = /* @__PURE__ */ new Set(["video/webm", "video/mp4", "video/quicktime", "audio/mpeg", "audio/wav", "audio/x-wav", "audio/ogg", "audio/webm", "audio/mp4"]);
438
+ function downloadType(mimeType) {
439
+ const type = mimeType.split(";")[0].trim().toLowerCase();
440
+ return MEDIA_TYPES.has(type) ? type : "application/octet-stream";
441
+ }
442
+ function byteRange(header, size) {
443
+ const match = header?.trim().match(/^bytes=(\d*)-(\d*)$/);
444
+ if (!match || !match[1] && !match[2]) return null;
445
+ if (!match[1]) {
446
+ const suffix = Number(match[2]);
447
+ return suffix === 0 ? "unsatisfiable" : { start: Math.max(0, size - suffix), end: size - 1 };
448
+ }
449
+ const start = Number(match[1]), end = match[2] ? Number(match[2]) : Infinity;
450
+ if (end < start) return null;
451
+ return start >= size ? "unsatisfiable" : { start, end: Math.min(end, size - 1) };
452
+ }
453
+
454
+ // src/server/api/routes/chats.ts
435
455
  function chatRoutes({ chats, bots }) {
436
456
  const routes = new Hono5();
437
457
  const view = (chat) => chatView(chat, (id) => chats.member(id));
@@ -484,11 +504,19 @@ function attachmentRoutes(chats) {
484
504
  routes.get("/:id", (c) => {
485
505
  const attachment = chats.download(c.get("actorId"), c.req.param("id"));
486
506
  if (!attachment) throw new HTTPException7(404, { message: "Attachment not found" });
487
- return c.body(new Uint8Array(attachment.data), 200, {
488
- "Content-Type": "application/octet-stream",
489
- "Content-Length": String(attachment.data.length),
507
+ const size = attachment.data.length, range = byteRange(c.req.header("Range"), size);
508
+ const headers2 = {
509
+ "Content-Type": downloadType(attachment.file.mimeType),
510
+ "Accept-Ranges": "bytes",
490
511
  "Content-Disposition": `attachment; filename*=UTF-8''${encodeURIComponent(attachment.file.name)}`
491
- });
512
+ };
513
+ if (range === "unsatisfiable") return c.body(null, 416, { ...headers2, "Content-Range": `bytes */${size}` });
514
+ if (!range) return c.body(new Uint8Array(attachment.data), 200, { ...headers2, "Content-Length": String(size) });
515
+ return c.body(
516
+ new Uint8Array(attachment.data.subarray(range.start, range.end + 1)),
517
+ 206,
518
+ { ...headers2, "Content-Length": String(range.end - range.start + 1), "Content-Range": `bytes ${range.start}-${range.end}/${size}` }
519
+ );
492
520
  });
493
521
  routes.get("/:id/run", (c) => {
494
522
  const attachment = chats.download(c.get("actorId"), c.req.param("id"));
@@ -685,12 +713,68 @@ function channelRoutes({ chats }) {
685
713
  return routes;
686
714
  }
687
715
 
716
+ // src/server/api/routes/live.ts
717
+ import { Hono as Hono9 } from "hono";
718
+ function liveRoutes({ account, bots }) {
719
+ const routes = new Hono9();
720
+ routes.get("/bots/:id/live", (c) => {
721
+ const token = c.get("sessionToken"), signal = c.req.raw.signal, live = bots.live(c.req.param("id"));
722
+ let disconnect;
723
+ const body = new ReadableStream({
724
+ start(controller) {
725
+ let ended = false;
726
+ const encoder = new TextEncoder();
727
+ const send = (event, data) => {
728
+ if (ended) return;
729
+ if ((controller.desiredSize ?? 0) <= 0) {
730
+ disconnect();
731
+ return;
732
+ }
733
+ controller.enqueue(encoder.encode(`event: ${event}
734
+ data: ${JSON.stringify(data)}
735
+
736
+ `));
737
+ };
738
+ const { turn, unsubscribe } = live.subscribe((update) => {
739
+ if (!account.authenticated(token)) {
740
+ disconnect();
741
+ return;
742
+ }
743
+ send(update.type, update);
744
+ }, () => disconnect());
745
+ const heartbeat = setInterval(() => {
746
+ if (!account.authenticated(token)) disconnect();
747
+ else send("heartbeat", {});
748
+ }, 15e3);
749
+ heartbeat.unref();
750
+ const aborted = () => disconnect();
751
+ disconnect = (close = true) => {
752
+ if (ended) return;
753
+ ended = true;
754
+ clearInterval(heartbeat);
755
+ unsubscribe();
756
+ signal.removeEventListener("abort", aborted);
757
+ if (close) controller.close();
758
+ };
759
+ signal.addEventListener("abort", aborted, { once: true });
760
+ if (signal.aborted) disconnect();
761
+ else send("snapshot", { turn, now: Date.now() });
762
+ },
763
+ cancel() {
764
+ disconnect(false);
765
+ }
766
+ }, new ByteLengthQueuingStrategy({ highWaterMark: c.env.outgoing.writableHighWaterMark }));
767
+ return c.body(body, 200, { "Content-Type": "text/event-stream", Connection: "keep-alive", "X-Accel-Buffering": "no" });
768
+ });
769
+ return routes;
770
+ }
771
+
688
772
  // src/server/api/app.ts
689
773
  function createHttpApp(services, webRoot) {
690
- const app = new Hono9();
774
+ const app = new Hono10();
691
775
  app.use("*", headers);
692
776
  app.onError(handleError);
693
- const api = new Hono9();
777
+ const api = new Hono10();
694
778
  api.use("*", apiSecurity);
695
779
  installAuthentication(api, services.account);
696
780
  api.route("/bots", botRoutes(services.bots));
@@ -699,6 +783,7 @@ function createHttpApp(services, webRoot) {
699
783
  api.route("/attachments", attachmentRoutes(services.chats));
700
784
  api.route("/", modelRoutes(services.models));
701
785
  api.route("/", eventRoutes(services));
786
+ api.route("/", liveRoutes(services));
702
787
  api.all("*", () => {
703
788
  throw new HTTPException9(404, { message: "API route not found" });
704
789
  });
@@ -3092,14 +3177,14 @@ function messageTargets({ dm, sender, members, mentioned, replied, subscribers }
3092
3177
  if (dm) return members.filter((member) => member.id !== sender.id).map((member) => ({ member, reason: "dm" }));
3093
3178
  const targets = mentioned.filter((member) => member.id !== sender.id).map((member) => ({ member, reason: "mention" }));
3094
3179
  const add = (member, reason) => {
3095
- if (member.id !== sender.id && !targets.some((target) => target.member.id === member.id)) targets.push({ member, reason });
3180
+ if (member.id !== sender.id && !targets.some((target2) => target2.member.id === member.id)) targets.push({ member, reason });
3096
3181
  };
3097
3182
  if (replied && members.some((member) => member.id === replied.id)) add(replied, "reply");
3098
3183
  for (const member of members) if (subscribers.includes(member.id)) add(member, "subscription");
3099
3184
  return targets;
3100
3185
  }
3101
3186
  function echoTargets({ sender, members, subscribers, reached }) {
3102
- return members.filter((member) => member.id !== sender.id && subscribers.includes(member.id) && !reached.some((target) => target.member.id === member.id)).map((member) => ({ member, reason: "subscription" }));
3187
+ return members.filter((member) => member.id !== sender.id && subscribers.includes(member.id) && !reached.some((target2) => target2.member.id === member.id)).map((member) => ({ member, reason: "subscription" }));
3103
3188
  }
3104
3189
  function reactionTargets({ actorId, author }) {
3105
3190
  return author && author.id !== actorId ? [{ member: author, reason: "author" }] : [];
@@ -3126,9 +3211,9 @@ function mutedUrgency(reason, urgency) {
3126
3211
 
3127
3212
  // src/server/service/delivery/delivery.ts
3128
3213
  function plan(kind, targets, muted = []) {
3129
- return targets.flatMap((target) => {
3130
- const urgency = urgencyOf(kind, target.reason);
3131
- return urgency ? [{ ...target, kind, urgency: muted.includes(target.member.id) ? mutedUrgency(target.reason, urgency) : urgency }] : [];
3214
+ return targets.flatMap((target2) => {
3215
+ const urgency = urgencyOf(kind, target2.reason);
3216
+ return urgency ? [{ ...target2, kind, urgency: muted.includes(target2.member.id) ? mutedUrgency(target2.reason, urgency) : urgency }] : [];
3132
3217
  });
3133
3218
  }
3134
3219
  var Dispatcher = class {
@@ -3650,9 +3735,9 @@ var ChatService = class {
3650
3735
  /** A DM with another member of the actor's space (a Bot DMs its owner or another Bot); one per pair. */
3651
3736
  openDirect(actorId, targetId) {
3652
3737
  const owner = this.repository.channels.ownerOf(actorId), directory = this.repository.channels.directory(owner);
3653
- const actor = directory.find((member) => member.id === actorId), target = directory.find((member) => member.id === targetId);
3654
- if (!actor || !target) throw new ChatError(404, "Member not found");
3655
- const chat = this.repository.openDirect(actorId, targetId, owner, `${actor.name} \xB7 ${target.name}`);
3738
+ const actor = directory.find((member) => member.id === actorId), target2 = directory.find((member) => member.id === targetId);
3739
+ if (!actor || !target2) throw new ChatError(404, "Member not found");
3740
+ const chat = this.repository.openDirect(actorId, targetId, owner, `${actor.name} \xB7 ${target2.name}`);
3656
3741
  this.notifySaved(chat.id);
3657
3742
  return chat;
3658
3743
  }
@@ -3964,8 +4049,8 @@ var Changes = class {
3964
4049
  // src/server/service/bot/activity.ts
3965
4050
  function activityPage(messages, history, limit) {
3966
4051
  const candidates2 = [
3967
- ...messages.map((record) => ({ kind: "chat", record })),
3968
- ...history.map((record) => ({ kind: "history", record }))
4052
+ ...messages.map((record2) => ({ kind: "chat", record: record2 })),
4053
+ ...history.map((record2) => ({ kind: "history", record: record2 }))
3969
4054
  ];
3970
4055
  candidates2.sort((a, b) => {
3971
4056
  if (a.record.timestamp !== b.record.timestamp) return b.record.timestamp - a.record.timestamp;
@@ -4121,7 +4206,74 @@ function configureHttp(timeoutMs) {
4121
4206
 
4122
4207
  // src/server/infra/pi/session.ts
4123
4208
  import { Type } from "typebox";
4209
+
4210
+ // src/server/infra/pi/live.ts
4211
+ var LiveTranslator = class {
4212
+ message = 0;
4213
+ open = /* @__PURE__ */ new Set();
4214
+ translate(event) {
4215
+ switch (event.type) {
4216
+ case "message_start":
4217
+ if (event.message.role === "assistant") this.message++;
4218
+ return [];
4219
+ case "message_update": {
4220
+ const update = event.assistantMessageEvent;
4221
+ if (update.type !== "thinking_start" && update.type !== "thinking_delta" && update.type !== "thinking_end" && update.type !== "text_start" && update.type !== "text_delta" && update.type !== "text_end") return [];
4222
+ const key = `${this.message}:${update.contentIndex}`, kind = update.type.startsWith("thinking") ? "thinking" : "text";
4223
+ const events = [];
4224
+ if (!this.open.has(key) && !update.type.endsWith("_end")) {
4225
+ this.open.add(key);
4226
+ events.push({ type: "block_start", key, kind });
4227
+ }
4228
+ if (update.type.endsWith("_delta") && "delta" in update && update.delta) events.push({ type: "block_delta", key, kind, delta: update.delta });
4229
+ if (update.type.endsWith("_end") && this.open.delete(key)) events.push({ type: "block_end", key });
4230
+ return events;
4231
+ }
4232
+ case "message_end": {
4233
+ if (event.message.role !== "assistant") return [];
4234
+ const prefix = `${this.message}:`, closing = [...this.open].filter((key) => key.startsWith(prefix));
4235
+ for (const key of closing) this.open.delete(key);
4236
+ return closing.map((key) => ({ type: "block_end", key }));
4237
+ }
4238
+ case "tool_execution_start":
4239
+ return [{ type: "tool_start", id: event.toolCallId, parentId: event.parentToolCallId ?? null, name: event.toolName, args: event.args }];
4240
+ case "tool_execution_end":
4241
+ return [{ type: "tool_end", id: event.toolCallId, output: resultText(event.result), failed: event.isError }];
4242
+ case "compaction_start":
4243
+ return [{ type: "compaction_start" }];
4244
+ case "compaction_end":
4245
+ return [{
4246
+ type: "compaction_end",
4247
+ failed: !!event.errorMessage || event.aborted,
4248
+ error: event.errorMessage ?? null,
4249
+ tokensBefore: event.result?.tokensBefore ?? null,
4250
+ tokensAfter: event.result?.estimatedTokensAfter ?? null
4251
+ }];
4252
+ case "auto_retry_start":
4253
+ return [{ type: "retry_start", attempt: event.attempt, maxAttempts: event.maxAttempts, error: event.errorMessage }];
4254
+ case "auto_retry_end":
4255
+ return [{ type: "retry_end", failed: !event.success, error: event.finalError ?? null }];
4256
+ default:
4257
+ return [];
4258
+ }
4259
+ }
4260
+ };
4261
+ function resultText(result) {
4262
+ const content2 = result?.content;
4263
+ if (Array.isArray(content2)) return content2.map((part) => part?.type === "text" ? String(part.text ?? "") : part?.type === "image" ? "[image]" : "").filter(Boolean).join("\n");
4264
+ if (typeof result === "string") return result;
4265
+ try {
4266
+ return JSON.stringify(result) ?? "";
4267
+ } catch {
4268
+ return String(result);
4269
+ }
4270
+ }
4271
+
4272
+ // src/server/infra/pi/session.ts
4124
4273
  var HTTP_IDLE_TIMEOUT_MS = 9e5;
4274
+ function compactionReserve(contextWindow) {
4275
+ return Math.min(65536, Math.max(16384, Math.min(32768, Math.floor(contextWindow / 4)), Math.floor(contextWindow * 0.13)));
4276
+ }
4125
4277
  var PiSession = class _PiSession {
4126
4278
  constructor(options, models, credentials) {
4127
4279
  this.options = options;
@@ -4129,6 +4281,7 @@ var PiSession = class _PiSession {
4129
4281
  this.credentials = credentials;
4130
4282
  }
4131
4283
  session;
4284
+ settings;
4132
4285
  unsubscribe;
4133
4286
  unsubscribeHistory;
4134
4287
  promptActive = false;
@@ -4140,6 +4293,7 @@ var PiSession = class _PiSession {
4140
4293
  abortFailure;
4141
4294
  result = { outcome: "completed" };
4142
4295
  signature = "";
4296
+ translator = new LiveTranslator();
4143
4297
  get context() {
4144
4298
  const usage = this.session.getContextUsage();
4145
4299
  return usage ? { tokens: usage.tokens, contextWindow: usage.contextWindow, percent: usage.percent } : void 0;
@@ -4185,7 +4339,10 @@ var PiSession = class _PiSession {
4185
4339
  const sessionManager = SessionManager.open(reference.file, sessionsDir, bot.workspacePath);
4186
4340
  const adapter = new _PiSession(options, models, credentials);
4187
4341
  configureHttp(HTTP_IDLE_TIMEOUT_MS);
4188
- const settingsManager = SettingsManager.inMemory({ httpIdleTimeoutMs: HTTP_IDLE_TIMEOUT_MS });
4342
+ const settingsManager = SettingsManager.inMemory({
4343
+ httpIdleTimeoutMs: HTTP_IDLE_TIMEOUT_MS,
4344
+ compaction: { reserveTokens: compactionReserve(model.contextWindow) }
4345
+ });
4189
4346
  const tools = ["bash", "read", "edit", "write", "codemode", ...options.tools.map((tool) => tool.name)];
4190
4347
  const resourceLoader = new DefaultResourceLoader({
4191
4348
  cwd: bot.workspacePath,
@@ -4225,6 +4382,7 @@ var PiSession = class _PiSession {
4225
4382
  tools
4226
4383
  });
4227
4384
  adapter.session = result.session;
4385
+ adapter.settings = settingsManager;
4228
4386
  if (result.extensionsResult.errors.length || !tools.every((name2) => adapter.session.getActiveToolNames().includes(name2))) {
4229
4387
  adapter.session.dispose();
4230
4388
  throw new RuntimeFault("tools_missing", "Couldn't load the run's tools.");
@@ -4238,8 +4396,8 @@ var PiSession = class _PiSession {
4238
4396
  for (let index = entries.length - 1; index >= 0; index--) {
4239
4397
  const entry = entries[index];
4240
4398
  if (entry.type !== "message" || entry.message !== event.message) continue;
4241
- const record = history.convert(entry);
4242
- if (record) await options.onHistory(record);
4399
+ const record2 = history.convert(entry);
4400
+ if (record2) await options.onHistory(record2);
4243
4401
  break;
4244
4402
  }
4245
4403
  });
@@ -4281,6 +4439,7 @@ var PiSession = class _PiSession {
4281
4439
  }
4282
4440
  );
4283
4441
  if (event.type === "message_end" && event.message.role === "assistant") this.result = this.classify(event.message);
4442
+ if (this.options.live) for (const live of this.translator.translate(event)) this.options.live(live);
4284
4443
  this.maybeAbort();
4285
4444
  }
4286
4445
  /** The last assistant message decides the prompt (a later retry success replaces an earlier error).
@@ -4324,17 +4483,24 @@ var PiSession = class _PiSession {
4324
4483
  const settled = new Promise((resolve6) => {
4325
4484
  this.settledResolve = resolve6;
4326
4485
  });
4486
+ this.options.live?.({ type: "start" });
4487
+ let outcome = "failed";
4327
4488
  try {
4328
- await this.session.prompt(xml, { expandPromptTemplates: false });
4489
+ try {
4490
+ await this.session.prompt(xml, { expandPromptTemplates: false });
4491
+ } finally {
4492
+ await this.session.waitForIdle();
4493
+ if (this.started && !this.settled) await settled;
4494
+ this.promptActive = false;
4495
+ await this.abortTask;
4496
+ this.options.store.runtime.recordSession(this.options.bot.id, this.sessionId, existsSync(this.sessionFile));
4497
+ }
4498
+ if (this.abortFailure) throw this.abortFailure;
4499
+ outcome = this.result.outcome;
4500
+ return this.result;
4329
4501
  } finally {
4330
- await this.session.waitForIdle();
4331
- if (this.started && !this.settled) await settled;
4332
- this.promptActive = false;
4333
- await this.abortTask;
4334
- this.options.store.runtime.recordSession(this.options.bot.id, this.sessionId, existsSync(this.sessionFile));
4502
+ this.options.live?.({ type: "end", outcome });
4335
4503
  }
4336
- if (this.abortFailure) throw this.abortFailure;
4337
- return this.result;
4338
4504
  }
4339
4505
  async configure(selection) {
4340
4506
  const connection = this.options.store.getModelConnection(selection.connectionId), configured2 = connection?.models.find((item) => item.id === selection.modelId);
@@ -4351,6 +4517,7 @@ var PiSession = class _PiSession {
4351
4517
  this.credentials.current = next;
4352
4518
  try {
4353
4519
  await this.session.setModel(model, { persist: false });
4520
+ this.settings.applyOverrides({ compaction: { reserveTokens: compactionReserve(model.contextWindow) } });
4354
4521
  this.session.setThinkingLevel(resolveThinking(model, selection.thinkingLevel), { persist: false });
4355
4522
  this.signature = signature;
4356
4523
  return this.session.thinkingLevel;
@@ -4440,17 +4607,17 @@ var HistoryRepository = class {
4440
4607
  const row = this.db.prepare("SELECT * FROM history WHERE id=?").get(id);
4441
4608
  return row ? this.decode(row) : null;
4442
4609
  }
4443
- links(record) {
4610
+ links(record2) {
4444
4611
  const links = [];
4445
- for (const block of record.message.content) {
4612
+ for (const block of record2.message.content) {
4446
4613
  if (block.type !== "tool_call" && block.type !== "tool_result") continue;
4447
4614
  const callId = block.tool_call_id;
4448
4615
  if (block.type === "tool_result") {
4449
- const call = this.db.prepare(`SELECT id,role FROM history WHERE id<? AND ${CALL} ORDER BY id DESC LIMIT 1`).get(record.id, "tool_call", callId);
4616
+ const call = this.db.prepare(`SELECT id,role FROM history WHERE id<? AND ${CALL} ORDER BY id DESC LIMIT 1`).get(record2.id, "tool_call", callId);
4450
4617
  if (call) links.push({ ...call, tool_call_id: callId });
4451
4618
  } else {
4452
- const next = this.db.prepare(`SELECT id FROM history WHERE id>? AND ${CALL} ORDER BY id LIMIT 1`).get(record.id, "tool_call", callId);
4453
- const results = this.db.prepare(`SELECT id,role FROM history WHERE id>? AND id<? AND ${CALL} ORDER BY id`).all(record.id, next?.id ?? Number.MAX_SAFE_INTEGER, "tool_result", callId);
4619
+ const next = this.db.prepare(`SELECT id FROM history WHERE id>? AND ${CALL} ORDER BY id LIMIT 1`).get(record2.id, "tool_call", callId);
4620
+ const results = this.db.prepare(`SELECT id,role FROM history WHERE id>? AND id<? AND ${CALL} ORDER BY id`).all(record2.id, next?.id ?? Number.MAX_SAFE_INTEGER, "tool_result", callId);
4454
4621
  links.push(...results.map((result) => ({ ...result, tool_call_id: callId })));
4455
4622
  }
4456
4623
  }
@@ -4494,14 +4661,14 @@ var Memory = class {
4494
4661
  return this.history().usage(since);
4495
4662
  }
4496
4663
  read(id) {
4497
- const record = this.history().read(id);
4498
- return record ? { record, links: this.history().links(record) } : null;
4664
+ const record2 = this.history().read(id);
4665
+ return record2 ? { record: record2, links: this.history().links(record2) } : null;
4499
4666
  }
4500
4667
  readMany(ids, mode) {
4501
4668
  const found = mode === "summary" ? this.history().summaries(ids).map((item) => item && summaryLine(item)) : this.history().details(ids);
4502
4669
  const result = { mode, records: [], missingIds: [] };
4503
- found.forEach((record, index) => {
4504
- if (record) result.records.push(record);
4670
+ found.forEach((record2, index) => {
4671
+ if (record2) result.records.push(record2);
4505
4672
  else result.missingIds.push(ids[index]);
4506
4673
  });
4507
4674
  return result;
@@ -4629,17 +4796,17 @@ var Bot = class extends Member {
4629
4796
  workspace: this.workspace.location(),
4630
4797
  tools: this.tools.definitions(),
4631
4798
  onStatus: (status) => this.updateState(status),
4632
- onHistory: (record) => this.recordHistory(record)
4799
+ onHistory: (record2) => this.recordHistory(record2)
4633
4800
  };
4634
4801
  }
4635
4802
  updateState(status) {
4636
4803
  this.state.update(status);
4637
4804
  }
4638
- async recordHistory(record) {
4639
- const historyId = await this.memory.append(record);
4640
- if (record.role !== "assistant") return;
4805
+ async recordHistory(record2) {
4806
+ const historyId = await this.memory.append(record2);
4807
+ if (record2.role !== "assistant") return;
4641
4808
  this.callHistory.clear();
4642
- for (const block of record.message.content) {
4809
+ for (const block of record2.message.content) {
4643
4810
  if (block.type === "tool_call") this.callHistory.set(block.tool_call_id, historyId);
4644
4811
  }
4645
4812
  }
@@ -4798,20 +4965,20 @@ ${content(block.result.content, heading + "#")}`;
4798
4965
  function formatHistory(result, format) {
4799
4966
  if (format === "json") return JSON.stringify(result, null, 2);
4800
4967
  const sections = [`# History (${result.mode})`];
4801
- for (const record of result.records) {
4802
- sections.push(`## #${record.id} \xB7 ${record.role}`, `Time: ${new Date(record.timestamp).toISOString()}`);
4803
- if (record.model) sections.push(`Model:
4968
+ for (const record2 of result.records) {
4969
+ sections.push(`## #${record2.id} \xB7 ${record2.role}`, `Time: ${new Date(record2.timestamp).toISOString()}`);
4970
+ if (record2.model) sections.push(`Model:
4804
4971
 
4805
- ${jsonBlock(record.model)}`);
4806
- if ("preview" in record) {
4807
- sections.push(fence(record.preview));
4972
+ ${jsonBlock(record2.model)}`);
4973
+ if ("preview" in record2) {
4974
+ sections.push(fence(record2.preview));
4808
4975
  continue;
4809
4976
  }
4810
- sections.push(content(record.message.content));
4811
- if (record.message.tools !== void 0) sections.push("### Tool definitions", jsonBlock(record.message.tools));
4812
- if (record.message.tools_removed !== void 0) sections.push("### Tools removed", jsonBlock(record.message.tools_removed));
4813
- if (record.message.stop_reason !== void 0) sections.push("### Stop reason", fence(record.message.stop_reason));
4814
- if (record.message.error !== void 0) sections.push("### Error", fence(record.message.error));
4977
+ sections.push(content(record2.message.content));
4978
+ if (record2.message.tools !== void 0) sections.push("### Tool definitions", jsonBlock(record2.message.tools));
4979
+ if (record2.message.tools_removed !== void 0) sections.push("### Tools removed", jsonBlock(record2.message.tools_removed));
4980
+ if (record2.message.stop_reason !== void 0) sections.push("### Stop reason", fence(record2.message.stop_reason));
4981
+ if (record2.message.error !== void 0) sections.push("### Error", fence(record2.message.error));
4815
4982
  }
4816
4983
  if (result.missingIds.length) sections.push(`## Missing IDs
4817
4984
 
@@ -5739,34 +5906,288 @@ When something will take a while, post a short progress note partway through, an
5739
5906
  `;
5740
5907
  }
5741
5908
 
5909
+ // src/server/shared/live.ts
5910
+ var LIVE_STEPS = 300;
5911
+ var LIVE_TEXT = 4e3;
5912
+ var LIVE_OUTPUT_END = 1200;
5913
+ var LIVE_INPUT = 300;
5914
+ var LIVE_MESSAGE = 2e3;
5915
+
5916
+ // src/server/service/bot/live-input.ts
5917
+ function clip(text3, length) {
5918
+ return text3.length > length ? `${text3.slice(0, length - 1)}\u2026` : text3;
5919
+ }
5920
+ var field = (value2, length = LIVE_INPUT) => typeof value2 === "string" ? clip(value2, length) : void 0;
5921
+ var record = (value2) => value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : {};
5922
+ function liveInput(tool, args, chat) {
5923
+ const values = record(args);
5924
+ switch (tool) {
5925
+ case "bash":
5926
+ return { command: field(values.command) };
5927
+ case "read":
5928
+ case "edit":
5929
+ case "write":
5930
+ return { path: field(values.path) };
5931
+ case "codemode":
5932
+ return { command: field(values.code) };
5933
+ case "chat":
5934
+ return gateway(values, chat);
5935
+ default: {
5936
+ let json2 = "";
5937
+ try {
5938
+ json2 = JSON.stringify(args) ?? "";
5939
+ } catch {
5940
+ }
5941
+ return json2 ? { args: clip(json2, LIVE_INPUT) } : {};
5942
+ }
5943
+ }
5944
+ }
5945
+ function gateway(values, chat) {
5946
+ const action = values.action === "call" && typeof values.tool === "string" ? values.tool : typeof values.action === "string" ? values.action : "";
5947
+ const inner = record(values.args), where = target(inner, chat);
5948
+ if (action === "messages.send") return { action, chat: where, text: field(inner.message, LIVE_MESSAGE) };
5949
+ if (action === "messages.ask") return {
5950
+ action,
5951
+ chat: where,
5952
+ text: field(inner.question, LIVE_MESSAGE),
5953
+ options: Array.isArray(inner.options) ? inner.options.map((option2) => field(record(option2).label) ?? "").filter(Boolean) : void 0
5954
+ };
5955
+ return where ? { action, chat: where } : { action };
5956
+ }
5957
+ function target(inner, chat) {
5958
+ if (typeof inner.to === "string") return { id: null, type: "dm", title: clip(inner.to, 80) };
5959
+ return typeof inner.chatId === "string" ? chat(inner.chatId) ?? void 0 : void 0;
5960
+ }
5961
+ function sentChat(output, chat) {
5962
+ try {
5963
+ const chatId = JSON.parse(output).chatId;
5964
+ return typeof chatId === "string" ? chat(chatId) ?? void 0 : void 0;
5965
+ } catch {
5966
+ return void 0;
5967
+ }
5968
+ }
5969
+
5970
+ // src/server/service/bot/live.ts
5971
+ var FLUSH_MS = 100;
5972
+ function liveOutput(text3) {
5973
+ if (!text3) return null;
5974
+ if (text3.length <= 2 * LIVE_OUTPUT_END) return { head: text3, tail: "", omitted: 0 };
5975
+ return { head: text3.slice(0, LIVE_OUTPUT_END), tail: text3.slice(-LIVE_OUTPUT_END), omitted: text3.length - 2 * LIVE_OUTPUT_END };
5976
+ }
5977
+ var LiveTurns = class {
5978
+ constructor(chat) {
5979
+ this.chat = chat;
5980
+ }
5981
+ turn = null;
5982
+ steps = /* @__PURE__ */ new Map();
5983
+ blocks = /* @__PURE__ */ new Map();
5984
+ compaction = null;
5985
+ retry = null;
5986
+ counter = 0;
5987
+ pending = [];
5988
+ timer = null;
5989
+ listeners = /* @__PURE__ */ new Map();
5990
+ /** Listens from now on and returns the turn so far (null when the Bot is not running one). Pending changes go to
5991
+ * the earlier listeners first, so the snapshot and the next patch never overlap. */
5992
+ subscribe(listener, close) {
5993
+ this.flush();
5994
+ this.listeners.set(listener, close);
5995
+ return { turn: this.turn ? structuredClone(this.turn) : null, unsubscribe: () => {
5996
+ this.listeners.delete(listener);
5997
+ } };
5998
+ }
5999
+ /** Ends every listener's stream (the Bot was deleted, or the server is stopping). */
6000
+ close() {
6001
+ for (const close of [...this.listeners.values()]) close();
6002
+ this.listeners.clear();
6003
+ }
6004
+ event(event) {
6005
+ if (event.type === "start") {
6006
+ this.flush();
6007
+ this.reset();
6008
+ this.turn = { startedAt: Date.now(), steps: [], dropped: 0 };
6009
+ this.emit({ type: "start", turn: structuredClone(this.turn) });
6010
+ return;
6011
+ }
6012
+ if (!this.turn) return;
6013
+ switch (event.type) {
6014
+ case "end": {
6015
+ for (const step of this.turn.steps) if (step.state === "running") this.finish(step, event.outcome !== "completed");
6016
+ this.flush();
6017
+ this.emit({ type: "end", outcome: event.outcome, endedAt: Date.now() });
6018
+ this.reset();
6019
+ return;
6020
+ }
6021
+ case "block_start":
6022
+ this.blocks.set(event.key, this.add({ kind: event.kind, text: "", omitted: 0 }).id);
6023
+ return;
6024
+ case "block_delta": {
6025
+ let id = this.blocks.get(event.key);
6026
+ if (!id) {
6027
+ id = this.add({ kind: event.kind, text: "", omitted: 0 }).id;
6028
+ this.blocks.set(event.key, id);
6029
+ }
6030
+ const step = this.steps.get(id);
6031
+ if (!step) return;
6032
+ const text3 = (step.text ?? "") + event.delta, over = Math.max(0, text3.length - LIVE_TEXT);
6033
+ step.text = text3.slice(over);
6034
+ step.omitted = (step.omitted ?? 0) + over;
6035
+ this.queue({ op: "text", id, append: event.delta });
6036
+ return;
6037
+ }
6038
+ case "block_end": {
6039
+ const step = this.steps.get(this.blocks.get(event.key) ?? "");
6040
+ if (step) this.finish(step, false);
6041
+ this.blocks.delete(event.key);
6042
+ return;
6043
+ }
6044
+ case "tool_start":
6045
+ this.add({ id: event.id, kind: "tool", parentId: event.parentId, tool: event.name, input: liveInput(event.name, event.args, this.chat) });
6046
+ return;
6047
+ case "tool_end": {
6048
+ const step = this.steps.get(event.id);
6049
+ if (!step) return;
6050
+ step.output = liveOutput(event.output);
6051
+ if (step.input?.action === "messages.send" && !event.failed) step.input.chat = sentChat(event.output, this.chat) ?? step.input.chat;
6052
+ this.finish(step, event.failed);
6053
+ return;
6054
+ }
6055
+ case "compaction_start":
6056
+ this.compaction = this.add({ kind: "compaction" }).id;
6057
+ return;
6058
+ case "compaction_end": {
6059
+ const step = this.steps.get(this.compaction ?? "");
6060
+ this.compaction = null;
6061
+ if (!step) return;
6062
+ step.tokensBefore = event.tokensBefore;
6063
+ step.tokensAfter = event.tokensAfter;
6064
+ step.output = event.error ? liveOutput(event.error) : null;
6065
+ this.finish(step, event.failed);
6066
+ return;
6067
+ }
6068
+ case "retry_start": {
6069
+ const previous = this.steps.get(this.retry ?? "");
6070
+ if (previous?.state === "running") this.finish(previous, true);
6071
+ this.retry = this.add({ kind: "retry", attempt: event.attempt, maxAttempts: event.maxAttempts, output: liveOutput(event.error) }).id;
6072
+ return;
6073
+ }
6074
+ case "retry_end": {
6075
+ const step = this.steps.get(this.retry ?? "");
6076
+ this.retry = null;
6077
+ if (step) {
6078
+ if (event.error) step.output = liveOutput(event.error);
6079
+ this.finish(step, event.failed);
6080
+ }
6081
+ return;
6082
+ }
6083
+ }
6084
+ }
6085
+ add(fields) {
6086
+ const turn = this.turn;
6087
+ if (turn.steps.length >= LIVE_STEPS) {
6088
+ const index = Math.max(0, turn.steps.findIndex((step2) => step2.state !== "running")), [gone] = turn.steps.splice(index, 1);
6089
+ this.steps.delete(gone.id);
6090
+ turn.dropped++;
6091
+ this.queue({ op: "drop", id: gone.id });
6092
+ }
6093
+ const step = { id: `s${++this.counter}`, state: "running", startedAt: Date.now(), endedAt: null, parentId: null, ...fields };
6094
+ turn.steps.push(step);
6095
+ this.steps.set(step.id, step);
6096
+ this.queue({ op: "step", step: structuredClone(step) });
6097
+ return step;
6098
+ }
6099
+ finish(step, failed) {
6100
+ step.state = failed ? "failed" : "ok";
6101
+ step.endedAt = Date.now();
6102
+ this.queue({ op: "step", step: structuredClone(step) });
6103
+ }
6104
+ queue(patch) {
6105
+ this.pending.push(patch);
6106
+ this.timer ??= setTimeout(() => this.flush(), FLUSH_MS);
6107
+ }
6108
+ /** Sends the pending changes as one patch: text streamed into the same step is joined, and a step sent whole
6109
+ * replaces what came before it for that step. A step is copied when it is queued, so text streamed after that
6110
+ * follows it as its own patch. */
6111
+ flush() {
6112
+ if (this.timer) {
6113
+ clearTimeout(this.timer);
6114
+ this.timer = null;
6115
+ }
6116
+ if (!this.pending.length) return;
6117
+ const patches = [];
6118
+ for (const patch of this.pending) {
6119
+ const last = patches.at(-1);
6120
+ if (patch.op === "text" && last?.op === "text" && last.id === patch.id) {
6121
+ last.append += patch.append;
6122
+ continue;
6123
+ }
6124
+ if (patch.op === "step") {
6125
+ for (let index = patches.length - 1; index >= 0; index--) {
6126
+ const item = patches[index];
6127
+ if ((item.op === "step" ? item.step.id : item.id) === patch.step.id) patches.splice(index, 1);
6128
+ }
6129
+ patches.push(patch);
6130
+ continue;
6131
+ }
6132
+ patches.push({ ...patch });
6133
+ }
6134
+ this.pending = [];
6135
+ this.emit({ type: "patch", patches });
6136
+ }
6137
+ emit(update) {
6138
+ for (const listener of [...this.listeners.keys()]) {
6139
+ try {
6140
+ listener(update);
6141
+ } catch (error) {
6142
+ console.error("[live] listener failed", { error });
6143
+ }
6144
+ }
6145
+ }
6146
+ reset() {
6147
+ this.turn = null;
6148
+ this.steps.clear();
6149
+ this.blocks.clear();
6150
+ this.compaction = null;
6151
+ this.retry = null;
6152
+ this.counter = 0;
6153
+ this.pending = [];
6154
+ if (this.timer) {
6155
+ clearTimeout(this.timer);
6156
+ this.timer = null;
6157
+ }
6158
+ }
6159
+ };
6160
+
5742
6161
  // src/server/service/bot/service.ts
5743
6162
  var BotService = class {
5744
6163
  constructor(deps) {
5745
6164
  this.deps = deps;
5746
6165
  }
5747
6166
  bots = /* @__PURE__ */ new Map();
6167
+ lives = /* @__PURE__ */ new Map();
5748
6168
  closed = false;
5749
6169
  started = false;
5750
6170
  get(id) {
5751
6171
  if (this.closed) throw new RuntimeFault("stopping", "Shutting down.", 503);
5752
- const record = this.deps.store.getBot(id);
5753
- if (!record) throw new RuntimeFault("bot_missing", "Bot not found", 404);
6172
+ const record2 = this.deps.store.getBot(id);
6173
+ if (!record2) throw new RuntimeFault("bot_missing", "Bot not found", 404);
5754
6174
  let bot = this.bots.get(id);
5755
6175
  if (bot) return bot;
5756
- const { store, chats, files, credentials } = this.deps;
6176
+ const { store, chats, files, credentials } = this.deps, live = this.live(id);
5757
6177
  const runtime = new AgentRuntime(new BotInbox(id, store.runtime, chats, files), (setup, model, reference, activity) => (this.deps.factory ?? PiSession.create)({
5758
6178
  store,
5759
- bot: record,
6179
+ bot: record2,
5760
6180
  selection: model.selection,
5761
6181
  reference,
5762
6182
  credentials,
5763
6183
  tools: setup.tools,
5764
6184
  systemPrompt: () => botPrompt(setup),
5765
6185
  onHistory: setup.onHistory,
5766
- activity
6186
+ activity,
6187
+ live: (event) => live.event(event)
5767
6188
  }));
5768
- const workspace = new Workspace(record.workspacePath);
5769
- const memory = new Memory(join6(dirname5(record.workspacePath), "history.db"));
6189
+ const workspace = new Workspace(record2.workspacePath);
6190
+ const memory = new Memory(join6(dirname5(record2.workspacePath), "history.db"));
5770
6191
  bot = new Bot(id, {
5771
6192
  identity: new Identity(id, store),
5772
6193
  model: new Model(id, store, resolveThinking),
@@ -5776,7 +6197,7 @@ var BotService = class {
5776
6197
  agentRuntime: runtime,
5777
6198
  state: new BotState(() => this.deps.changed(id))
5778
6199
  }, { deliver: async (message) => {
5779
- const outgoing = await files.readOutgoing(record.workspacePath, message.attachments ?? [], message.signal);
6200
+ const outgoing = await files.readOutgoing(record2.workspacePath, message.attachments ?? [], message.signal);
5780
6201
  message.signal?.throwIfAborted();
5781
6202
  return chats.deliver(id, message.chatId, {
5782
6203
  content: message.content,
@@ -5792,23 +6213,40 @@ var BotService = class {
5792
6213
  this.bots.set(id, bot);
5793
6214
  return bot;
5794
6215
  }
5795
- details(record) {
5796
- const model = record.connectionId ? this.deps.store.getModelConnection(record.connectionId)?.models.find((model2) => model2.id === record.modelId) : void 0;
5797
- return { ...record, effectiveThinkingLevel: model ? resolveThinking(model, record.thinkingLevel) : null };
6216
+ details(record2) {
6217
+ const model = record2.connectionId ? this.deps.store.getModelConnection(record2.connectionId)?.models.find((model2) => model2.id === record2.modelId) : void 0;
6218
+ return { ...record2, effectiveThinkingLevel: model ? resolveThinking(model, record2.thinkingLevel) : null };
5798
6219
  }
5799
6220
  owns(id) {
5800
6221
  return this.deps.store.getBot(id) !== null;
5801
6222
  }
6223
+ /** The turn a Bot is running, live (A47); kept for a Bot whether or not it has started. */
6224
+ live(id) {
6225
+ if (!this.deps.store.getBot(id)) throw new RuntimeFault("bot_missing", "Bot not found", 404);
6226
+ let live = this.lives.get(id);
6227
+ if (!live) {
6228
+ live = new LiveTurns((chatId) => this.liveChat(id, chatId));
6229
+ this.lives.set(id, live);
6230
+ }
6231
+ return live;
6232
+ }
6233
+ /** A chat as the Bot sees it, for the live turn: a DM is named after the other member. */
6234
+ liveChat(botId, chatId) {
6235
+ const chat = this.deps.chats.get(botId, chatId);
6236
+ if (!chat) return null;
6237
+ const other = chat.type === "dm" ? chat.memberIds.find((member) => member !== botId) : void 0;
6238
+ return { id: chat.id, type: chat.type, title: other ? this.deps.chats.member(other)?.name ?? chat.title : chat.title };
6239
+ }
5802
6240
  list() {
5803
- return this.deps.store.listBots().map((record) => this.details(record));
6241
+ return this.deps.store.listBots().map((record2) => this.details(record2));
5804
6242
  }
5805
6243
  create(name2) {
5806
6244
  return this.details(this.deps.store.createBot(name2));
5807
6245
  }
5808
6246
  describe(id) {
5809
- const record = this.deps.store.getBot(id);
5810
- if (!record) throw new RuntimeFault("bot_missing", "Bot not found", 404);
5811
- return this.details(record);
6247
+ const record2 = this.deps.store.getBot(id);
6248
+ if (!record2) throw new RuntimeFault("bot_missing", "Bot not found", 404);
6249
+ return this.details(record2);
5812
6250
  }
5813
6251
  runs(id) {
5814
6252
  this.describe(id);
@@ -5856,15 +6294,15 @@ var BotService = class {
5856
6294
  /** Every Bot's run state, for `bots.list` (A43). A Bot not loaded in this process is stopped. */
5857
6295
  roster() {
5858
6296
  const { store } = this.deps;
5859
- return store.listBots().map((record) => {
5860
- const bot = this.bots.get(record.id), runId = bot?.agentRuntime.activeRunId ?? null;
6297
+ return store.listBots().map((record2) => {
6298
+ const bot = this.bots.get(record2.id), runId = bot?.agentRuntime.activeRunId ?? null;
5861
6299
  const status = bot?.status() ?? { status: "stop", activity: null, context: null, error: null };
5862
6300
  return {
5863
- id: record.id,
5864
- name: record.name,
6301
+ id: record2.id,
6302
+ name: record2.name,
5865
6303
  status,
5866
6304
  runChatIds: status.status === "running" && runId ? store.runtime.inputs(runId).map((input) => input.chatId) : [],
5867
- errorCode: store.runtime.latestErrorCode(record.id)
6305
+ errorCode: store.runtime.latestErrorCode(record2.id)
5868
6306
  };
5869
6307
  });
5870
6308
  }
@@ -5895,6 +6333,8 @@ var BotService = class {
5895
6333
  const bot = this.get(id), workspacePath = bot.workspace.location().cwd;
5896
6334
  await bot.stop();
5897
6335
  this.bots.delete(id);
6336
+ this.lives.get(id)?.close();
6337
+ this.lives.delete(id);
5898
6338
  if (!this.deps.store.deleteBot(id)) throw new RuntimeFault("bot_missing", "Bot not found", 404);
5899
6339
  if (!keepFiles) await rm2(dirname5(workspacePath), { recursive: true, force: true });
5900
6340
  }
@@ -5902,6 +6342,7 @@ var BotService = class {
5902
6342
  if (this.closed) return;
5903
6343
  this.closed = true;
5904
6344
  await Promise.all([...this.bots.values()].map((bot) => bot.stop()));
6345
+ for (const live of this.lives.values()) live.close();
5905
6346
  }
5906
6347
  };
5907
6348
 
@@ -6003,8 +6444,8 @@ import { join as join8, resolve as resolve5 } from "node:path";
6003
6444
  var pidPath = (dir) => join8(resolve5(dir), "jarvy.pid");
6004
6445
  var logPath = (dir) => join8(resolve5(dir), "jarvy.log");
6005
6446
  function writePidFile(dir, url) {
6006
- const record = { pid: process.pid, startToken: startToken(process.pid), url, startedAt: Date.now() };
6007
- writeFileSync(pidPath(dir), JSON.stringify(record), { mode: 384 });
6447
+ const record2 = { pid: process.pid, startToken: startToken(process.pid), url, startedAt: Date.now() };
6448
+ writeFileSync(pidPath(dir), JSON.stringify(record2), { mode: 384 });
6008
6449
  }
6009
6450
  function removePidFile(dir) {
6010
6451
  if (readRecord(dir)?.pid === process.pid) rmSync(pidPath(dir), { force: true });
@@ -6032,14 +6473,14 @@ function alive(pid) {
6032
6473
  }
6033
6474
  }
6034
6475
  function runningServer(dir) {
6035
- const record = readRecord(dir);
6036
- if (!record) return null;
6037
- const token = alive(record.pid) ? startToken(record.pid) : null;
6038
- if (token === null || token && record.startToken && token !== record.startToken) {
6476
+ const record2 = readRecord(dir);
6477
+ if (!record2) return null;
6478
+ const token = alive(record2.pid) ? startToken(record2.pid) : null;
6479
+ if (token === null || token && record2.startToken && token !== record2.startToken) {
6039
6480
  rmSync(pidPath(dir), { force: true });
6040
6481
  return null;
6041
6482
  }
6042
- return record;
6483
+ return record2;
6043
6484
  }
6044
6485
  function startBackground(script, runArgs, dir) {
6045
6486
  mkdirSync3(resolve5(dir), { recursive: true, mode: 448 });
@@ -6072,11 +6513,11 @@ function signalReady(url) {
6072
6513
  process.disconnect();
6073
6514
  }
6074
6515
  async function stopServer(dir, waitMs = 3e4) {
6075
- const record = runningServer(dir);
6076
- if (!record) return "not_running";
6077
- process.kill(record.pid, "SIGTERM");
6516
+ const record2 = runningServer(dir);
6517
+ if (!record2) return "not_running";
6518
+ process.kill(record2.pid, "SIGTERM");
6078
6519
  for (const deadline = Date.now() + waitMs; Date.now() < deadline; ) {
6079
- if (!alive(record.pid)) {
6520
+ if (!alive(record2.pid)) {
6080
6521
  rmSync(pidPath(dir), { force: true });
6081
6522
  return "stopped";
6082
6523
  }