agent-workbench 0.6.0 → 0.7.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.
package/dist/server.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // packages/server/src/index.ts
4
4
  import { existsSync as existsSync4 } from "node:fs";
5
5
  import { createServer } from "node:http";
6
- import path59 from "node:path";
6
+ import path62 from "node:path";
7
7
  import { fileURLToPath as fileURLToPath2 } from "node:url";
8
8
  import express from "express";
9
9
 
@@ -172,6 +172,8 @@ function noticeText(part) {
172
172
  }
173
173
  }
174
174
  var MAX_SUBAGENT_DESCRIPTION = 120;
175
+ var MAX_QUEUED_PROMPTS = 20;
176
+ var MAX_QUEUED_PROMPT_CHARS = 4e3;
175
177
  var EMPTY_CONTEXT_USAGE = {
176
178
  lastRequestTokens: 0,
177
179
  lastOutputTokens: 0,
@@ -365,6 +367,11 @@ function modelFamilyOf(value, options = MODEL_OPTIONS) {
365
367
  return candidates.find((option) => value.includes(option.family))?.family ?? null;
366
368
  }
367
369
 
370
+ // packages/shared/src/files.ts
371
+ function isContentHash(value) {
372
+ return typeof value === "string" && /^[0-9a-f]{64}$/.test(value);
373
+ }
374
+
368
375
  // packages/shared/src/composer-draft.ts
369
376
  var MAX_COMPOSER_DRAFT_CHARS = 1e6;
370
377
  var MAX_COMPOSER_DRAFT_PASTES = 100;
@@ -722,6 +729,7 @@ var MAX_SUBMIT_IMAGE_BYTES = 12 * 1024 * 1024;
722
729
  var MAX_SUBMIT_IMAGES = 8;
723
730
  var MAX_SUBMIT_FILE_BYTES = 10 * 1024 * 1024;
724
731
  var MAX_SUBMIT_FILES = 4;
732
+ var REWIND_RESTORES = ["conversation", "conversation-and-code"];
725
733
  var FILES_CHECK_MAX_PATHS = 100;
726
734
  var FILES_CHECK_MAX_PATH_CHARS = 512;
727
735
  var CONTINUE_LABEL_MAX_CHARS = 120;
@@ -800,6 +808,18 @@ function parseClientMessage(raw) {
800
808
  const terminalId = asNonEmptyString(record["terminalId"]);
801
809
  return terminalId === null ? null : { type: "agent.interrupt", terminalId };
802
810
  }
811
+ case "agent.forceSend": {
812
+ const terminalId = asNonEmptyString(record["terminalId"]);
813
+ const requestId = asNonEmptyString(record["requestId"]);
814
+ return terminalId === null || requestId === null ? null : { type: "agent.forceSend", terminalId, requestId };
815
+ }
816
+ case "agent.rewind": {
817
+ const terminalId = asNonEmptyString(record["terminalId"]);
818
+ const eventId = asNonEmptyString(record["eventId"]);
819
+ const restore = asLiteral(record["restore"], REWIND_RESTORES);
820
+ const requestId = asNonEmptyString(record["requestId"]);
821
+ return terminalId === null || eventId === null || restore === null || requestId === null ? null : { type: "agent.rewind", terminalId, eventId, restore, requestId };
822
+ }
803
823
  case "resize": {
804
824
  const terminalId = asNonEmptyString(record["terminalId"]);
805
825
  const cols = asPositiveInt(record["cols"]);
@@ -934,6 +954,14 @@ function parseClientMessage(raw) {
934
954
  }
935
955
  return { type: "files.check", terminalId, requestId, paths };
936
956
  }
957
+ case "files.write": {
958
+ const terminalId = asNonEmptyString(record["terminalId"]);
959
+ const requestId = asNonEmptyString(record["requestId"]);
960
+ const writePath = asNonEmptyString(record["path"]);
961
+ const text = asString(record["text"]);
962
+ const baseHash = record["baseHash"];
963
+ return terminalId === null || requestId === null || writePath === null || text === null || !isContentHash(baseHash) ? null : { type: "files.write", terminalId, requestId, path: writePath, text, baseHash };
964
+ }
937
965
  case "plans.read": {
938
966
  const terminalId = asNonEmptyString(record["terminalId"]);
939
967
  const fileName = asNonEmptyString(record["fileName"]);
@@ -1425,10 +1453,14 @@ import { copyFile, mkdir as mkdir2, open as open2, readdir, readFile as readFile
1425
1453
  import path5 from "node:path";
1426
1454
 
1427
1455
  // packages/server/src/jsonl-reader.ts
1428
- import { open } from "node:fs/promises";
1456
+ import { open, stat as stat2 } from "node:fs/promises";
1429
1457
  var CHUNK_SIZE = 64 * 1024;
1430
- async function readHeadLines(filePath, options) {
1431
- const handle = await open(filePath, "r");
1458
+ var DISK_FILES = {
1459
+ size: async (filePath) => (await stat2(filePath)).size,
1460
+ open: (filePath) => open(filePath, "r")
1461
+ };
1462
+ async function readHeadLines(filePath, options, files = DISK_FILES) {
1463
+ const handle = await files.open(filePath);
1432
1464
  try {
1433
1465
  const lines = [];
1434
1466
  let pending = "";
@@ -1460,8 +1492,8 @@ async function readHeadLines(filePath, options) {
1460
1492
  await handle.close();
1461
1493
  }
1462
1494
  }
1463
- async function readTailLines(filePath, options) {
1464
- const handle = await open(filePath, "r");
1495
+ async function readTailLines(filePath, options, files = DISK_FILES) {
1496
+ const handle = await files.open(filePath);
1465
1497
  try {
1466
1498
  const { size } = await handle.stat();
1467
1499
  if (size === 0) return [];
@@ -1806,14 +1838,6 @@ var ANTIGRAVITY_MODEL_OPTIONS = [
1806
1838
  model("gemini-3.8-flash-high", "Gemini 3.8 Flash (High)"),
1807
1839
  model("gemini-3.8-flash-medium", "Gemini 3.8 Flash (Medium)"),
1808
1840
  model("gemini-3.8-flash-low", "Gemini 3.8 Flash (Low)"),
1809
- model("gemini-3.7-flash-high", "Gemini 3.7 Flash (High)"),
1810
- model("gemini-3.7-flash-medium", "Gemini 3.7 Flash (Medium)"),
1811
- model("gemini-3.7-flash-low", "Gemini 3.7 Flash (Low)"),
1812
- model("gemini-3.6-flash-high", "Gemini 3.6 Flash (High)"),
1813
- model("gemini-3.6-flash-medium", "Gemini 3.6 Flash (Medium)"),
1814
- model("gemini-3.6-flash-low", "Gemini 3.6 Flash (Low)"),
1815
- model("gemini-3.1-pro-high", "Gemini 3.1 Pro (High)"),
1816
- model("gemini-3.1-pro-low", "Gemini 3.1 Pro (Low)"),
1817
1841
  model("claude-sonnet-4-6", "Claude Sonnet 4.6 (Thinking)"),
1818
1842
  model("claude-opus-4-6-thinking", "Claude Opus 4.6 (Thinking)"),
1819
1843
  model("gpt-oss-120b-medium", "GPT-OSS 120B (Medium)")
@@ -2251,7 +2275,7 @@ var AntigravityCatalog = class {
2251
2275
  };
2252
2276
 
2253
2277
  // packages/server/src/agents/antigravity/discovery.ts
2254
- import { open as open3, readdir as readdir2, stat as stat2 } from "node:fs/promises";
2278
+ import { open as open3, readdir as readdir2, stat as stat3 } from "node:fs/promises";
2255
2279
  import path6 from "node:path";
2256
2280
  var CONVERSATION_LINE = /\] (?:Created|Streaming) conversation ([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\s*$/i;
2257
2281
  var PID_LINE = /\] Starting language server process with pid (\d+)\s*$/;
@@ -2466,7 +2490,7 @@ var LogWatcher = class {
2466
2490
  if (!/^cli-.*\.log$/i.test(name) || this.checked.has(name)) continue;
2467
2491
  const file = path6.join(root, name);
2468
2492
  try {
2469
- const info = await stat2(file);
2493
+ const info = await stat3(file);
2470
2494
  if (!info.isFile() || info.mtimeMs < launchedAt - 2e3) continue;
2471
2495
  } catch {
2472
2496
  continue;
@@ -2522,7 +2546,6 @@ function withoutPastedText(raw) {
2522
2546
  import { stat as stat4 } from "node:fs/promises";
2523
2547
 
2524
2548
  // packages/server/src/agents/jsonl-follower.ts
2525
- import { open as open4, stat as stat3 } from "node:fs/promises";
2526
2549
  var READ_CHUNK_BYTES = 256 * 1024;
2527
2550
  var MAX_EVENTS = 4e3;
2528
2551
  var NEWLINE = 10;
@@ -2537,6 +2560,8 @@ var JsonlFollower = class {
2537
2560
  this.path = filePath;
2538
2561
  this.maxEvents = options.maxEvents ?? MAX_EVENTS;
2539
2562
  this.fingerprintBytes = Math.max(0, Math.floor(options.fingerprintBytes ?? 0));
2563
+ this.files = options.files ?? DISK_FILES;
2564
+ this.isVisible = options.isVisible ?? null;
2540
2565
  }
2541
2566
  sink;
2542
2567
  path;
@@ -2551,6 +2576,8 @@ var JsonlFollower = class {
2551
2576
  fingerprintBytes;
2552
2577
  /** Los primeros `fingerprintBytes` del archivo leido; null sin huella o si todavia no llego a ese tamano. */
2553
2578
  fingerprint = null;
2579
+ files;
2580
+ isVisible;
2554
2581
  get filePath() {
2555
2582
  return this.path;
2556
2583
  }
@@ -2584,22 +2611,28 @@ var JsonlFollower = class {
2584
2611
  }
2585
2612
  /** Ultimos `limit` eventos, que es lo que se quiere ver al abrir el panel. */
2586
2613
  getTail(limit) {
2587
- const events = this.events.slice(-limit);
2614
+ const visible = this.visibleEvents();
2615
+ const events = visible.slice(-limit);
2588
2616
  return {
2589
2617
  events,
2590
- hasMore: this.events.length > events.length || this.dropped > 0
2618
+ hasMore: visible.length > events.length || this.dropped > 0
2591
2619
  };
2592
2620
  }
2593
2621
  /** Tramo anterior a un evento ya entregado. Vacio si ese id ya no esta. */
2594
2622
  getPageBefore(beforeEventId, limit) {
2595
- const index = this.events.findIndex((event2) => event2.eventId === beforeEventId);
2623
+ const visible = this.visibleEvents();
2624
+ const index = visible.findIndex((event2) => event2.eventId === beforeEventId);
2596
2625
  if (index <= 0) return { events: [], hasMore: false };
2597
2626
  const start = Math.max(0, index - limit);
2598
2627
  return {
2599
- events: this.events.slice(start, index),
2628
+ events: visible.slice(start, index),
2600
2629
  hasMore: start > 0 || this.dropped > 0
2601
2630
  };
2602
2631
  }
2632
+ /** Los eventos que se entregan: todos, salvo los que `isVisible` deja afuera. */
2633
+ visibleEvents() {
2634
+ return this.isVisible === null ? this.events : this.events.filter(this.isVisible);
2635
+ }
2603
2636
  /**
2604
2637
  * Lee lo que haya de nuevo.
2605
2638
  *
@@ -2614,8 +2647,7 @@ var JsonlFollower = class {
2614
2647
  }
2615
2648
  let size;
2616
2649
  try {
2617
- const info = await stat3(this.path);
2618
- size = info.size;
2650
+ size = await this.files.size(this.path);
2619
2651
  } catch {
2620
2652
  if (this.events.length > 0 || this.offset > 0) {
2621
2653
  this.reset();
@@ -2638,7 +2670,7 @@ var JsonlFollower = class {
2638
2670
  return { reset: didReset, added: [] };
2639
2671
  }
2640
2672
  const added = await this.readFrom(this.path, size);
2641
- return { reset: didReset, added };
2673
+ return { reset: didReset, added: this.isVisible === null ? added : added.filter(this.isVisible) };
2642
2674
  }
2643
2675
  reset() {
2644
2676
  this.offset = 0;
@@ -2651,7 +2683,7 @@ var JsonlFollower = class {
2651
2683
  }
2652
2684
  /** true si el archivo todavia empieza con `expected`. */
2653
2685
  async sameFingerprint(filePath, expected) {
2654
- const handle = await open4(filePath, "r");
2686
+ const handle = await this.files.open(filePath);
2655
2687
  try {
2656
2688
  const head = Buffer.alloc(expected.length);
2657
2689
  const { bytesRead } = await handle.read(head, 0, expected.length, 0);
@@ -2662,7 +2694,7 @@ var JsonlFollower = class {
2662
2694
  }
2663
2695
  async readFrom(filePath, size) {
2664
2696
  const added = [];
2665
- const handle = await open4(filePath, "r");
2697
+ const handle = await this.files.open(filePath);
2666
2698
  try {
2667
2699
  const buffer = Buffer.allocUnsafe(READ_CHUNK_BYTES);
2668
2700
  while (this.offset < size) {
@@ -4005,7 +4037,9 @@ var ANTIGRAVITY_BASE_CAPABILITIES = {
4005
4037
  contextWindowSource: null,
4006
4038
  plans: false,
4007
4039
  // Con la status line, su confirmacion abierta ya la frena `submitRefusal` (12.7).
4008
- waitingBlocksSubmit: false
4040
+ waitingBlocksSubmit: false,
4041
+ // Su tecla de "send immediately", Alt+M (hito 45, §12.13). Sin status line no hay cola que forzar.
4042
+ forceSend: true
4009
4043
  };
4010
4044
  var ANTIGRAVITY_STATUS_LINE_CAPABILITIES = {
4011
4045
  ...ANTIGRAVITY_BASE_CAPABILITIES,
@@ -4196,6 +4230,11 @@ function createAntigravityAdapter(options = {}) {
4196
4230
  },
4197
4231
  history: createAntigravityHistory({ statusLine: createFollowerStatusLine(store, isActive) }),
4198
4232
  status,
4233
+ /*
4234
+ La cola no queda escrita en ningun lado que la app lea (hito 45, §12.13):
4235
+ es lo que la app le mando con el agente trabajando. "Enviar ya" es Alt+M.
4236
+ */
4237
+ promptQueue: { source: "app", forceSend: "alt-m" },
4199
4238
  /**
4200
4239
  * El modelo de `settings.json`, provisional hasta la primera respuesta. La
4201
4240
  * etiqueta va entera (`Gemini 3.7 Flash (Low)`): es la familia con la que
@@ -5221,9 +5260,182 @@ function toTurnDuration(record) {
5221
5260
  return { eventId, durationMs };
5222
5261
  }
5223
5262
 
5263
+ // packages/server/src/agents/claude-code/branch-tracker.ts
5264
+ var BranchTracker = class {
5265
+ nodes = /* @__PURE__ */ new Map();
5266
+ /** La ultima linea que llego. */
5267
+ last = null;
5268
+ /**
5269
+ * El mensaje desde el que se escondio lo que la CLI deshizo sin escribirlo,
5270
+ * o null. Mientras este, la conversacion termina en su padre.
5271
+ */
5272
+ withdrawn = null;
5273
+ hidden = /* @__PURE__ */ new Set();
5274
+ /** Una linea nueva. true si cambio lo que se ve. */
5275
+ observe(line) {
5276
+ if (this.nodes.has(line.uuid)) return false;
5277
+ this.nodes.set(line.uuid, { parent: line.parent, prompt: line.prompt });
5278
+ const previous = this.last;
5279
+ this.last = line.uuid;
5280
+ if (this.withdrawn !== null) {
5281
+ if (!line.speaks && this.descendsFrom(line.uuid, this.withdrawn)) return this.recompute();
5282
+ this.withdrawn = null;
5283
+ return this.recompute();
5284
+ }
5285
+ if (line.mayBranch && previous !== null && line.parent !== previous) return this.recompute();
5286
+ return false;
5287
+ }
5288
+ /**
5289
+ * Esconde desde ese mensaje en adelante: la CLI lo deshizo y todavia no
5290
+ * escribio nada que lo diga. Solo un mensaje del usuario que esta en la
5291
+ * conversacion que se ve. true si cambio lo que se ve.
5292
+ */
5293
+ withdraw(uuid) {
5294
+ const node = this.nodes.get(uuid);
5295
+ if (node === void 0 || !node.prompt || this.withdrawn === uuid) return false;
5296
+ if (!this.pathFrom(this.leaf()).has(uuid)) return false;
5297
+ this.withdrawn = uuid;
5298
+ return this.recompute();
5299
+ }
5300
+ /** El mensaje desde el que se escondio, mientras la CLI no escriba otra cosa. */
5301
+ withdrawnFrom() {
5302
+ return this.withdrawn;
5303
+ }
5304
+ isHidden(uuid) {
5305
+ return this.hidden.has(uuid);
5306
+ }
5307
+ /** Cuantas lineas se esconden. Para el chequeo. */
5308
+ hiddenCount() {
5309
+ return this.hidden.size;
5310
+ }
5311
+ /** El archivo encogio o se reemplazo. */
5312
+ reset() {
5313
+ this.nodes.clear();
5314
+ this.last = null;
5315
+ this.withdrawn = null;
5316
+ this.hidden = /* @__PURE__ */ new Set();
5317
+ }
5318
+ /** Donde termina la conversacion que se ve. */
5319
+ leaf() {
5320
+ if (this.withdrawn === null) return this.last;
5321
+ return this.nodes.get(this.withdrawn)?.parent ?? null;
5322
+ }
5323
+ /** true si `uuid` es `ancestor` o cuelga de el. */
5324
+ descendsFrom(uuid, ancestor) {
5325
+ return this.pathFrom(uuid).has(ancestor);
5326
+ }
5327
+ /** La linea y todas las de las que cuelga. */
5328
+ pathFrom(leaf) {
5329
+ const path63 = /* @__PURE__ */ new Set();
5330
+ let current = leaf;
5331
+ while (current !== null && !path63.has(current)) {
5332
+ const node = this.nodes.get(current);
5333
+ if (node === void 0) break;
5334
+ path63.add(current);
5335
+ current = node.parent;
5336
+ }
5337
+ return path63;
5338
+ }
5339
+ recompute() {
5340
+ const path63 = this.pathFrom(this.leaf());
5341
+ const children = /* @__PURE__ */ new Map();
5342
+ for (const [uuid, node] of this.nodes) {
5343
+ const key = node.parent !== null && this.nodes.has(node.parent) ? node.parent : null;
5344
+ const list = children.get(key);
5345
+ if (list === void 0) children.set(key, [uuid]);
5346
+ else list.push(uuid);
5347
+ }
5348
+ const roots = [];
5349
+ for (const uuid of children.get(null) ?? []) if (!path63.has(uuid)) roots.push(uuid);
5350
+ for (const uuid of path63) {
5351
+ for (const child of children.get(uuid) ?? []) if (!path63.has(child)) roots.push(child);
5352
+ }
5353
+ const hidden = /* @__PURE__ */ new Set();
5354
+ for (const root of roots) {
5355
+ const subtree = [];
5356
+ const pending = [root];
5357
+ let hasPrompt = false;
5358
+ while (pending.length > 0) {
5359
+ const uuid = pending.pop();
5360
+ subtree.push(uuid);
5361
+ if (this.nodes.get(uuid)?.prompt === true) hasPrompt = true;
5362
+ for (const child of children.get(uuid) ?? []) pending.push(child);
5363
+ }
5364
+ if (hasPrompt) for (const uuid of subtree) hidden.add(uuid);
5365
+ }
5366
+ const changed = hidden.size !== this.hidden.size || [...hidden].some((uuid) => !this.hidden.has(uuid));
5367
+ this.hidden = hidden;
5368
+ return changed;
5369
+ }
5370
+ };
5371
+
5372
+ // packages/server/src/agents/claude-code/prompt-queue.ts
5373
+ var MAX_TRACKED = 100;
5374
+ var AGENT_NOTICE = /^\s*<(task-notification|agent-message)\b/;
5375
+ function queuedPromptOf(content, id, at) {
5376
+ if (AGENT_NOTICE.test(content)) return null;
5377
+ const { text: withoutImages, images } = takePastedImages(cleanUserText(content));
5378
+ const text = withoutImages.trim();
5379
+ if (text.length === 0 && images.length === 0) return null;
5380
+ return {
5381
+ id,
5382
+ text: text.slice(0, MAX_QUEUED_PROMPT_CHARS),
5383
+ truncated: text.length > MAX_QUEUED_PROMPT_CHARS,
5384
+ images: images.length,
5385
+ at
5386
+ };
5387
+ }
5388
+ var PromptQueueTracker = class {
5389
+ queue = [];
5390
+ seq = 0;
5391
+ /** Una linea del JSONL, ya parseada. Nunca lanza: el esquema cambia. */
5392
+ observe(record) {
5393
+ if (record["type"] !== "queue-operation") return;
5394
+ const operation = record["operation"];
5395
+ const content = typeof record["content"] === "string" ? record["content"] : null;
5396
+ if (operation === "enqueue") {
5397
+ if (content === null) return;
5398
+ this.seq += 1;
5399
+ this.queue.push({ seq: this.seq, content, at: readTimestamp(record["timestamp"]) });
5400
+ if (this.queue.length > MAX_TRACKED) this.queue.shift();
5401
+ return;
5402
+ }
5403
+ if (operation === "dequeue") {
5404
+ this.queue.shift();
5405
+ return;
5406
+ }
5407
+ if (content === null) return;
5408
+ const index = this.queue.findIndex((item) => item.content === content);
5409
+ if (index !== -1) this.queue.splice(index, 1);
5410
+ }
5411
+ /**
5412
+ * Los mensajes del usuario en cola del proceso lanzado en `launchedAt` o
5413
+ * despues, en orden. Uno sin hora no se puede atribuir a ningun proceso y no
5414
+ * cuenta.
5415
+ *
5416
+ * Lo de un proceso anterior ademas se olvida: quedo en una cola que ya no
5417
+ * existe, y si siguiera adelante el `dequeue` del proceso de ahora sacaria
5418
+ * ese en vez del suyo.
5419
+ */
5420
+ pending(launchedAt) {
5421
+ this.queue = this.queue.filter((item) => item.at > 0 && item.at >= launchedAt);
5422
+ const result = [];
5423
+ for (const item of this.queue) {
5424
+ const prompt = queuedPromptOf(item.content, `queue-${item.seq}`, item.at);
5425
+ if (prompt !== null) result.push(prompt);
5426
+ if (result.length >= MAX_QUEUED_PROMPTS) break;
5427
+ }
5428
+ return result;
5429
+ }
5430
+ /** El archivo se reemplazo: lo anotado era de otra lectura. */
5431
+ reset() {
5432
+ this.queue = [];
5433
+ }
5434
+ };
5435
+
5224
5436
  // packages/server/src/agents/claude-code/subagent-tracker.ts
5225
5437
  var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
5226
- var MAX_TRACKED = 50;
5438
+ var MAX_TRACKED2 = 50;
5227
5439
  var NOTIFICATION = /<task-notification>([\s\S]*?)<\/task-notification>/g;
5228
5440
  var TOOL_USE_ID = /<tool-use-id>\s*([^<\s]+)\s*<\/tool-use-id>/;
5229
5441
  function labelOf(value) {
@@ -5318,7 +5530,7 @@ var SubagentTracker = class {
5318
5530
  background: input?.["run_in_background"] === true,
5319
5531
  startedAt
5320
5532
  });
5321
- if (this.pending.size > MAX_TRACKED) {
5533
+ if (this.pending.size > MAX_TRACKED2) {
5322
5534
  const oldest = this.pending.keys().next();
5323
5535
  if (!oldest.done) this.pending.delete(oldest.value);
5324
5536
  }
@@ -5375,7 +5587,10 @@ var ConversationFollower = class {
5375
5587
  this.installVariants = installVariants;
5376
5588
  this.limits = options.limits ?? TRANSPORT_LIMITS;
5377
5589
  this.target = options.target ?? { cwd: "", sessionId: "" };
5378
- this.jsonl = new JsonlFollower(filePath, this, { maxEvents: options.maxEvents });
5590
+ this.jsonl = new JsonlFollower(filePath, this, {
5591
+ maxEvents: options.maxEvents,
5592
+ isVisible: (event2) => !this.branch.isHidden(event2.eventId)
5593
+ });
5379
5594
  }
5380
5595
  installVariants;
5381
5596
  /** La lectura del archivo. Esta clase es su sink. */
@@ -5441,8 +5656,20 @@ var ConversationFollower = class {
5441
5656
  * —el aviso de fin es una `queue-operation`—, asi que va antes de todo filtro.
5442
5657
  */
5443
5658
  subagents = new SubagentTracker();
5659
+ /**
5660
+ * Los mensajes que esperan en la cola de la CLI (hito 45, §4.4.3). Tambien
5661
+ * son lineas que no son tarjetas (`queue-operation`): va antes de todo filtro.
5662
+ */
5663
+ promptQueue = new PromptQueueTracker();
5444
5664
  /** Los topes de cada parte. Los de transporte salvo que se lea para la copia propia. */
5445
5665
  limits;
5666
+ /**
5667
+ * Que ramas del archivo quedaron abandonadas (hito 44, §4.16): un mensaje que
5668
+ * se deshizo no se entrega. Va antes que `jsonl`, que lo consulta.
5669
+ */
5670
+ branch = new BranchTracker();
5671
+ /** true si en la lectura en curso cambio lo que se ve: el `poll` avisa un reinicio. */
5672
+ branchChanged = false;
5446
5673
  /** La pestana cuyos documentos se reconocen. Ver el constructor. */
5447
5674
  target;
5448
5675
  /** La ruta que se sigue. Siempre fija: esta CLI la conoce al lanzar. */
@@ -5497,6 +5724,10 @@ var ConversationFollower = class {
5497
5724
  runningSubagents(launchedAt) {
5498
5725
  return this.subagents.running(launchedAt);
5499
5726
  }
5727
+ /** Los mensajes del proceso de `launchedAt` que esperan en la cola de la CLI (§4.4.3). */
5728
+ queuedPrompts(launchedAt) {
5729
+ return this.promptQueue.pending(launchedAt);
5730
+ }
5500
5731
  /** Ultimos `limit` eventos, que es lo que se quiere ver al abrir el panel. */
5501
5732
  getTail(limit) {
5502
5733
  return this.jsonl.getTail(limit);
@@ -5515,7 +5746,10 @@ var ConversationFollower = class {
5515
5746
  this.turns = [];
5516
5747
  this.partUpdates = [];
5517
5748
  this.freshPlans = [];
5518
- const { reset, added } = await this.jsonl.poll();
5749
+ const read = await this.jsonl.poll();
5750
+ const { added } = read;
5751
+ const reset = read.reset || this.branchChanged;
5752
+ this.branchChanged = false;
5519
5753
  const fresh = new Set(added.map((event2) => event2.eventId));
5520
5754
  const turns = this.turns.filter((turn) => !fresh.has(turn.eventId));
5521
5755
  const parts = [
@@ -5543,6 +5777,18 @@ var ConversationFollower = class {
5543
5777
  this.attachmentRoots.clear();
5544
5778
  this.planFiles = [];
5545
5779
  this.subagents.reset();
5780
+ this.promptQueue.reset();
5781
+ this.branch.reset();
5782
+ }
5783
+ /**
5784
+ * Esconde desde ese mensaje: la CLI lo deshizo y todavia no escribio nada
5785
+ * (hito 44). true si cambio lo que se ve; quien llama avisa el reinicio.
5786
+ */
5787
+ withdrawFrom(eventId) {
5788
+ return this.branch.withdraw(eventId);
5789
+ }
5790
+ withdrawnFrom() {
5791
+ return this.branch.withdrawnFrom();
5546
5792
  }
5547
5793
  /** true si la linea es la invocacion de `/model` que la CLI guarda. */
5548
5794
  mentionsModelCommand(record) {
@@ -5559,7 +5805,29 @@ var ConversationFollower = class {
5559
5805
  * los que ya guardo, para pegarle a uno anterior la duracion o una imagen.
5560
5806
  */
5561
5807
  consume(record, lineNumber, events) {
5808
+ const event2 = this.consumeRecord(record, lineNumber, events);
5809
+ this.observeBranch(record, event2);
5810
+ return event2;
5811
+ }
5812
+ /**
5813
+ * Anota la linea en el arbol de la conversacion (hito 44, §4.16). Solo las de
5814
+ * la cadena principal: las de un subagente son otra conversacion.
5815
+ */
5816
+ observeBranch(record, event2) {
5817
+ const uuid = record["uuid"];
5818
+ if (typeof uuid !== "string" || uuid.length === 0 || record["isSidechain"] === true) return;
5819
+ if (!Object.prototype.hasOwnProperty.call(record, "parentUuid")) return;
5820
+ const parentUuid = record["parentUuid"];
5821
+ const logicalParent = record["logicalParentUuid"];
5822
+ const parent = typeof parentUuid === "string" && parentUuid.length > 0 ? parentUuid : typeof logicalParent === "string" && logicalParent.length > 0 ? logicalParent : null;
5823
+ const prompt = event2 !== null && event2.role === "user" && event2.parts.some((part) => part.kind === "text" || part.kind === "image");
5824
+ const mayBranch = record["type"] === "user" && !isToolResultRecord(record);
5825
+ const speaks = record["type"] === "user" || record["type"] === "assistant";
5826
+ if (this.branch.observe({ uuid, parent, prompt, mayBranch, speaks })) this.branchChanged = true;
5827
+ }
5828
+ consumeRecord(record, lineNumber, events) {
5562
5829
  this.subagents.observe(record);
5830
+ this.promptQueue.observe(record);
5563
5831
  if (record["type"] === "cost-state") {
5564
5832
  this.sessionVariants.observeCostState(record, Date.now());
5565
5833
  this.recomputeModel();
@@ -5747,9 +6015,15 @@ var ConversationFollower = class {
5747
6015
  return model2;
5748
6016
  }
5749
6017
  };
6018
+ function isToolResultRecord(record) {
6019
+ const message = record["message"];
6020
+ if (typeof message !== "object" || message === null) return false;
6021
+ const content = message["content"];
6022
+ return Array.isArray(content) && content.some((block) => typeof block === "object" && block !== null && block["type"] === "tool_result");
6023
+ }
5750
6024
 
5751
6025
  // packages/server/src/agents/claude-code/conversation-image.ts
5752
- import { open as open5, readFile as readFile9, stat as stat10 } from "node:fs/promises";
6026
+ import { open as open4, readFile as readFile9, stat as stat10 } from "node:fs/promises";
5753
6027
  import path18 from "node:path";
5754
6028
 
5755
6029
  // packages/server/src/image-signature.ts
@@ -5783,7 +6057,7 @@ var CHUNK_SIZE2 = 256 * 1024;
5783
6057
  var NEWLINE2 = 10;
5784
6058
  var MAX_IMAGE_BYTES = MAX_SUBMIT_IMAGE_BYTES;
5785
6059
  async function loadConversationImage(filePath, eventId, index, source = "content", pastedDir = pasteRoot()) {
5786
- const handle = await open5(filePath, "r").catch(() => null);
6060
+ const handle = await open4(filePath, "r").catch(() => null);
5787
6061
  if (handle === null) return null;
5788
6062
  try {
5789
6063
  if (source === "pasted") return await findPastedImage(linesOf(handle), eventId, index, pastedDir);
@@ -6050,6 +6324,17 @@ var ClaudeCodeSessionFollower = class {
6050
6324
  runningSubagents(launchedAt) {
6051
6325
  return this.inner.runningSubagents(launchedAt);
6052
6326
  }
6327
+ /** Los mensajes del proceso de `launchedAt` que esperan en la cola de la CLI (hito 45, §4.4.3). */
6328
+ queuedPrompts(launchedAt) {
6329
+ return this.inner.queuedPrompts(launchedAt);
6330
+ }
6331
+ /** Esconde desde un mensaje que la CLI deshizo sin escribirlo todavia (hito 44, §4.16). */
6332
+ withdrawFrom(eventId) {
6333
+ return this.inner.withdrawFrom(eventId);
6334
+ }
6335
+ withdrawnFrom() {
6336
+ return this.inner.withdrawnFrom();
6337
+ }
6053
6338
  };
6054
6339
 
6055
6340
  // packages/server/src/agents/claude-code/session-scan.ts
@@ -6293,6 +6578,328 @@ function autoAnswerResumeDialog(options) {
6293
6578
  return () => finish("cancelled");
6294
6579
  }
6295
6580
 
6581
+ // packages/server/src/agents/claude-code/handback.ts
6582
+ import { stat as stat13 } from "node:fs/promises";
6583
+
6584
+ // packages/server/src/debug.ts
6585
+ var enabled = process.env["AGENT_WORKBENCH_DEBUG"] === "1";
6586
+ function debugLog(scope, message) {
6587
+ if (!enabled) return;
6588
+ console.log(`[${scope}] ${message}`);
6589
+ }
6590
+
6591
+ // packages/server/src/agents/claude-code/handback.ts
6592
+ var CLAUDE_CODE_CLEAR_GAP_MS = 2e3;
6593
+ var TAIL_BYTES = 512 * 1024;
6594
+ var MAX_TAIL_BYTES = 32 * 1024 * 1024;
6595
+ var HOUR_SLACK_MS = 2e3;
6596
+ var INTERRUPTED_MARKER = "[Request interrupted by user";
6597
+ var SETTLE_WITHOUT_STAMP_MS = 800;
6598
+ var FLUSH_GRACE_MS = 3e3;
6599
+ function normalize(text) {
6600
+ return cleanUserText(text).replace(/\s+/g, " ").trim();
6601
+ }
6602
+ function promptTextOf(record) {
6603
+ if (record["type"] !== "user" || record["isMeta"] === true || record["isSidechain"] === true) return null;
6604
+ const message = record["message"];
6605
+ if (typeof message !== "object" || message === null) return null;
6606
+ const content = message["content"];
6607
+ if (typeof content === "string") return content;
6608
+ if (!Array.isArray(content)) return null;
6609
+ const texts = [];
6610
+ for (const block of content) {
6611
+ if (typeof block !== "object" || block === null) continue;
6612
+ const record2 = block;
6613
+ if (record2["type"] === "tool_result") return null;
6614
+ if (record2["type"] === "text" && typeof record2["text"] === "string") texts.push(record2["text"]);
6615
+ }
6616
+ return texts.length === 0 ? null : texts.join("\n");
6617
+ }
6618
+ function findHandedBack(lines, submissions, interruptedAt) {
6619
+ const records = lines.map((line) => parseJsonlLine(line));
6620
+ const found = [];
6621
+ for (const submission of submissions) {
6622
+ const wanted = normalize(submission.text);
6623
+ if (wanted.length === 0) continue;
6624
+ let at = -1;
6625
+ for (let index = records.length - 1; index >= 0; index -= 1) {
6626
+ const record = records[index];
6627
+ if (record === null || record === void 0) continue;
6628
+ const text = promptTextOf(record);
6629
+ if (text !== null && normalize(text) === wanted) {
6630
+ at = index;
6631
+ break;
6632
+ }
6633
+ }
6634
+ if (at === -1) continue;
6635
+ const line = records[at];
6636
+ const uuid = line["uuid"];
6637
+ if (typeof uuid !== "string" || uuid.length === 0) continue;
6638
+ if (readTimestamp(line["timestamp"]) > interruptedAt + HOUR_SLACK_MS) continue;
6639
+ const parent = records.find((record) => record !== null && record !== void 0 && record["uuid"] === line["parentUuid"]);
6640
+ if (parent !== void 0 && parent !== null && (promptTextOf(parent) ?? "").startsWith(INTERRUPTED_MARKER)) continue;
6641
+ let answered = false;
6642
+ for (let index = at + 1; index < records.length; index += 1) {
6643
+ const record = records[index];
6644
+ if (record === null || record === void 0 || record["isSidechain"] === true) continue;
6645
+ if (record["type"] === "assistant" || record["type"] === "user") {
6646
+ answered = true;
6647
+ break;
6648
+ }
6649
+ }
6650
+ if (answered) continue;
6651
+ found.push({ text: submission.original ?? submission.text, eventId: uuid });
6652
+ }
6653
+ return found;
6654
+ }
6655
+ function holdsTwoTurns(lines) {
6656
+ let count = 0;
6657
+ for (const line of lines) {
6658
+ const record = parseJsonlLine(line);
6659
+ if (record === null || record["isSidechain"] === true) continue;
6660
+ if (record["type"] === "user" || record["type"] === "assistant") count += 1;
6661
+ if (count >= 2) return true;
6662
+ }
6663
+ return false;
6664
+ }
6665
+ async function readLastTurns(file) {
6666
+ const { size } = await stat13(file);
6667
+ for (let window = TAIL_BYTES; ; window *= 4) {
6668
+ const lines = await readTailLines(file, { maxBytes: window });
6669
+ if (window >= size || window >= MAX_TAIL_BYTES || holdsTwoTurns(lines)) return lines;
6670
+ }
6671
+ }
6672
+ function settledAfter(status, interruptedAt, now) {
6673
+ if (status === null || status.status !== "idle") return false;
6674
+ if (status.statusUpdatedAt !== null) return status.statusUpdatedAt >= interruptedAt;
6675
+ return now - interruptedAt >= SETTLE_WITHOUT_STAMP_MS;
6676
+ }
6677
+ function createClaudeCodeHandback(deps) {
6678
+ const now = deps.now ?? Date.now;
6679
+ return {
6680
+ clearGapMs: CLAUDE_CODE_CLEAR_GAP_MS,
6681
+ returnReason: "stopped-early",
6682
+ async check(target, submissions, interruptedAt) {
6683
+ try {
6684
+ const status = await deps.status(target.sessionId);
6685
+ const lines = await readLastTurns(deps.sessionFile(target.cwd, target.sessionId));
6686
+ const found = findHandedBack(lines, submissions, interruptedAt);
6687
+ const settled = settledAfter(status, interruptedAt, now());
6688
+ debugLog("write", `handback check: status ${status?.status ?? "none"}, settled ${settled}, handed back ${found.length}`);
6689
+ return {
6690
+ settled,
6691
+ undelivered: found.map((each) => each.text),
6692
+ withdrawn: found.map((each) => each.eventId),
6693
+ // Sin la CLI libre, o sin la linea todavia en disco, no se concluye nada.
6694
+ pending: !settled || found.length === 0 && now() - interruptedAt < FLUSH_GRACE_MS
6695
+ };
6696
+ } catch {
6697
+ debugLog("write", "handback check: no session file yet");
6698
+ return { settled: false, undelivered: [], pending: true };
6699
+ }
6700
+ }
6701
+ };
6702
+ }
6703
+
6704
+ // packages/server/src/agents/claude-code/rewind-menu.ts
6705
+ import { createReadStream } from "node:fs";
6706
+ import { createInterface } from "node:readline";
6707
+ var ESC = "\x1B";
6708
+ var UP = "\x1B[A";
6709
+ var DOWN = "\x1B[B";
6710
+ var ENTER = "\r";
6711
+ var CLEAR = "";
6712
+ var MARKER = "\u276F";
6713
+ var REWIND_OPTION_CODE_AND_CONVERSATION = "Restore code and conversation";
6714
+ var REWIND_OPTION_CONVERSATION = "Restore conversation";
6715
+ var ARROWS = /^[↑↓]\s*/;
6716
+ function readRewindScreen(lines) {
6717
+ const header = lines.findIndex((line) => line.trim() === "Rewind");
6718
+ if (header === -1) return { kind: "none" };
6719
+ const rest = lines.slice(header + 1).filter((line) => line.trim().length > 0);
6720
+ const intro = rest[0]?.trim() ?? "";
6721
+ if (intro.startsWith("Restore the code and/or conversation")) {
6722
+ const block = [];
6723
+ let selected;
6724
+ for (const raw of rest.slice(1)) {
6725
+ const line = raw.trim();
6726
+ if (line.startsWith("Enter to continue")) break;
6727
+ block.push(line);
6728
+ if (selected === void 0 && line.startsWith(MARKER)) {
6729
+ const text = line.slice(MARKER.length).trim();
6730
+ selected = text === "(current)" ? null : text;
6731
+ }
6732
+ }
6733
+ if (selected === void 0) return { kind: "none" };
6734
+ return { kind: "list", selected, block: block.join("\n") };
6735
+ }
6736
+ if (intro.startsWith("Confirm you want to restore")) {
6737
+ const quoted = [];
6738
+ const options = [];
6739
+ const block = [];
6740
+ for (const raw of rest.slice(1)) {
6741
+ const line = raw.trim();
6742
+ if (line.length === 0) continue;
6743
+ block.push(line);
6744
+ if (line.startsWith("\u2502")) {
6745
+ const text = line.slice(1).trim();
6746
+ if (!/^\(.*\bago\)$/.test(text)) quoted.push(text);
6747
+ continue;
6748
+ }
6749
+ const selected = line.startsWith(MARKER);
6750
+ const option = /^(\d+)\.\s+(.+)$/.exec(line.replace(MARKER, "").replace(ARROWS, "").trim());
6751
+ if (option !== null) {
6752
+ options.push({ number: Number(option[1]), label: (option[2] ?? "").trim(), selected });
6753
+ }
6754
+ }
6755
+ return { kind: "confirm", quoted, options, block: block.join("\n") };
6756
+ }
6757
+ return { kind: "none" };
6758
+ }
6759
+ function comparable(text) {
6760
+ return text.replace(/@"[^"]*"/g, " ").replace(/\[Image #\d+\]/g, " ").replace(/\s+/g, " ").trim();
6761
+ }
6762
+ function shownMatches(shown, firstLine2) {
6763
+ const cut2 = /…\s*$/.test(shown);
6764
+ const visible = comparable(shown.replace(/…\s*$/, ""));
6765
+ const wanted = comparable(firstLine2);
6766
+ if (visible.length === 0 || wanted.length === 0) return false;
6767
+ return cut2 ? wanted.startsWith(visible) : wanted === visible;
6768
+ }
6769
+ function editorLine(lines) {
6770
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
6771
+ const line = (lines[index] ?? "").trim();
6772
+ if (line.startsWith(MARKER)) return line.slice(MARKER.length).trim();
6773
+ }
6774
+ return null;
6775
+ }
6776
+ function editorHolds(lines, firstLine2) {
6777
+ const text = editorLine(lines);
6778
+ if (text === null || text.length === 0) return false;
6779
+ return shownMatches(text.endsWith("\u2026") ? text : `${text}\u2026`, firstLine2) || /^\[Pasted text #\d+/.test(text);
6780
+ }
6781
+ var DEFAULT_TIMING = { keyGapMs: 150, stepTimeoutMs: 3e3, pollMs: 50 };
6782
+ async function waitForScreen(io, timing, accept) {
6783
+ const deadline = io.now() + timing.stepTimeoutMs;
6784
+ let candidate = null;
6785
+ for (; ; ) {
6786
+ const lines = await io.screen();
6787
+ const screen = readRewindScreen(lines);
6788
+ const key = screen.kind === "none" ? editorLine(lines) ?? "" : screen.block;
6789
+ if (accept(screen, lines)) {
6790
+ if (candidate === key) return { screen, lines, ok: true };
6791
+ candidate = key;
6792
+ } else {
6793
+ candidate = null;
6794
+ }
6795
+ if (io.now() >= deadline) return { screen, lines, ok: false };
6796
+ await io.wait(timing.pollMs);
6797
+ }
6798
+ }
6799
+ async function closeMenu(io, timing) {
6800
+ for (let attempt = 0; attempt < 3; attempt += 1) {
6801
+ if (readRewindScreen(await io.screen()).kind === "none") return;
6802
+ if (!io.write(ESC)) return;
6803
+ await waitForScreen(io, timing, (screen) => screen.kind === "none");
6804
+ }
6805
+ }
6806
+ async function fail(io, timing, reason2) {
6807
+ await closeMenu(io, timing);
6808
+ return { kind: "failed", reason: reason2 };
6809
+ }
6810
+ async function runRewind(io, request, timing = DEFAULT_TIMING) {
6811
+ if (!io.write(ESC)) return { kind: "failed", reason: "gone" };
6812
+ await io.wait(timing.keyGapMs);
6813
+ if (!io.write(ESC)) return { kind: "failed", reason: "gone" };
6814
+ let current = await waitForScreen(io, timing, (screen) => screen.kind === "list");
6815
+ if (!current.ok) return fail(io, timing, "menu-not-open");
6816
+ let matches2 = 0;
6817
+ for (let step2 = 0; ; step2 += 1) {
6818
+ if (step2 >= request.maxSteps) return fail(io, timing, "message-not-found");
6819
+ const before = current.screen.kind === "list" ? current.screen.block : "";
6820
+ if (!io.write(UP)) return { kind: "failed", reason: "gone" };
6821
+ current = await waitForScreen(io, timing, (screen) => screen.kind === "list" && screen.block !== before);
6822
+ if (!current.ok || current.screen.kind !== "list") return fail(io, timing, "message-not-found");
6823
+ const selected = current.screen.selected;
6824
+ if (selected !== null && shownMatches(selected, request.firstLine)) {
6825
+ matches2 += 1;
6826
+ if (matches2 >= request.occurrence) break;
6827
+ }
6828
+ }
6829
+ if (!io.write(ENTER)) return { kind: "failed", reason: "gone" };
6830
+ current = await waitForScreen(io, timing, (screen) => screen.kind === "confirm");
6831
+ if (!current.ok || current.screen.kind !== "confirm") return fail(io, timing, "not-confirmed");
6832
+ const quotedFirst = current.screen.quoted[0] ?? "";
6833
+ if (!shownMatches(quotedFirst, request.firstLine) && !shownMatches(`${quotedFirst}\u2026`, request.firstLine)) {
6834
+ return fail(io, timing, "message-not-found");
6835
+ }
6836
+ let codeRestored = request.restoreCode;
6837
+ let wanted = current.screen.options.find(
6838
+ (option) => option.label === (request.restoreCode ? REWIND_OPTION_CODE_AND_CONVERSATION : REWIND_OPTION_CONVERSATION)
6839
+ );
6840
+ if (wanted === void 0 && request.restoreCode) {
6841
+ wanted = current.screen.options.find((option) => option.label === REWIND_OPTION_CONVERSATION);
6842
+ codeRestored = false;
6843
+ }
6844
+ const marked = current.screen.options.find((option) => option.selected);
6845
+ if (wanted === void 0 || marked === void 0) return fail(io, timing, "option-missing");
6846
+ const key = wanted.number > marked.number ? DOWN : UP;
6847
+ for (let moves = Math.abs(wanted.number - marked.number); moves > 0; moves -= 1) {
6848
+ const before = current.screen.kind === "confirm" ? current.screen.block : "";
6849
+ if (!io.write(key)) return { kind: "failed", reason: "gone" };
6850
+ current = await waitForScreen(io, timing, (screen) => screen.kind === "confirm" && screen.block !== before);
6851
+ if (!current.ok || current.screen.kind !== "confirm") return fail(io, timing, "option-missing");
6852
+ }
6853
+ const chosen = current.screen.kind === "confirm" ? current.screen.options.find((option) => option.selected) : void 0;
6854
+ if (chosen === void 0 || chosen.label !== wanted.label) return fail(io, timing, "option-missing");
6855
+ if (!io.write(ENTER)) return { kind: "failed", reason: "gone" };
6856
+ current = await waitForScreen(io, timing, (screen) => screen.kind === "none");
6857
+ let editorCleared = false;
6858
+ const holding = await waitForScreen(io, timing, (_screen, lines) => editorHolds(lines, request.firstLine));
6859
+ if (holding.ok && io.write(CLEAR)) {
6860
+ const cleared = await waitForScreen(io, timing, (_screen, lines) => !editorHolds(lines, request.firstLine));
6861
+ editorCleared = cleared.ok;
6862
+ }
6863
+ return { kind: "done", codeRestored, editorCleared };
6864
+ }
6865
+ async function readPromptText(cwd, sessionId, uuid) {
6866
+ const stream = createReadStream(sessionFilePath(cwd, sessionId), { encoding: "utf8" });
6867
+ const failed = new Promise((resolve) => stream.once("error", () => resolve(null)));
6868
+ const lines = createInterface({ input: stream, crlfDelay: Infinity });
6869
+ const found = (async () => {
6870
+ try {
6871
+ for await (const line of lines) {
6872
+ if (!line.includes(uuid)) continue;
6873
+ const record = parseJsonlLine(line.trim());
6874
+ if (record === null || record["uuid"] !== uuid) continue;
6875
+ const message = record["message"];
6876
+ if (typeof message !== "object" || message === null) return null;
6877
+ const content = message["content"];
6878
+ const raw = typeof content === "string" ? content : Array.isArray(content) ? content.flatMap(
6879
+ (block) => typeof block === "object" && block !== null && block["type"] === "text" ? [String(block["text"] ?? "")] : []
6880
+ ).join("\n") : null;
6881
+ if (raw === null) return null;
6882
+ return cleanUserText(raw).replace(/@"[^"]*\.(?:png|jpe?g|gif|webp)"/gi, "").replace(/[ \t]+\n/g, "\n").trim();
6883
+ }
6884
+ } catch {
6885
+ return null;
6886
+ }
6887
+ return null;
6888
+ })();
6889
+ try {
6890
+ return await Promise.race([found, failed]);
6891
+ } finally {
6892
+ lines.close();
6893
+ stream.destroy();
6894
+ }
6895
+ }
6896
+ function createClaudeCodeRewind(timing = DEFAULT_TIMING) {
6897
+ return {
6898
+ run: (io, request) => runRewind(io, request, timing),
6899
+ fullText: (target, eventId) => readPromptText(target.cwd, target.sessionId, eventId)
6900
+ };
6901
+ }
6902
+
6296
6903
  // packages/server/src/agents/claude-code/index.ts
6297
6904
  var CLAUDE_CODE_CAPABILITIES = {
6298
6905
  sessionIdAtLaunch: true,
@@ -6315,7 +6922,9 @@ var CLAUDE_CODE_CAPABILITIES = {
6315
6922
  contextWindowSource: "usage-with-variants",
6316
6923
  plans: true,
6317
6924
  // Hito 29: sin candado nuevo. Con la CLI esperando, el cuadro manda como siempre.
6318
- waitingBlocksSubmit: false
6925
+ waitingBlocksSubmit: false,
6926
+ // Su tecla de "enviar ya", Ctrl+X Ctrl+S, medida con la 2.1.301 (hito 45, §4.4.3).
6927
+ forceSend: true
6319
6928
  };
6320
6929
  var CLAUDE_CODE_INPUT = {
6321
6930
  imageReference: "at-quoted",
@@ -6399,6 +7008,18 @@ function createClaudeCodeAdapter() {
6399
7008
  },
6400
7009
  history: createClaudeCodeHistory(variants),
6401
7010
  status,
7011
+ /*
7012
+ Un mensaje detenido antes de que el agente hiciera nada: la CLI lo deshace
7013
+ y lo devuelve a su caja, y la app lo trae al cuadro (hito 44, §4.16).
7014
+ */
7015
+ queuedSubmits: createClaudeCodeHandback({
7016
+ sessionFile: sessionFilePath,
7017
+ status: (sessionId) => watcher.read(sessionId)
7018
+ }),
7019
+ // Volver a antes de un mensaje con el menu de rewind de la CLI (hito 44, §4.16).
7020
+ rewind: createClaudeCodeRewind(),
7021
+ // La cola la dice el JSONL (hito 45, §4.4.3), y "enviar ya" es su tecla.
7022
+ promptQueue: { source: "session", forceSend: "ctrl-x-ctrl-s" },
6402
7023
  defaults: (cwd, sessionModel) => readAgentDefaults(cwd, sessionModel ?? null),
6403
7024
  /** La carpeta de la CLI. No se lista ni se entra: regla 2.1. */
6404
7025
  protectedDirs: () => [path21.join(homedir5(), ".claude")],
@@ -6408,19 +7029,12 @@ function createClaudeCodeAdapter() {
6408
7029
 
6409
7030
  // packages/server/src/agents/codex/index.ts
6410
7031
  import { homedir as homedir7 } from "node:os";
6411
- import path27 from "node:path";
7032
+ import path28 from "node:path";
6412
7033
 
6413
7034
  // packages/server/src/agents/codex/discovery.ts
6414
- import { stat as stat13 } from "node:fs/promises";
7035
+ import { stat as stat14 } from "node:fs/promises";
6415
7036
  import path24 from "node:path";
6416
7037
 
6417
- // packages/server/src/debug.ts
6418
- var enabled = process.env["AGENT_WORKBENCH_DEBUG"] === "1";
6419
- function debugLog(scope, message) {
6420
- if (!enabled) return;
6421
- console.log(`[${scope}] ${message}`);
6422
- }
6423
-
6424
7038
  // packages/server/src/agents/codex/find-rollout.ts
6425
7039
  import { readdir as readdir8 } from "node:fs/promises";
6426
7040
  import path23 from "node:path";
@@ -6464,6 +7078,10 @@ function codexArchivedRoot() {
6464
7078
  const home = codexHome();
6465
7079
  return home === null ? null : path22.join(home, "archived_sessions");
6466
7080
  }
7081
+ function parseCompressedRolloutFileName(name) {
7082
+ if (!name.toLowerCase().endsWith(".jsonl.zst")) return null;
7083
+ return parseRolloutFileName(name.slice(0, -".zst".length));
7084
+ }
6467
7085
  function parseRolloutFileName(name) {
6468
7086
  const match = ROLLOUT_FILE_PATTERN.exec(name);
6469
7087
  if (match === null) return null;
@@ -6520,15 +7138,18 @@ async function entriesOf(folder) {
6520
7138
  async function numberedFolders(folder, pattern) {
6521
7139
  return (await entriesOf(folder)).filter((entry) => entry.isDirectory() && pattern.test(entry.name)).map((entry) => entry.name).sort();
6522
7140
  }
6523
- async function rolloutFilesIn(folder) {
6524
- return (await entriesOf(folder)).filter((entry) => entry.isFile() && parseRolloutFileName(entry.name) !== null).map((entry) => entry.name).sort().map((name) => path23.join(folder, name));
7141
+ async function filesIn(folder, parse) {
7142
+ return (await entriesOf(folder)).filter((entry) => entry.isFile() && parse(entry.name) !== null).map((entry) => entry.name).sort().map((name) => path23.join(folder, name));
7143
+ }
7144
+ function rolloutFilesIn(folder) {
7145
+ return filesIn(folder, parseRolloutFileName);
6525
7146
  }
6526
- async function walkSessionFiles(root, limit = Number.POSITIVE_INFINITY) {
7147
+ async function walk(root, limit, parse) {
6527
7148
  const files = [];
6528
7149
  for (const year of await numberedFolders(root, YEAR)) {
6529
7150
  for (const month of await numberedFolders(path23.join(root, year), TWO_DIGITS)) {
6530
7151
  for (const day of await numberedFolders(path23.join(root, year, month), TWO_DIGITS)) {
6531
- for (const file of await rolloutFilesIn(path23.join(root, year, month, day))) {
7152
+ for (const file of await filesIn(path23.join(root, year, month, day), parse)) {
6532
7153
  files.push(file);
6533
7154
  if (files.length >= limit) return files;
6534
7155
  }
@@ -6537,13 +7158,31 @@ async function walkSessionFiles(root, limit = Number.POSITIVE_INFINITY) {
6537
7158
  }
6538
7159
  return files;
6539
7160
  }
6540
- function matching(files, sessionId) {
7161
+ function walkSessionFiles(root, limit = Number.POSITIVE_INFINITY) {
7162
+ return walk(root, limit, parseRolloutFileName);
7163
+ }
7164
+ function compressedRolloutFilesIn(folder) {
7165
+ return filesIn(folder, parseCompressedRolloutFileName);
7166
+ }
7167
+ function walkCompressedSessionFiles(root, limit = Number.POSITIVE_INFINITY) {
7168
+ return walk(root, limit, parseCompressedRolloutFileName);
7169
+ }
7170
+ function matching(files, sessionId, parse) {
6541
7171
  for (const file of files) {
6542
- if (parseRolloutFileName(path23.basename(file))?.sessionId === sessionId) return file;
7172
+ if (parse(path23.basename(file))?.sessionId === sessionId) return file;
6543
7173
  }
6544
7174
  return null;
6545
7175
  }
6546
- async function findRollout(sessionId, roots = {}) {
7176
+ function findRollout(sessionId, roots = {}) {
7177
+ return find(sessionId, roots, parseRolloutFileName);
7178
+ }
7179
+ function findCompressedRollout(sessionId, roots = {}) {
7180
+ return find(sessionId, roots, parseCompressedRolloutFileName);
7181
+ }
7182
+ async function findReadableRollout(sessionId, roots = {}) {
7183
+ return await findRollout(sessionId, roots) ?? await findCompressedRollout(sessionId, roots);
7184
+ }
7185
+ async function find(sessionId, roots, parse) {
6547
7186
  const id = sessionId.toLowerCase();
6548
7187
  if (!isUuid(id)) return null;
6549
7188
  const sessionsRoot3 = roots.sessionsRoot !== void 0 ? roots.sessionsRoot : codexSessionsRoot();
@@ -6551,16 +7190,16 @@ async function findRollout(sessionId, roots = {}) {
6551
7190
  const ms = uuidV7Millis(id);
6552
7191
  if (ms !== null && sessionsRoot3 !== null) {
6553
7192
  for (const folder of localDayFolders(sessionsRoot3, ms - DAY_MS, ms + DAY_MS)) {
6554
- const found = matching(await rolloutFilesIn(folder), id);
7193
+ const found = matching(await filesIn(folder, parse), id, parse);
6555
7194
  if (found !== null) return found;
6556
7195
  }
6557
7196
  }
6558
7197
  if (archivedRoot !== null) {
6559
- const found = matching(await rolloutFilesIn(archivedRoot), id);
7198
+ const found = matching(await filesIn(archivedRoot, parse), id, parse);
6560
7199
  if (found !== null) return found;
6561
7200
  }
6562
7201
  if (ms === null && sessionsRoot3 !== null) {
6563
- return matching(await walkSessionFiles(sessionsRoot3, FULL_WALK_MAX_FILES), id);
7202
+ return matching(await walk(sessionsRoot3, FULL_WALK_MAX_FILES, parse), id, parse);
6564
7203
  }
6565
7204
  return null;
6566
7205
  }
@@ -6693,8 +7332,7 @@ function readToolOutput(output, maxChars = TOOL_RESULT_MAX_CHARS) {
6693
7332
  isError = exitCode !== null && exitCode !== 0;
6694
7333
  } else {
6695
7334
  text = output;
6696
- const match = /^Exit code: (\d+)/.exec(output);
6697
- isError = match !== null && Number(match[1]) !== 0;
7335
+ isError = commandFailed(output);
6698
7336
  }
6699
7337
  } else if (Array.isArray(output)) {
6700
7338
  ({ text, imageCount } = readOutputItems(output));
@@ -6709,6 +7347,14 @@ function readToolOutput(output, maxChars = TOOL_RESULT_MAX_CHARS) {
6709
7347
  const limited = cut(text, maxChars);
6710
7348
  return { text: limited.text, truncated: limited.truncated, isError, imageCount };
6711
7349
  }
7350
+ function commandFailed(output) {
7351
+ const legacy = /^Exit code: (-?\d+)/.exec(output);
7352
+ if (legacy !== null) return Number(legacy[1]) !== 0;
7353
+ const headerEnd = output.indexOf("\nOutput:");
7354
+ if (headerEnd === -1) return false;
7355
+ const unified = /^Process exited with code (-?\d+)\r?$/m.exec(output.slice(0, headerEnd));
7356
+ return unified !== null && Number(unified[1]) !== 0;
7357
+ }
6712
7358
  function parseObject(raw) {
6713
7359
  try {
6714
7360
  return asRecord(JSON.parse(raw));
@@ -6907,7 +7553,7 @@ var CodexSessionDiscovery = class {
6907
7553
  if (this.seen.has(filePath)) continue;
6908
7554
  let info;
6909
7555
  try {
6910
- info = await stat13(filePath);
7556
+ info = await stat14(filePath);
6911
7557
  } catch {
6912
7558
  continue;
6913
7559
  }
@@ -6994,8 +7640,76 @@ var CodexSessionDiscovery = class {
6994
7640
  };
6995
7641
 
6996
7642
  // packages/server/src/agents/codex/history.ts
6997
- import { stat as stat14 } from "node:fs/promises";
6998
- import path26 from "node:path";
7643
+ import { stat as stat16 } from "node:fs/promises";
7644
+ import path27 from "node:path";
7645
+
7646
+ // packages/server/src/agents/codex/compressed-rollout.ts
7647
+ import { readFile as readFile10, stat as stat15 } from "node:fs/promises";
7648
+ import path25 from "node:path";
7649
+ import { Decompress } from "fzstd";
7650
+ var COMPRESSED_ROLLOUT_SUFFIX = ".zst";
7651
+ var COMPRESSED_ROLLOUT_MAX_BYTES = 64 * 1024 * 1024;
7652
+ var DECOMPRESSED_ROLLOUT_MAX_BYTES = 256 * 1024 * 1024;
7653
+ function isCompressedRollout(filePath) {
7654
+ return filePath.toLowerCase().endsWith(`.jsonl${COMPRESSED_ROLLOUT_SUFFIX}`);
7655
+ }
7656
+ function plainRolloutPathOf(compressedPath) {
7657
+ return compressedPath.slice(0, -COMPRESSED_ROLLOUT_SUFFIX.length);
7658
+ }
7659
+ function compressedRolloutPathOf(plainPath) {
7660
+ return `${plainPath}${COMPRESSED_ROLLOUT_SUFFIX}`;
7661
+ }
7662
+ function decompressRolloutBytes(compressed) {
7663
+ const chunks = [];
7664
+ let total = 0;
7665
+ const stream = new Decompress((chunk) => {
7666
+ total += chunk.length;
7667
+ if (total > DECOMPRESSED_ROLLOUT_MAX_BYTES) {
7668
+ throw new Error(`Compressed rollout over ${DECOMPRESSED_ROLLOUT_MAX_BYTES} bytes once decompressed`);
7669
+ }
7670
+ chunks.push(chunk);
7671
+ });
7672
+ stream.push(compressed, true);
7673
+ return Buffer.concat(chunks, total);
7674
+ }
7675
+ async function readCompressedRollout(filePath) {
7676
+ const info = await stat15(filePath);
7677
+ if (info.size > COMPRESSED_ROLLOUT_MAX_BYTES) {
7678
+ throw new Error(`Compressed rollout over ${COMPRESSED_ROLLOUT_MAX_BYTES} bytes: ${path25.basename(filePath)}`);
7679
+ }
7680
+ return decompressRolloutBytes(await readFile10(filePath));
7681
+ }
7682
+ function memoryHandle(data) {
7683
+ return {
7684
+ async read(buffer, offset, length, position) {
7685
+ const start = Math.min(Math.max(0, position), data.length);
7686
+ const end = Math.min(data.length, start + Math.max(0, length));
7687
+ const bytesRead = data.copy(buffer, offset, start, end);
7688
+ return { bytesRead };
7689
+ },
7690
+ async stat() {
7691
+ return { size: data.length };
7692
+ },
7693
+ async close() {
7694
+ }
7695
+ };
7696
+ }
7697
+ function createRolloutFiles() {
7698
+ let cached = null;
7699
+ const decompressed = async (filePath) => {
7700
+ const info = await stat15(filePath);
7701
+ if (cached !== null && cached.filePath === filePath && cached.mtimeMs === info.mtimeMs && cached.size === info.size) {
7702
+ return cached.data;
7703
+ }
7704
+ const data = await readCompressedRollout(filePath);
7705
+ cached = { filePath, mtimeMs: info.mtimeMs, size: info.size, data };
7706
+ return data;
7707
+ };
7708
+ return {
7709
+ size: async (filePath) => isCompressedRollout(filePath) ? (await decompressed(filePath)).length : DISK_FILES.size(filePath),
7710
+ open: async (filePath) => isCompressedRollout(filePath) ? memoryHandle(await decompressed(filePath)) : DISK_FILES.open(filePath)
7711
+ };
7712
+ }
6999
7713
 
7000
7714
  // packages/server/src/agents/codex/rollout-scan.ts
7001
7715
  var TAIL_MAX_BYTES2 = 256 * 1024;
@@ -7023,6 +7737,7 @@ function listableMeta(line, item) {
7023
7737
  }
7024
7738
  async function scanRollout(item) {
7025
7739
  const found = { meta: null };
7740
+ const files = createRolloutFiles();
7026
7741
  const head = await readHeadLines(item.ref, {
7027
7742
  maxLines: ROLLOUT_HEAD_MAX_LINES,
7028
7743
  maxBytes: ROLLOUT_HEAD_MAX_BYTES,
@@ -7031,7 +7746,7 @@ async function scanRollout(item) {
7031
7746
  found.meta = listableMeta(line, item);
7032
7747
  return found.meta === null;
7033
7748
  }
7034
- });
7749
+ }, files);
7035
7750
  const meta = found.meta;
7036
7751
  if (meta === null) return null;
7037
7752
  let title = "";
@@ -7043,7 +7758,7 @@ async function scanRollout(item) {
7043
7758
  break;
7044
7759
  }
7045
7760
  let resumeCwd = null;
7046
- const tail = await readTailLines(item.ref, { maxBytes: TAIL_MAX_BYTES2 });
7761
+ const tail = await readTailLines(item.ref, { maxBytes: TAIL_MAX_BYTES2 }, files);
7047
7762
  for (let index = tail.length - 1; index >= 0; index -= 1) {
7048
7763
  const line = tail[index];
7049
7764
  const payload = line === void 0 ? null : payloadOf(line, "turn_context");
@@ -7069,11 +7784,11 @@ async function scanRollout(item) {
7069
7784
  }
7070
7785
 
7071
7786
  // packages/server/src/agents/codex/session-follower.ts
7072
- import path25 from "node:path";
7787
+ import path26 from "node:path";
7073
7788
 
7074
7789
  // packages/server/src/agents/codex/rollout-image.ts
7075
- import { createReadStream } from "node:fs";
7076
- import { createInterface } from "node:readline";
7790
+ import { createReadStream as createReadStream2 } from "node:fs";
7791
+ import { createInterface as createInterface2 } from "node:readline";
7077
7792
  var MAX_IMAGE_BYTES2 = MAX_SUBMIT_IMAGE_BYTES;
7078
7793
  var DATA_URL = /^data:([^;,]+);base64,(.*)$/s;
7079
7794
  async function loadRolloutImage(filePath, contentLine, index) {
@@ -7099,9 +7814,10 @@ async function loadRolloutImage(filePath, contentLine, index) {
7099
7814
  return { mediaType, data };
7100
7815
  }
7101
7816
  async function readLine(filePath, lineNumber) {
7102
- const stream = createReadStream(filePath, { encoding: "utf8" });
7817
+ if (isCompressedRollout(filePath)) return readCompressedLine(filePath, lineNumber);
7818
+ const stream = createReadStream2(filePath, { encoding: "utf8" });
7103
7819
  const failed = new Promise((resolve) => stream.once("error", () => resolve(null)));
7104
- const lines = createInterface({ input: stream, crlfDelay: Infinity });
7820
+ const lines = createInterface2({ input: stream, crlfDelay: Infinity });
7105
7821
  const found = (async () => {
7106
7822
  let current = 0;
7107
7823
  try {
@@ -7121,6 +7837,24 @@ async function readLine(filePath, lineNumber) {
7121
7837
  stream.destroy();
7122
7838
  }
7123
7839
  }
7840
+ async function readCompressedLine(filePath, lineNumber) {
7841
+ let data;
7842
+ try {
7843
+ data = await readCompressedRollout(filePath);
7844
+ } catch {
7845
+ return null;
7846
+ }
7847
+ let start = 0;
7848
+ for (let current = 1; current < lineNumber; current += 1) {
7849
+ const newline2 = data.indexOf(10, start);
7850
+ if (newline2 === -1) return null;
7851
+ start = newline2 + 1;
7852
+ }
7853
+ if (start >= data.length) return null;
7854
+ const newline = data.indexOf(10, start);
7855
+ const line = data.toString("utf8", start, newline === -1 ? data.length : newline);
7856
+ return line.endsWith("\r") ? line.slice(0, -1) : line;
7857
+ }
7124
7858
 
7125
7859
  // packages/server/src/agents/codex/rollout-sink.ts
7126
7860
  var TURN_END_TYPES = ["task_complete", "turn_complete", "turn_aborted"];
@@ -7389,20 +8123,38 @@ function prettyJson(raw) {
7389
8123
 
7390
8124
  // packages/server/src/agents/codex/session-follower.ts
7391
8125
  var LOOKUP_INTERVAL_MS = 2e3;
8126
+ function sessionIdOf(filePath) {
8127
+ const name = path26.basename(filePath);
8128
+ return (parseRolloutFileName(name) ?? parseCompressedRolloutFileName(name))?.sessionId ?? null;
8129
+ }
8130
+ function samePath(a, b) {
8131
+ return a === b || normalizeCwdKey(a, process.platform) === normalizeCwdKey(b, process.platform);
8132
+ }
7392
8133
  var CodexSessionFollower = class {
7393
8134
  constructor(cwd, sessionId, options = {}) {
7394
8135
  this.cwd = cwd;
7395
8136
  this.sessionId = sessionId;
7396
8137
  this.sink = new CodexRolloutSink(sessionId, options.limits);
7397
- this.jsonl = new JsonlFollower(null, this.sink, { maxEvents: options.maxEvents });
8138
+ this.maxEvents = options.maxEvents;
8139
+ this.jsonl = this.followerFor(null);
7398
8140
  this.now = options.now ?? Date.now;
7399
8141
  }
7400
8142
  cwd;
7401
8143
  sessionId;
7402
8144
  sink;
7403
8145
  jsonl;
8146
+ maxEvents;
7404
8147
  now;
7405
8148
  lastLookupAt = null;
8149
+ /** Llego un aviso de la otra forma de su sesion: buscar otra vez en el proximo `poll`. */
8150
+ relookupPending = false;
8151
+ followerFor(filePath) {
8152
+ return new JsonlFollower(filePath, this.sink, { maxEvents: this.maxEvents, files: createRolloutFiles() });
8153
+ }
8154
+ /** El rollout que lee, para que la barra se entere de lo que el watcher no ve (§10.10). */
8155
+ get sourcePath() {
8156
+ return this.jsonl.filePath;
8157
+ }
7406
8158
  get label() {
7407
8159
  const filePath = this.jsonl.filePath;
7408
8160
  if (filePath !== null) return filePath;
@@ -7416,28 +8168,58 @@ var CodexSessionFollower = class {
7416
8168
  await this.lookup();
7417
8169
  }
7418
8170
  this.sink.takeTurns(/* @__PURE__ */ new Set());
7419
- const { reset, added } = await this.jsonl.poll();
8171
+ let { reset, added } = await this.jsonl.poll();
8172
+ if (await this.relookup()) {
8173
+ ({ added } = await this.jsonl.poll());
8174
+ reset = true;
8175
+ }
7420
8176
  const fresh = new Set(added.map((event2) => event2.eventId));
7421
8177
  return { reset, added, turns: this.sink.takeTurns(fresh), plans: [], parts: [] };
7422
8178
  }
8179
+ /**
8180
+ * Cambia de archivo si la sesion cambio de forma. true si cambio.
8181
+ *
8182
+ * Se busca con un aviso de la otra forma, o si el archivo seguido ya no esta
8183
+ * (con el mismo intervalo que la busqueda inicial: son `readdir`). El orden de
8184
+ * los avisos no importa: si el comprimido aparece antes de que se borre el
8185
+ * `.jsonl`, la busqueda devuelve el `.jsonl` y no se cambia nada, y cuando se
8186
+ * borra, el archivo seguido ya no esta.
8187
+ */
8188
+ async relookup() {
8189
+ const own = this.jsonl.filePath;
8190
+ if (own === null || this.sessionId.length === 0) return false;
8191
+ const vanished = this.jsonl.getState() === "waiting";
8192
+ const due = this.lastLookupAt === null || this.now() - this.lastLookupAt >= LOOKUP_INTERVAL_MS;
8193
+ if (!this.relookupPending && !(vanished && due)) return false;
8194
+ this.relookupPending = false;
8195
+ this.lastLookupAt = this.now();
8196
+ const found = await findReadableRollout(this.sessionId);
8197
+ if (found === null || samePath(found, own) || this.jsonl.filePath !== own) return false;
8198
+ this.sink.reset();
8199
+ this.jsonl = this.followerFor(found);
8200
+ return true;
8201
+ }
7423
8202
  async lookup() {
7424
8203
  if (this.sessionId.length === 0 || this.jsonl.filePath !== null) return;
7425
8204
  this.lastLookupAt = this.now();
7426
- const found = await findRollout(this.sessionId);
8205
+ const found = await findReadableRollout(this.sessionId);
7427
8206
  if (found !== null && this.jsonl.filePath === null) this.jsonl.setPath(found);
7428
8207
  }
7429
8208
  /**
7430
8209
  * true si el aviso es de su archivo. Si todavia no tiene uno y el aviso es un
7431
8210
  * rollout con su id, se queda con ese: es el watcher llegando antes que el
7432
- * reintento de busqueda.
8211
+ * reintento de busqueda. Si tiene uno y el aviso es la otra forma de su
8212
+ * sesion, la busca otra vez en el proximo `poll` (§10.12).
7433
8213
  */
7434
8214
  noticeChange(filePath) {
7435
8215
  const own = this.jsonl.filePath;
8216
+ if (own !== null && samePath(own, filePath)) return true;
8217
+ if (this.sessionId.length === 0) return false;
8218
+ if (sessionIdOf(filePath) !== this.sessionId.toLowerCase()) return false;
7436
8219
  if (own !== null) {
7437
- return own === filePath || normalizeCwdKey(own, process.platform) === normalizeCwdKey(filePath, process.platform);
8220
+ this.relookupPending = true;
8221
+ return true;
7438
8222
  }
7439
- if (this.sessionId.length === 0) return false;
7440
- if (parseRolloutFileName(path25.basename(filePath))?.sessionId !== this.sessionId.toLowerCase()) return false;
7441
8223
  this.jsonl.setPath(filePath);
7442
8224
  return true;
7443
8225
  }
@@ -7475,30 +8257,39 @@ var CodexSessionFollower = class {
7475
8257
  // packages/server/src/agents/codex/history.ts
7476
8258
  async function pathExists2(candidate) {
7477
8259
  try {
7478
- await stat14(candidate);
8260
+ await stat16(candidate);
7479
8261
  return true;
7480
8262
  } catch {
7481
8263
  return false;
7482
8264
  }
7483
8265
  }
7484
- var accepts = (filePath) => ROLLOUT_FILE_PATTERN.test(path26.basename(filePath));
8266
+ function rolloutNameOf(filePath) {
8267
+ const name = path27.basename(filePath);
8268
+ return parseRolloutFileName(name) ?? parseCompressedRolloutFileName(name);
8269
+ }
8270
+ var accepts = (filePath) => rolloutNameOf(filePath) !== null;
7485
8271
  var CODEX_FOLLOW_POLL_MS = 1e3;
7486
8272
  function isInside2(root, filePath) {
7487
- const relative = path26.relative(root, filePath);
7488
- return relative.length > 0 && !relative.startsWith("..") && !path26.isAbsolute(relative);
8273
+ const relative = path27.relative(root, filePath);
8274
+ return relative.length > 0 && !relative.startsWith("..") && !path27.isAbsolute(relative);
7489
8275
  }
7490
8276
  function toItem2(ref, info) {
7491
- const name = parseRolloutFileName(path26.basename(ref));
8277
+ const name = rolloutNameOf(ref);
7492
8278
  if (name === null) return null;
7493
8279
  return { ref, sessionId: name.sessionId, group: name.sessionId, mtimeMs: info.mtimeMs, sizeBytes: info.size };
7494
8280
  }
7495
8281
  async function itemOf(ref) {
8282
+ if (isCompressedRollout(ref) && await pathExists2(plainRolloutPathOf(ref))) return null;
7496
8283
  try {
7497
- return toItem2(ref, await stat14(ref));
8284
+ return toItem2(ref, await stat16(ref));
7498
8285
  } catch {
7499
8286
  return null;
7500
8287
  }
7501
8288
  }
8289
+ function withoutPlainTwin(compressed, plain) {
8290
+ const plainSet = new Set(plain);
8291
+ return compressed.filter((ref) => !plainSet.has(plainRolloutPathOf(ref)));
8292
+ }
7502
8293
  function createCodexHistory() {
7503
8294
  return {
7504
8295
  /*
@@ -7525,10 +8316,15 @@ function createCodexHistory() {
7525
8316
  const hasSessions = await pathExists2(sessionsRoot3);
7526
8317
  const hasArchived = await pathExists2(archivedRoot);
7527
8318
  if (!hasSessions && !hasArchived) return null;
7528
- const refs = [
8319
+ const plain = [
7529
8320
  ...hasSessions ? await walkSessionFiles(sessionsRoot3) : [],
7530
8321
  ...hasArchived ? await rolloutFilesIn(archivedRoot) : []
7531
8322
  ];
8323
+ const compressed = [
8324
+ ...hasSessions ? await walkCompressedSessionFiles(sessionsRoot3) : [],
8325
+ ...hasArchived ? await compressedRolloutFilesIn(archivedRoot) : []
8326
+ ];
8327
+ const refs = [...plain, ...withoutPlainTwin(compressed, plain)];
7532
8328
  const items = [];
7533
8329
  for (const ref of refs) {
7534
8330
  const item = await itemOf(ref);
@@ -7536,10 +8332,17 @@ function createCodexHistory() {
7536
8332
  }
7537
8333
  return items;
7538
8334
  },
8335
+ /*
8336
+ Las dos formas de la sesion, siempre juntas: cuando `codex resume`
8337
+ reemplaza el comprimido por el `.jsonl`, o la app de escritorio comprime
8338
+ uno, cual de las dos se lista depende de la otra (`itemOf`).
8339
+ */
7539
8340
  async changedRefs(filePath) {
7540
8341
  if (!accepts(filePath)) return null;
7541
8342
  const roots = [codexSessionsRoot(), codexArchivedRoot()];
7542
- return roots.some((root) => root !== null && isInside2(root, filePath)) ? [filePath] : null;
8343
+ if (!roots.some((root) => root !== null && isInside2(root, filePath))) return null;
8344
+ const plain = isCompressedRollout(filePath) ? plainRolloutPathOf(filePath) : filePath;
8345
+ return [plain, compressedRolloutPathOf(plain)];
7543
8346
  },
7544
8347
  item: (ref) => itemOf(ref),
7545
8348
  scan: (item) => scanRollout(item),
@@ -7548,8 +8351,12 @@ function createCodexHistory() {
7548
8351
  /**
7549
8352
  * true si esa sesion tiene rollout. El `cwd` no importa: la carpeta de
7550
8353
  * Codex es por dia, no por proyecto.
8354
+ *
8355
+ * Un rollout comprimido tambien cuenta: `codex resume` lo reemplaza por el
8356
+ * `.jsonl` y sigue escribiendo ahi (§10.12). Sin eso, despertar la pestana
8357
+ * de una sesion comprimida abria una sesion nueva.
7551
8358
  */
7552
- exists: async (_cwd, sessionId) => await findRollout(sessionId) !== null,
8359
+ exists: async (_cwd, sessionId) => await findReadableRollout(sessionId) !== null,
7553
8360
  follow(target, options) {
7554
8361
  return new CodexSessionFollower(target.cwd, target.sessionId, {
7555
8362
  limits: options?.limits,
@@ -7601,7 +8408,9 @@ var CODEX_CAPABILITIES = {
7601
8408
  // `update_plan` es una lista de pasos, no un archivo.
7602
8409
  plans: false,
7603
8410
  // Sin estado publicado no hay espera que mirar: manda el candado de 10.8.
7604
- waitingBlocksSubmit: false
8411
+ waitingBlocksSubmit: false,
8412
+ // No hace cola: un mensaje con el agente trabajando le llega al modelo en el acto (hito 45).
8413
+ forceSend: false
7605
8414
  };
7606
8415
  function codexInput(platform) {
7607
8416
  const windows = platform === "win32";
@@ -7688,7 +8497,7 @@ function createCodexAdapter() {
7688
8497
  * credenciales de antes de cambiarla.
7689
8498
  */
7690
8499
  protectedDirs: () => {
7691
- const dirs = [path27.join(homedir7(), ".codex")];
8500
+ const dirs = [path28.join(homedir7(), ".codex")];
7692
8501
  const home = codexHome();
7693
8502
  if (home !== null && !dirs.includes(home)) dirs.unshift(home);
7694
8503
  return dirs;
@@ -7881,7 +8690,7 @@ var DbChangeSignal = class {
7881
8690
  };
7882
8691
 
7883
8692
  // packages/server/src/agents/opencode/history.ts
7884
- import { stat as stat15 } from "node:fs/promises";
8693
+ import { stat as stat17 } from "node:fs/promises";
7885
8694
 
7886
8695
  // packages/server/src/agents/opencode/events.ts
7887
8696
  var TOOL_INPUT_PARSE_MAX_CHARS = 16e3;
@@ -8610,7 +9419,61 @@ var OpenCodeSessionFollower = class {
8610
9419
  }
8611
9420
  return false;
8612
9421
  }
9422
+ /**
9423
+ * Los mensajes del usuario que esperan en la cola de OpenCode (hito 45,
9424
+ * §11.13).
9425
+ *
9426
+ * OpenCode escribe el mensaje en la base en el acto, aunque el agente este
9427
+ * contestando otro: medido con la 1.19.1, el hilo lo mostraba a los 0,9 s
9428
+ * como uno mas, y la respuesta que se seguia escribiendo aparecia despues
9429
+ * encima de el. Su TUI lo marca "QUEUED" con una regla que se lee en el
9430
+ * binario: va despues de la respuesta que se esta escribiendo, la ultima sin
9431
+ * `time.completed` detras de la ultima completa. Es la misma, mas el corte
9432
+ * del proceso: una respuesta que quedo a medias con un proceso anterior no se
9433
+ * va a terminar nunca.
9434
+ *
9435
+ * Detenida, la respuesta se cierra (con `MessageAbortedError`) y el mensaje
9436
+ * sale de la cola sin contestar: OpenCode no lo atiende despues (medido), y
9437
+ * el hilo lo muestra donde esta. El mensaje ya es un evento: va con su
9438
+ * `eventId` y la web lo esconde de su lugar mientras espera.
9439
+ */
9440
+ queuedPrompts(launchedAt) {
9441
+ const messages = this.visibleMessages;
9442
+ const lastCompleted = findLastIndex(messages, (message) => message.role === "assistant" && message.completed_at !== null);
9443
+ const pending = findLastIndex(
9444
+ messages,
9445
+ (message, index) => index > lastCompleted && message.role === "assistant" && message.completed_at === null
9446
+ );
9447
+ const running = messages[pending];
9448
+ if (running === void 0 || running.time_created < launchedAt) return [];
9449
+ const queued = [];
9450
+ for (const message of messages.slice(pending + 1)) {
9451
+ if (message.role !== "user") continue;
9452
+ const event2 = this.delivered.find((each) => each.eventId === message.id);
9453
+ if (event2 === void 0) continue;
9454
+ const text = event2.parts.flatMap((part) => part.kind === "text" ? [part.text] : []).join("\n").trim();
9455
+ const images = event2.parts.filter((part) => part.kind === "image").length;
9456
+ if (text.length === 0 && images === 0) continue;
9457
+ queued.push({
9458
+ id: message.id,
9459
+ eventId: message.id,
9460
+ text: text.slice(0, MAX_QUEUED_PROMPT_CHARS),
9461
+ truncated: text.length > MAX_QUEUED_PROMPT_CHARS,
9462
+ images,
9463
+ at: event2.at
9464
+ });
9465
+ if (queued.length >= MAX_QUEUED_PROMPTS) break;
9466
+ }
9467
+ return queued;
9468
+ }
8613
9469
  };
9470
+ function findLastIndex(items, test) {
9471
+ for (let index = items.length - 1; index >= 0; index -= 1) {
9472
+ const item = items[index];
9473
+ if (item !== void 0 && test(item, index)) return index;
9474
+ }
9475
+ return -1;
9476
+ }
8614
9477
  function addPart(target, part) {
8615
9478
  let byId = target.get(part.message_id);
8616
9479
  if (byId === void 0) {
@@ -8832,7 +9695,7 @@ function createOpenCodeHistory(deps) {
8832
9695
  async rootExists() {
8833
9696
  if (dbFile === null) return false;
8834
9697
  try {
8835
- await stat15(dbFile);
9698
+ await stat17(dbFile);
8836
9699
  return true;
8837
9700
  } catch {
8838
9701
  return false;
@@ -8864,13 +9727,13 @@ function createOpenCodeHistory(deps) {
8864
9727
  var OPENCODE_SESSION_ID_PATTERN = /^ses_[0-9A-Za-z]{20,40}$/;
8865
9728
 
8866
9729
  // packages/server/src/agents/opencode/paths.ts
8867
- import path28 from "node:path";
9730
+ import path29 from "node:path";
8868
9731
  function isFullyQualified2(candidate, platform) {
8869
9732
  if (platform === "win32") return /^([a-zA-Z]:[\\/]|[\\/]{2})/.test(candidate);
8870
- return path28.posix.isAbsolute(candidate);
9733
+ return path29.posix.isAbsolute(candidate);
8871
9734
  }
8872
9735
  function resolveOpenCodePaths(env, home, platform) {
8873
- const flavor = platform === "win32" ? path28.win32 : path28.posix;
9736
+ const flavor = platform === "win32" ? path29.win32 : path29.posix;
8874
9737
  const variable = (name) => {
8875
9738
  const value = env[name];
8876
9739
  return value === void 0 || value.length === 0 ? null : value;
@@ -9319,7 +10182,7 @@ import { execFile as execFile2, spawn as nodeSpawn } from "node:child_process";
9319
10182
  import { randomBytes } from "node:crypto";
9320
10183
  import { mkdir as mkdir4 } from "node:fs/promises";
9321
10184
  import { tmpdir as tmpdir4 } from "node:os";
9322
- import path29 from "node:path";
10185
+ import path30 from "node:path";
9323
10186
  var OPENCODE_SERVE_HOST = "127.0.0.1";
9324
10187
  var OPENCODE_SERVE_START_TIMEOUT_MS = 2e4;
9325
10188
  var OPENCODE_SERVE_IDLE_STOP_MS = 3e5;
@@ -9351,7 +10214,7 @@ function parseListeningLine(line) {
9351
10214
  return `http://${OPENCODE_SERVE_HOST}:${found.port}`;
9352
10215
  }
9353
10216
  function defaultServeCwd() {
9354
- return path29.join(tmpdir4(), "agent-workbench", "opencode-serve");
10217
+ return path30.join(tmpdir4(), "agent-workbench", "opencode-serve");
9355
10218
  }
9356
10219
  var ServeStartError = class extends Error {
9357
10220
  };
@@ -9485,7 +10348,7 @@ var OpenCodeServeProcess = class {
9485
10348
  this.running = running;
9486
10349
  return new Promise((resolve, reject) => {
9487
10350
  let settled = false;
9488
- const fail = (reason2) => {
10351
+ const fail2 = (reason2) => {
9489
10352
  if (settled) return;
9490
10353
  settled = true;
9491
10354
  this.timers.clearTimeout(timer);
@@ -9495,7 +10358,7 @@ var OpenCodeServeProcess = class {
9495
10358
  const timer = this.timers.setTimeout(
9496
10359
  () => {
9497
10360
  const ms = this.startTimeoutMs;
9498
- fail(`didn't say which port it listens on within ${ms >= 1e3 ? `${Math.round(ms / 1e3)} s` : `${ms} ms`}`);
10361
+ fail2(`didn't say which port it listens on within ${ms >= 1e3 ? `${Math.round(ms / 1e3)} s` : `${ms} ms`}`);
9499
10362
  },
9500
10363
  this.startTimeoutMs
9501
10364
  );
@@ -9512,7 +10375,7 @@ var OpenCodeServeProcess = class {
9512
10375
  return;
9513
10376
  }
9514
10377
  const other = matchListeningLine(line);
9515
- if (other !== null) fail(`listens on ${other.scheme}://${other.host} and not on http://${OPENCODE_SERVE_HOST}`);
10378
+ if (other !== null) fail2(`listens on ${other.scheme}://${other.host} and not on http://${OPENCODE_SERVE_HOST}`);
9516
10379
  };
9517
10380
  const read = (stream) => {
9518
10381
  if (stream === null) return;
@@ -9527,13 +10390,13 @@ var OpenCodeServeProcess = class {
9527
10390
  };
9528
10391
  read(child.stdout);
9529
10392
  read(child.stderr);
9530
- child.once("error", (error) => fail(error.message));
10393
+ child.once("error", (error) => fail2(error.message));
9531
10394
  child.once("close", (code, signal) => {
9532
10395
  if (this.running === running) this.running = null;
9533
10396
  const how = signal ?? `code ${code ?? "?"}`;
9534
10397
  if (!settled) {
9535
10398
  const tail = running.tail.trim();
9536
- fail(`exited before listening (${how})${tail.length > 0 ? `: ${tail}` : ""}`);
10399
+ fail2(`exited before listening (${how})${tail.length > 0 ? `: ${tail}` : ""}`);
9537
10400
  return;
9538
10401
  }
9539
10402
  const endpoint = running.endpoint;
@@ -9626,6 +10489,9 @@ var SUBMIT = "\r";
9626
10489
  var END_KEY = "\x1B[F";
9627
10490
  var INTERRUPT = "\x1B";
9628
10491
  var CLEAR_EDITOR = "";
10492
+ function buildForceSendKeys(method) {
10493
+ return method === "ctrl-x-ctrl-s" ? "" : "\x1Bm";
10494
+ }
9629
10495
  var MAX_SUBMIT_CHARS = 2e5;
9630
10496
  function sanitizeForPaste(text) {
9631
10497
  return text.replace(/\x1b\[20[01]~/g, "").replace(/\r\n?/g, "\n").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
@@ -10216,7 +11082,9 @@ var OPENCODE_CAPABILITIES = {
10216
11082
  en el menu del TUI. El candado de §10.8 (`hasOpenToolCall`) no sirve: se
10217
11083
  apaga con `statusSource`.
10218
11084
  */
10219
- waitingBlocksSubmit: true
11085
+ waitingBlocksSubmit: true,
11086
+ // Detenerla con algo en cola lo deja sin contestar: no hay forma de forzarlo (hito 45, §11.13).
11087
+ forceSend: false
10220
11088
  };
10221
11089
  var OPENCODE_INPUT = {
10222
11090
  imageReference: "bare-path-paste",
@@ -10334,6 +11202,8 @@ function createOpenCodeAdapter(options = {}) {
10334
11202
  }),
10335
11203
  status,
10336
11204
  questions,
11205
+ // La cola la dice la base: un mensaje sin respuesta detras de uno que se esta contestando (hito 45, §11.13).
11206
+ promptQueue: { source: "session", forceSend: null },
10337
11207
  /**
10338
11208
  * La base que se lee, para el arranque. Solo si hay algo que decir, para que
10339
11209
  * quien no usa OpenCode no vea una linea nueva:
@@ -10379,13 +11249,13 @@ function createOpenCodeAdapter(options = {}) {
10379
11249
  }
10380
11250
 
10381
11251
  // packages/server/src/agents/pi/index.ts
10382
- import { stat as stat21 } from "node:fs/promises";
11252
+ import { stat as stat23 } from "node:fs/promises";
10383
11253
  import { homedir as homedir10 } from "node:os";
10384
- import path37 from "node:path";
11254
+ import path38 from "node:path";
10385
11255
 
10386
11256
  // packages/server/src/agents/pi/catalog.ts
10387
11257
  import { readFile as readFileFromDisk2, stat as statFromDisk2 } from "node:fs/promises";
10388
- import path30 from "node:path";
11258
+ import path31 from "node:path";
10389
11259
  var PI_CATALOG_RECHECK_MS = 3e4;
10390
11260
  var PI_MODEL_LIST_MAX = 300;
10391
11261
  var STORE_MAX_BYTES = 8 * 1024 * 1024;
@@ -10542,7 +11412,7 @@ var PiModelCatalog = class {
10542
11412
  let windows = /* @__PURE__ */ new Map();
10543
11413
  for (const dataDir of packageDataDirs(packageDir)) {
10544
11414
  try {
10545
- windows = packageWindows(JSON.parse(await this.readFile(path30.join(dataDir, `${provider}.json`))));
11415
+ windows = packageWindows(JSON.parse(await this.readFile(path31.join(dataDir, `${provider}.json`))));
10546
11416
  break;
10547
11417
  } catch {
10548
11418
  }
@@ -10554,8 +11424,8 @@ var PiModelCatalog = class {
10554
11424
  function packageDataDirs(packageDir) {
10555
11425
  const tail = ["pi-ai", "dist", "providers", "data"];
10556
11426
  return [
10557
- path30.join(packageDir, "node_modules", "@earendil-works", ...tail),
10558
- path30.join(packageDir, "..", ...tail)
11427
+ path31.join(packageDir, "node_modules", "@earendil-works", ...tail),
11428
+ path31.join(packageDir, "..", ...tail)
10559
11429
  ];
10560
11430
  }
10561
11431
 
@@ -10582,22 +11452,22 @@ var PI_READY_AFTER_LAUNCH_MS = 3e3;
10582
11452
  var PI_CLEAR_GAP_MS = 750;
10583
11453
 
10584
11454
  // packages/server/src/agents/pi/history.ts
10585
- import { stat as stat18 } from "node:fs/promises";
10586
- import path34 from "node:path";
11455
+ import { stat as stat20 } from "node:fs/promises";
11456
+ import path35 from "node:path";
10587
11457
 
10588
11458
  // packages/server/src/agents/pi/paths.ts
10589
- import { readdir as readdir9, stat as stat16 } from "node:fs/promises";
11459
+ import { readdir as readdir9, stat as stat18 } from "node:fs/promises";
10590
11460
  import { homedir as homedir9 } from "node:os";
10591
- import path31 from "node:path";
11461
+ import path32 from "node:path";
10592
11462
  var warned = /* @__PURE__ */ new Set();
10593
11463
  function isFullyQualified3(candidate) {
10594
- if (!path31.isAbsolute(candidate)) return false;
11464
+ if (!path32.isAbsolute(candidate)) return false;
10595
11465
  return process.platform !== "win32" || /^([a-zA-Z]:[\\/]|[\\/]{2})/.test(candidate);
10596
11466
  }
10597
11467
  function resolveConfigured(name, value) {
10598
11468
  if (value === "~") return homedir9();
10599
- if (value.startsWith("~/") || value.startsWith("~\\")) return path31.join(homedir9(), value.slice(2));
10600
- if (isFullyQualified3(value)) return path31.normalize(value);
11469
+ if (value.startsWith("~/") || value.startsWith("~\\")) return path32.join(homedir9(), value.slice(2));
11470
+ if (isFullyQualified3(value)) return path32.normalize(value);
10601
11471
  const key = `${name}=${value}`;
10602
11472
  if (!warned.has(key)) {
10603
11473
  warned.add(key);
@@ -10607,7 +11477,7 @@ function resolveConfigured(name, value) {
10607
11477
  }
10608
11478
  function piAgentDir() {
10609
11479
  const configured = process.env["PI_CODING_AGENT_DIR"];
10610
- if (configured === void 0 || configured.length === 0) return path31.join(homedir9(), ".pi", "agent");
11480
+ if (configured === void 0 || configured.length === 0) return path32.join(homedir9(), ".pi", "agent");
10611
11481
  return resolveConfigured("PI_CODING_AGENT_DIR", configured);
10612
11482
  }
10613
11483
  function piSessionsLayout() {
@@ -10617,14 +11487,14 @@ function piSessionsLayout() {
10617
11487
  return root === null ? null : { root, flat: true };
10618
11488
  }
10619
11489
  const agentDir = piAgentDir();
10620
- return agentDir === null ? null : { root: path31.join(agentDir, "sessions"), flat: false };
11490
+ return agentDir === null ? null : { root: path32.join(agentDir, "sessions"), flat: false };
10621
11491
  }
10622
11492
  function piModelsStorePath() {
10623
11493
  const agentDir = piAgentDir();
10624
- return agentDir === null ? null : path31.join(agentDir, "models-store.json");
11494
+ return agentDir === null ? null : path32.join(agentDir, "models-store.json");
10625
11495
  }
10626
11496
  function projectFolderName(cwd) {
10627
- const resolved = path31.resolve(cwd);
11497
+ const resolved = path32.resolve(cwd);
10628
11498
  return `--${resolved.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
10629
11499
  }
10630
11500
  function isProjectFolderName(name) {
@@ -10651,12 +11521,12 @@ function parseSessionFileName(name) {
10651
11521
  return Number.isFinite(startedAt) ? { sessionId, startedAt } : null;
10652
11522
  }
10653
11523
  function isInside3(root, filePath) {
10654
- const relative = path31.relative(root, filePath);
10655
- return relative.length > 0 && !relative.startsWith("..") && !path31.isAbsolute(relative);
11524
+ const relative = path32.relative(root, filePath);
11525
+ return relative.length > 0 && !relative.startsWith("..") && !path32.isAbsolute(relative);
10656
11526
  }
10657
11527
  function sessionDirFor(cwd, layout = piSessionsLayout()) {
10658
11528
  if (layout === null) return null;
10659
- return layout.flat ? layout.root : path31.join(layout.root, projectFolderName(cwd));
11529
+ return layout.flat ? layout.root : path32.join(layout.root, projectFolderName(cwd));
10660
11530
  }
10661
11531
  async function sessionFilesIn(dir) {
10662
11532
  try {
@@ -10670,7 +11540,7 @@ async function projectFolders(layout) {
10670
11540
  if (layout.flat) return [];
10671
11541
  try {
10672
11542
  const entries = await readdir9(layout.root, { withFileTypes: true });
10673
- return entries.filter((entry) => entry.isDirectory() && isProjectFolderName(entry.name)).map((entry) => path31.join(layout.root, entry.name)).sort();
11543
+ return entries.filter((entry) => entry.isDirectory() && isProjectFolderName(entry.name)).map((entry) => path32.join(layout.root, entry.name)).sort();
10674
11544
  } catch {
10675
11545
  return [];
10676
11546
  }
@@ -10679,7 +11549,7 @@ async function listSessionFiles(layout) {
10679
11549
  const dirs = layout.flat ? [layout.root] : await projectFolders(layout);
10680
11550
  const files = [];
10681
11551
  for (const dir of dirs) {
10682
- for (const name of await sessionFilesIn(dir)) files.push(path31.join(dir, name));
11552
+ for (const name of await sessionFilesIn(dir)) files.push(path32.join(dir, name));
10683
11553
  }
10684
11554
  return files;
10685
11555
  }
@@ -10695,14 +11565,14 @@ async function findSessionFile(sessionId, cwd) {
10695
11565
  }
10696
11566
  }
10697
11567
  for (const dir of dirs) {
10698
- const matches = (await sessionFilesIn(dir)).filter((name) => parseSessionFileName(name)?.sessionId === sessionId);
10699
- if (matches.length === 0) continue;
10700
- if (matches.length === 1) return path31.join(dir, matches[0] ?? "");
11568
+ const matches2 = (await sessionFilesIn(dir)).filter((name) => parseSessionFileName(name)?.sessionId === sessionId);
11569
+ if (matches2.length === 0) continue;
11570
+ if (matches2.length === 1) return path32.join(dir, matches2[0] ?? "");
10701
11571
  let best = null;
10702
- for (const name of matches) {
11572
+ for (const name of matches2) {
10703
11573
  try {
10704
- const info = await stat16(path31.join(dir, name));
10705
- if (best === null || info.mtimeMs > best.mtimeMs) best = { file: path31.join(dir, name), mtimeMs: info.mtimeMs };
11574
+ const info = await stat18(path32.join(dir, name));
11575
+ if (best === null || info.mtimeMs > best.mtimeMs) best = { file: path32.join(dir, name), mtimeMs: info.mtimeMs };
10706
11576
  } catch {
10707
11577
  }
10708
11578
  }
@@ -10712,7 +11582,7 @@ async function findSessionFile(sessionId, cwd) {
10712
11582
  }
10713
11583
 
10714
11584
  // packages/server/src/agents/pi/session-follower.ts
10715
- import path33 from "node:path";
11585
+ import path34 from "node:path";
10716
11586
 
10717
11587
  // packages/server/src/agents/pi/session-entries.ts
10718
11588
  var NOTICE_DETAIL_MAX_CHARS2 = 300;
@@ -10932,10 +11802,10 @@ function readEntry(record, lineNumber, limits) {
10932
11802
  }
10933
11803
 
10934
11804
  // packages/server/src/agents/pi/session-image.ts
10935
- import { createReadStream as createReadStream2 } from "node:fs";
10936
- import { readFile as readFile10, stat as stat17 } from "node:fs/promises";
10937
- import path32 from "node:path";
10938
- import { createInterface as createInterface2 } from "node:readline";
11805
+ import { createReadStream as createReadStream3 } from "node:fs";
11806
+ import { readFile as readFile11, stat as stat19 } from "node:fs/promises";
11807
+ import path33 from "node:path";
11808
+ import { createInterface as createInterface3 } from "node:readline";
10939
11809
  function tooBig2(data) {
10940
11810
  return data.length > MAX_SUBMIT_IMAGE_BYTES * 1.4;
10941
11811
  }
@@ -10957,18 +11827,18 @@ function textOf2(message) {
10957
11827
  return blocksOf2(message).filter((block) => block["type"] === "text" && typeof block["text"] === "string").map((block) => block["text"]).join("\n");
10958
11828
  }
10959
11829
  function sameFile(a, b) {
10960
- const normalize = (value) => {
10961
- const normalized = path32.normalize(value);
11830
+ const normalize3 = (value) => {
11831
+ const normalized = path33.normalize(value);
10962
11832
  return process.platform === "win32" ? normalized.toLowerCase() : normalized;
10963
11833
  };
10964
- return normalize(a) === normalize(b);
11834
+ return normalize3(a) === normalize3(b);
10965
11835
  }
10966
11836
  async function readPastedFile2(target, pastedDir) {
10967
11837
  try {
10968
- const inside = await resolveInside(pastedDir, path32.relative(pastedDir, target), { mustExist: true });
10969
- const info = await stat17(inside);
11838
+ const inside = await resolveInside(pastedDir, path33.relative(pastedDir, target), { mustExist: true });
11839
+ const info = await stat19(inside);
10970
11840
  if (!info.isFile() || info.size === 0 || info.size > MAX_SUBMIT_IMAGE_BYTES) return null;
10971
- const bytes = await readFile10(inside);
11841
+ const bytes = await readFile11(inside);
10972
11842
  const format = detectImageFormat(bytes);
10973
11843
  return format === null ? null : { mediaType: format.mediaType, data: bytes.toString("base64") };
10974
11844
  } catch {
@@ -10989,12 +11859,12 @@ function readCallOf(record, target) {
10989
11859
  async function loadPiImage(file, eventId, index, source, pastedDir = pasteRoot()) {
10990
11860
  if (source === "attachment" || !Number.isInteger(index) || index < 0) return null;
10991
11861
  try {
10992
- if (!(await stat17(file)).isFile()) return null;
11862
+ if (!(await stat19(file)).isFile()) return null;
10993
11863
  } catch {
10994
11864
  return null;
10995
11865
  }
10996
- const stream = createReadStream2(file, { encoding: "utf8" });
10997
- const lines = createInterface2({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
11866
+ const stream = createReadStream3(file, { encoding: "utf8" });
11867
+ const lines = createInterface3({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
10998
11868
  stream.on("error", () => lines.close());
10999
11869
  let target = null;
11000
11870
  let readId = null;
@@ -11359,7 +12229,7 @@ var PiSessionFollower = class {
11359
12229
  return own === filePath || normalizeCwdKey(own, process.platform) === normalizeCwdKey(filePath, process.platform);
11360
12230
  }
11361
12231
  if (!isPiSessionId(this.sessionId)) return false;
11362
- if (parseSessionFileName(path33.basename(filePath))?.sessionId !== this.sessionId) return false;
12232
+ if (parseSessionFileName(path34.basename(filePath))?.sessionId !== this.sessionId) return false;
11363
12233
  this.jsonl.setPath(filePath);
11364
12234
  return true;
11365
12235
  }
@@ -11468,29 +12338,29 @@ async function scanPiSession(item) {
11468
12338
 
11469
12339
  // packages/server/src/agents/pi/history.ts
11470
12340
  var PI_FOLLOW_POLL_MS = 1e3;
11471
- var accepts2 = (filePath) => parseSessionFileName(path34.basename(filePath)) !== null;
12341
+ var accepts2 = (filePath) => parseSessionFileName(path35.basename(filePath)) !== null;
11472
12342
  function toItem3(ref, info) {
11473
- const name = parseSessionFileName(path34.basename(ref));
12343
+ const name = parseSessionFileName(path35.basename(ref));
11474
12344
  if (name === null) return null;
11475
12345
  return {
11476
12346
  ref,
11477
12347
  sessionId: name.sessionId,
11478
12348
  // La carpeta de proyecto: respaldo del `cwd` si la primera linea no lo trajera.
11479
- group: path34.basename(path34.dirname(ref)),
12349
+ group: path35.basename(path35.dirname(ref)),
11480
12350
  mtimeMs: info.mtimeMs,
11481
12351
  sizeBytes: info.size
11482
12352
  };
11483
12353
  }
11484
12354
  async function itemOf3(ref) {
11485
12355
  try {
11486
- const info = await stat18(ref);
12356
+ const info = await stat20(ref);
11487
12357
  return info.isFile() ? toItem3(ref, info) : null;
11488
12358
  } catch {
11489
12359
  return null;
11490
12360
  }
11491
12361
  }
11492
12362
  function ignoresPath(root, filePath) {
11493
- const relative = path34.relative(root, filePath).split(path34.sep).join("/");
12363
+ const relative = path35.relative(root, filePath).split(path35.sep).join("/");
11494
12364
  if (relative.length === 0) return false;
11495
12365
  if (relative.startsWith("..")) return true;
11496
12366
  const parts = relative.split("/");
@@ -11518,7 +12388,7 @@ function createPiHistory(catalog) {
11518
12388
  const layout = piSessionsLayout();
11519
12389
  if (layout === null) return null;
11520
12390
  try {
11521
- if (!(await stat18(layout.root)).isDirectory()) return null;
12391
+ if (!(await stat20(layout.root)).isDirectory()) return null;
11522
12392
  } catch {
11523
12393
  return null;
11524
12394
  }
@@ -11561,7 +12431,7 @@ function createPiHistory(catalog) {
11561
12431
  }
11562
12432
 
11563
12433
  // packages/server/src/agents/pi/live.ts
11564
- import { open as open6, stat as stat19 } from "node:fs/promises";
12434
+ import { open as open5, stat as stat21 } from "node:fs/promises";
11565
12435
  var PI_LIVE_POLL_MS = 500;
11566
12436
  var PI_ERROR_CALM_MS = 2e3;
11567
12437
  var PI_TRUST_WAITING = "trust dialog";
@@ -11716,7 +12586,7 @@ var PiLiveSessions = class {
11716
12586
  if (live.file === null) live.file = await this.findFile(live.sessionId, live.cwd).catch(() => null);
11717
12587
  if (live.file === null || fromStart) return;
11718
12588
  try {
11719
- live.offset = (await stat19(live.file)).size;
12589
+ live.offset = (await stat21(live.file)).size;
11720
12590
  } catch {
11721
12591
  live.offset = 0;
11722
12592
  }
@@ -11734,7 +12604,7 @@ var PiLiveSessions = class {
11734
12604
  }
11735
12605
  let size;
11736
12606
  try {
11737
- size = (await stat19(live.file)).size;
12607
+ size = (await stat21(live.file)).size;
11738
12608
  } catch {
11739
12609
  return;
11740
12610
  }
@@ -11744,7 +12614,7 @@ var PiLiveSessions = class {
11744
12614
  live.lineNumber = 0;
11745
12615
  }
11746
12616
  if (size === live.offset) return;
11747
- const handle = await open6(live.file, "r");
12617
+ const handle = await open5(live.file, "r");
11748
12618
  try {
11749
12619
  const buffer = Buffer.allocUnsafe(READ_CHUNK_BYTES2);
11750
12620
  while (live.offset < size) {
@@ -11818,14 +12688,14 @@ var PiStatusSource = class {
11818
12688
  };
11819
12689
 
11820
12690
  // packages/server/src/agents/pi/locate.ts
11821
- import { readFile as readFile11, realpath as realpath2 } from "node:fs/promises";
11822
- import path35 from "node:path";
12691
+ import { readFile as readFile12, realpath as realpath2 } from "node:fs/promises";
12692
+ import path36 from "node:path";
11823
12693
  var PACKAGE_PARTS = PI_PACKAGE_NAME.split("/");
11824
12694
  var MANAGED_VERSION_PATTERN = /^[0-9A-Za-z._+-]+$/;
11825
12695
  var WALK_UP_LEVELS = 6;
11826
12696
  async function packageVersion(dir, read) {
11827
12697
  try {
11828
- const manifest = asRecord(JSON.parse(await read(path35.join(dir, "package.json"))));
12698
+ const manifest = asRecord(JSON.parse(await read(path36.join(dir, "package.json"))));
11829
12699
  if (manifest === null || manifest["name"] !== PI_PACKAGE_NAME) return null;
11830
12700
  const version = manifest["version"];
11831
12701
  return typeof version === "string" && version.length > 0 ? version : null;
@@ -11834,24 +12704,24 @@ async function packageVersion(dir, read) {
11834
12704
  }
11835
12705
  }
11836
12706
  async function candidateDirs(resolvedPath, deps) {
11837
- const dir = path35.dirname(resolvedPath);
12707
+ const dir = path36.dirname(resolvedPath);
11838
12708
  const candidates = [
11839
- path35.join(dir, "node_modules", ...PACKAGE_PARTS),
11840
- path35.join(dir, "..", "lib", "node_modules", ...PACKAGE_PARTS)
12709
+ path36.join(dir, "node_modules", ...PACKAGE_PARTS),
12710
+ path36.join(dir, "..", "lib", "node_modules", ...PACKAGE_PARTS)
11841
12711
  ];
11842
12712
  try {
11843
- const installRoot = path35.join(dir, "..", "install");
11844
- const version = (await deps.readFile(path35.join(installRoot, "current-version"))).trim();
12713
+ const installRoot = path36.join(dir, "..", "install");
12714
+ const version = (await deps.readFile(path36.join(installRoot, "current-version"))).trim();
11845
12715
  if (version !== "." && version !== ".." && MANAGED_VERSION_PATTERN.test(version)) {
11846
- candidates.push(path35.join(installRoot, "releases", version, "node_modules", ...PACKAGE_PARTS));
12716
+ candidates.push(path36.join(installRoot, "releases", version, "node_modules", ...PACKAGE_PARTS));
11847
12717
  }
11848
12718
  } catch {
11849
12719
  }
11850
12720
  try {
11851
- let cursor = path35.dirname(await deps.realpath(resolvedPath));
12721
+ let cursor = path36.dirname(await deps.realpath(resolvedPath));
11852
12722
  for (let level = 0; level < WALK_UP_LEVELS; level += 1) {
11853
12723
  candidates.push(cursor);
11854
- const parent = path35.dirname(cursor);
12724
+ const parent = path36.dirname(cursor);
11855
12725
  if (parent === cursor) break;
11856
12726
  cursor = parent;
11857
12727
  }
@@ -11863,7 +12733,7 @@ var warnedForeign = /* @__PURE__ */ new Set();
11863
12733
  async function locatePi(overrides = {}) {
11864
12734
  const deps = {
11865
12735
  findInPath: overrides.findInPath ?? findInPath,
11866
- readFile: overrides.readFile ?? ((file) => readFile11(file, "utf8")),
12736
+ readFile: overrides.readFile ?? ((file) => readFile12(file, "utf8")),
11867
12737
  realpath: overrides.realpath ?? realpath2
11868
12738
  };
11869
12739
  const resolvedPath = await deps.findInPath(PI_COMMAND);
@@ -11882,8 +12752,8 @@ async function locatePi(overrides = {}) {
11882
12752
  }
11883
12753
 
11884
12754
  // packages/server/src/agents/pi/tab-watch.ts
11885
- import { readdir as readdir10, stat as stat20 } from "node:fs/promises";
11886
- import path36 from "node:path";
12755
+ import { readdir as readdir10, stat as stat22 } from "node:fs/promises";
12756
+ import path37 from "node:path";
11887
12757
  var TRUST_TITLE = "Trust project folder?";
11888
12758
  var INTERFACE_STARTED = "\x1B[?1049h";
11889
12759
  function trustDialogOpen(output) {
@@ -11912,8 +12782,8 @@ async function listSessionsIn(dir) {
11912
12782
  for (const name of names) {
11913
12783
  if (parseSessionFileName(name) === null) continue;
11914
12784
  try {
11915
- const info = await stat20(path36.join(dir, name));
11916
- if (info.isFile()) files.push({ file: path36.join(dir, name), size: info.size, mtimeMs: info.mtimeMs });
12785
+ const info = await stat22(path37.join(dir, name));
12786
+ if (info.isFile()) files.push({ file: path37.join(dir, name), size: info.size, mtimeMs: info.mtimeMs });
11917
12787
  } catch {
11918
12788
  }
11919
12789
  }
@@ -12053,7 +12923,7 @@ var PiTabWatch = class {
12053
12923
  const previous = this.sizes;
12054
12924
  this.sizes = new Map(files.map((each) => [each.file, each.size]));
12055
12925
  if (this.file === null && this.sessionId.length > 0) {
12056
- this.file = files.find((each) => parseSessionFileName(path36.basename(each.file))?.sessionId === this.sessionId)?.file ?? null;
12926
+ this.file = files.find((each) => parseSessionFileName(path37.basename(each.file))?.sessionId === this.sessionId)?.file ?? null;
12057
12927
  }
12058
12928
  if (previous === null) return;
12059
12929
  const own = this.currentFile;
@@ -12167,7 +13037,9 @@ var PI_BASE_CAPABILITIES = {
12167
13037
  contextWindowSource: "usage-with-catalog",
12168
13038
  plans: false,
12169
13039
  // Con el dialogo de confianza abierto, el Enter de un mensaje lo aceptaria (16.11).
12170
- waitingBlocksSubmit: true
13040
+ waitingBlocksSubmit: true,
13041
+ // Sin tecla de "enviar ya": se detiene y se vuelve a mandar lo que tenia en cola (hito 45, §16.18).
13042
+ forceSend: true
12171
13043
  };
12172
13044
  var PI_INPUT = {
12173
13045
  imageReference: "quoted-path-line",
@@ -12191,7 +13063,7 @@ function createPiAdapter(options = {}) {
12191
13063
  const file = piModelsStorePath();
12192
13064
  if (file === null) return "none";
12193
13065
  try {
12194
- const info = await stat21(file);
13066
+ const info = await stat23(file);
12195
13067
  return `${info.mtimeMs}:${info.size}`;
12196
13068
  } catch {
12197
13069
  return "none";
@@ -12265,8 +13137,15 @@ function createPiAdapter(options = {}) {
12265
13137
  status,
12266
13138
  queuedSubmits: {
12267
13139
  clearGapMs: PI_CLEAR_GAP_MS,
13140
+ returnReason: "returned-by-cli",
12268
13141
  check: (target, submissions, interruptedAt) => live.check(target.sessionId, submissions, interruptedAt)
12269
13142
  },
13143
+ /*
13144
+ Pi no escribe su cola hasta entregarla (16.5): es lo que la app le mando
13145
+ con el agente trabajando. Sin tecla de "enviar ya", se detiene y se vuelve
13146
+ a mandar lo que devolvio a su editor (hito 45, §16.18).
13147
+ */
13148
+ promptQueue: { source: "app", forceSend: "interrupt-and-resend" },
12270
13149
  // `settings.json` no se abre: sin modelo por defecto que anunciar. Al reanudar,
12271
13150
  // el modelo y el nivel salen de la propia sesion.
12272
13151
  defaults: () => Promise.resolve(null),
@@ -12277,9 +13156,9 @@ function createPiAdapter(options = {}) {
12277
13156
  * un proyecto.
12278
13157
  */
12279
13158
  protectedDirs: () => {
12280
- const dirs = [path37.join(homedir10(), ".pi")];
13159
+ const dirs = [path38.join(homedir10(), ".pi")];
12281
13160
  const agentDir = piAgentDir();
12282
- if (agentDir !== null && !dirs.some((dir) => agentDir === dir || agentDir.startsWith(`${dir}${path37.sep}`))) {
13161
+ if (agentDir !== null && !dirs.some((dir) => agentDir === dir || agentDir.startsWith(`${dir}${path38.sep}`))) {
12283
13162
  dirs.unshift(agentDir);
12284
13163
  }
12285
13164
  return dirs;
@@ -12542,11 +13421,137 @@ async function migrateLegacyConfigDir() {
12542
13421
 
12543
13422
  // packages/server/src/conversation-hub.ts
12544
13423
  import { EventEmitter } from "node:events";
12545
- import path38 from "node:path";
13424
+ import path39 from "node:path";
13425
+
13426
+ // packages/server/src/app-prompt-queue.ts
13427
+ var CLOCK_SLACK_MS3 = 2e3;
13428
+ function normalize2(text) {
13429
+ return text.replace(/\s+/g, " ").trim();
13430
+ }
13431
+ function userTextOf(event2) {
13432
+ if (event2.role !== "user") return null;
13433
+ const texts = [];
13434
+ let images = 0;
13435
+ for (const part of event2.parts) {
13436
+ if (part.kind === "text") texts.push(part.text);
13437
+ else if (part.kind === "image") images += 1;
13438
+ else if (part.kind === "tool-result" || part.kind === "tool-call") return null;
13439
+ }
13440
+ return { text: normalize2(texts.join(" ")), images };
13441
+ }
13442
+ function matches(entry, message) {
13443
+ const wanted = normalize2(entry.text);
13444
+ if (wanted.length === 0) return message.text.length === 0 && message.images >= entry.images && entry.images > 0;
13445
+ return message.text === wanted || message.text.includes(wanted) || message.text.length >= 200 && wanted.startsWith(message.text);
13446
+ }
13447
+ var AppPromptQueue = class {
13448
+ byTerminal = /* @__PURE__ */ new Map();
13449
+ seq = 0;
13450
+ /** La app termino de escribir un mensaje con el agente trabajando. */
13451
+ note(terminalId, input) {
13452
+ if (input.text.trim().length === 0 && input.images === 0) return;
13453
+ this.seq += 1;
13454
+ const entries = this.byTerminal.get(terminalId) ?? [];
13455
+ entries.push({ id: `app-${this.seq}`, text: input.text, images: input.images, at: input.at, forced: false });
13456
+ this.byTerminal.set(terminalId, entries.slice(-MAX_QUEUED_PROMPTS));
13457
+ }
13458
+ /**
13459
+ * Saca las que ya aparecieron en el hilo. Cada mensaje del hilo se lleva una
13460
+ * sola entrada, la mas vieja que le corresponde. true si cambio algo.
13461
+ */
13462
+ settle(terminalId, events) {
13463
+ const entries = this.byTerminal.get(terminalId);
13464
+ if (entries === void 0 || entries.length === 0) return false;
13465
+ const used = /* @__PURE__ */ new Set();
13466
+ const remaining = entries.filter((entry) => {
13467
+ for (const event2 of events) {
13468
+ if (used.has(event2.eventId) || event2.at < entry.at - CLOCK_SLACK_MS3) continue;
13469
+ const message = userTextOf(event2);
13470
+ if (message === null || !matches(entry, message)) continue;
13471
+ used.add(event2.eventId);
13472
+ return false;
13473
+ }
13474
+ return true;
13475
+ });
13476
+ if (remaining.length === entries.length) return false;
13477
+ this.set(terminalId, remaining);
13478
+ return true;
13479
+ }
13480
+ /** Se forzo el envio: lo que esperaba ya se le esta pasando al agente. true si cambio algo. */
13481
+ markForced(terminalId) {
13482
+ const entries = this.byTerminal.get(terminalId);
13483
+ if (entries === void 0 || entries.every((entry) => entry.forced)) return false;
13484
+ this.set(terminalId, entries.map((entry) => ({ ...entry, forced: true })));
13485
+ return true;
13486
+ }
13487
+ /** Nada sigue en cola: la pestana quedo sin proceso o libre, o Detener lo devolvio al cuadro. true si habia algo. */
13488
+ clear(terminalId) {
13489
+ const had = (this.byTerminal.get(terminalId)?.length ?? 0) > 0;
13490
+ this.byTerminal.delete(terminalId);
13491
+ return had;
13492
+ }
13493
+ list(terminalId) {
13494
+ return (this.byTerminal.get(terminalId) ?? []).map((entry) => {
13495
+ const text = entry.text.trim();
13496
+ const prompt = {
13497
+ id: entry.id,
13498
+ text: text.slice(0, MAX_QUEUED_PROMPT_CHARS),
13499
+ truncated: text.length > MAX_QUEUED_PROMPT_CHARS,
13500
+ images: entry.images,
13501
+ at: entry.at
13502
+ };
13503
+ if (entry.forced) prompt.forced = true;
13504
+ return prompt;
13505
+ });
13506
+ }
13507
+ set(terminalId, entries) {
13508
+ if (entries.length === 0) this.byTerminal.delete(terminalId);
13509
+ else this.byTerminal.set(terminalId, entries);
13510
+ }
13511
+ };
13512
+
13513
+ // packages/server/src/rewind-target.ts
13514
+ function promptTextOf2(event2) {
13515
+ if (event2.role !== "user") return null;
13516
+ if (event2.parts.some((part) => part.kind === "tool-result" || part.kind === "notice")) return null;
13517
+ const texts = event2.parts.flatMap((part) => part.kind === "text" ? [part] : []);
13518
+ if (texts.length === 0 && !event2.parts.some((part) => part.kind === "image")) return null;
13519
+ return {
13520
+ text: texts.map((part) => part.text).join("\n"),
13521
+ truncated: texts.some((part) => part.truncated)
13522
+ };
13523
+ }
13524
+ function firstLineOf(text) {
13525
+ return text.split("\n").map((line) => line.trim()).find((line) => line.length > 0) ?? "";
13526
+ }
13527
+ var comparable2 = (line) => line.replace(/\s+/g, " ").trim();
13528
+ function rewindTargetFrom(events, eventId) {
13529
+ const index = events.findIndex((event3) => event3.eventId === eventId);
13530
+ if (index === -1) return null;
13531
+ const event2 = events[index];
13532
+ const prompt = event2 === void 0 ? null : promptTextOf2(event2);
13533
+ if (prompt === null) return null;
13534
+ const firstLine2 = firstLineOf(prompt.text);
13535
+ let occurrence = 0;
13536
+ let promptsFromHere = 0;
13537
+ for (const later of events.slice(index)) {
13538
+ const text = promptTextOf2(later);
13539
+ if (text === null) continue;
13540
+ promptsFromHere += 1;
13541
+ if (comparable2(firstLineOf(text.text)) === comparable2(firstLine2)) occurrence += 1;
13542
+ }
13543
+ return { text: prompt.text, truncated: prompt.truncated, firstLine: firstLine2, occurrence, promptsFromHere };
13544
+ }
13545
+
13546
+ // packages/server/src/conversation-hub.ts
12546
13547
  var INITIAL_EVENT_LIMIT = 300;
12547
13548
  var MAX_PAGE_LIMIT = 500;
12548
13549
  var PENDING_QUESTION_LOOKBACK = 50;
12549
13550
  var POLL_DEBOUNCE_MS = 150;
13551
+ var GROWTH_REFRESH_MS = 3e3;
13552
+ var WITHDRAWAL_PENDING_MS = 15e3;
13553
+ var IDLE_CLEARS_QUEUE_MS = 3e3;
13554
+ var QUEUE_SETTLE_LOOKBACK = 50;
12550
13555
  function sameSubagents(a, b) {
12551
13556
  return a.length === b.length && a.every((item, index) => item.toolUseId === b[index]?.toolUseId && item.background === b[index]?.background);
12552
13557
  }
@@ -12561,11 +13566,39 @@ var ConversationHub = class extends EventEmitter {
12561
13566
  this.refreshOpenToolCall(terminalId, entry);
12562
13567
  this.refreshSubagents(terminalId, entry);
12563
13568
  }
13569
+ for (const terminalId of /* @__PURE__ */ new Set([...this.entries.keys(), ...this.announcedQueues.keys()])) {
13570
+ if (this.registry.launchedAtOf(terminalId) === null) this.appQueue.clear(terminalId);
13571
+ this.refreshQueue(terminalId);
13572
+ }
12564
13573
  });
13574
+ registry.on("activity", (terminalId, activity) => this.noteActivity(terminalId, activity));
12565
13575
  }
12566
13576
  registry;
12567
13577
  agents;
12568
13578
  entries = /* @__PURE__ */ new Map();
13579
+ /**
13580
+ * La cola de las CLIs que no la escriben en ningun lado (hito 45): lo que la
13581
+ * app les mando con el agente trabajando. Vive aca y no en el seguidor porque
13582
+ * no sale del historial, y tiene que sobrevivir a que nadie mire la pestana.
13583
+ */
13584
+ appQueue = new AppPromptQueue();
13585
+ /** La ultima cola avisada de cada pestana, por su clave: un aviso igual no se repite. */
13586
+ announcedQueues = /* @__PURE__ */ new Map();
13587
+ /** La ultima actividad vista de cada pestana: solo cuentan los cambios. */
13588
+ lastActivity = /* @__PURE__ */ new Map();
13589
+ /** El temporizador de cada pestana que quedo libre (`IDLE_CLEARS_QUEUE_MS`). */
13590
+ idleTimers = /* @__PURE__ */ new Map();
13591
+ /** Las pestanas libres hace mas de `IDLE_CLEARS_QUEUE_MS`: no tienen nada en cola. */
13592
+ idleSettled = /* @__PURE__ */ new Set();
13593
+ /**
13594
+ * El mensaje que la CLI deshizo sin escribirlo todavia, por pestana (hito 44,
13595
+ * §4.16): desde ahi el hilo se esconde. Vive aca y no en el seguidor porque
13596
+ * tiene que sobrevivir a que nadie mire la conversacion: un seguidor nuevo
13597
+ * relee el archivo, donde lo deshecho sigue igual, y se lo vuelve a esconder.
13598
+ * Se olvida cuando la CLI escribe otra cosa (el seguidor lo suelta), al
13599
+ * cambiar de sesion o al cerrar la pestana.
13600
+ */
13601
+ withdrawals = /* @__PURE__ */ new Map();
12569
13602
  /**
12570
13603
  * Engancha un cliente mas a la conversacion de una pestana y devuelve lo que
12571
13604
  * se sabe hasta ahora.
@@ -12603,13 +13636,14 @@ var ConversationHub = class extends EventEmitter {
12603
13636
  await entry.follower.start();
12604
13637
  debugLog(
12605
13638
  "conversation",
12606
- `following ${path38.basename(entry.follower.label)} for ${terminalId.slice(0, 8)}`
13639
+ `following ${path39.basename(entry.follower.label)} for ${terminalId.slice(0, 8)}`
12607
13640
  );
12608
13641
  entry = this.entries.get(terminalId) ?? entry;
12609
13642
  }
12610
13643
  entry.refs += 1;
12611
13644
  entry.primed = true;
12612
13645
  await this.runPoll(terminalId, entry, { silent: true });
13646
+ this.applyWithdrawal(terminalId, entry);
12613
13647
  const tail = entry.follower.getTail(INITIAL_EVENT_LIMIT);
12614
13648
  const plans = entry.adapter.history.plans;
12615
13649
  return {
@@ -12674,7 +13708,8 @@ var ConversationHub = class extends EventEmitter {
12674
13708
  subagents: [],
12675
13709
  primed: false,
12676
13710
  stopWatchingStatus: null,
12677
- followTicker: null
13711
+ followTicker: null,
13712
+ growthTimer: null
12678
13713
  };
12679
13714
  }
12680
13715
  /**
@@ -12697,6 +13732,25 @@ var ConversationHub = class extends EventEmitter {
12697
13732
  stopFollowTicker(entry) {
12698
13733
  if (entry.followTicker !== null) clearInterval(entry.followTicker);
12699
13734
  entry.followTicker = null;
13735
+ if (entry.growthTimer !== null) clearTimeout(entry.growthTimer);
13736
+ entry.growthTimer = null;
13737
+ }
13738
+ /**
13739
+ * La sesion crecio: la barra se entera aunque el watcher no avise, una vez
13740
+ * cada `GROWTH_REFRESH_MS` como mucho y con la ruta de ese momento. Solo con
13741
+ * una CLI que se relee sola y un seguidor que dice su archivo: con Claude Code
13742
+ * el watcher ya avisa.
13743
+ */
13744
+ noteGrowth(terminalId, entry) {
13745
+ if ((entry.adapter.history.followPollMs ?? null) === null || entry.growthTimer !== null) return;
13746
+ if (typeof entry.follower.sourcePath !== "string") return;
13747
+ entry.growthTimer = setTimeout(() => {
13748
+ entry.growthTimer = null;
13749
+ if (this.entries.get(terminalId) !== entry) return;
13750
+ const filePath = entry.follower.sourcePath;
13751
+ if (typeof filePath === "string" && filePath.length > 0) this.emit("historyGrew", entry.agent, filePath);
13752
+ }, GROWTH_REFRESH_MS);
13753
+ entry.growthTimer.unref();
12700
13754
  }
12701
13755
  /**
12702
13756
  * Recalcula `openToolCall` y avisa si cambio.
@@ -12731,6 +13785,94 @@ var ConversationHub = class extends EventEmitter {
12731
13785
  getSubagents(terminalId) {
12732
13786
  return this.entries.get(terminalId)?.subagents ?? [];
12733
13787
  }
13788
+ /**
13789
+ * La cola de la CLI de una pestana, ahora (hito 45, §6.35): la del historial
13790
+ * si su seguidor la dice, o la que lleva la app. Vacia sin proceso, si la CLI
13791
+ * no hace cola, o si lleva libre `IDLE_CLEARS_QUEUE_MS`. La de una CLI cuyo
13792
+ * historial la dice se ve solo con alguien siguiendo la pestana: quien mira
13793
+ * la cola mira el hilo.
13794
+ */
13795
+ computeQueue(terminalId) {
13796
+ const descriptor = this.registry.get(terminalId);
13797
+ if (descriptor === null || descriptor.agent === null) return [];
13798
+ const support = this.agents.get(descriptor.agent)?.adapter.promptQueue;
13799
+ const launchedAt = this.registry.launchedAtOf(terminalId);
13800
+ if (support === void 0 || launchedAt === null || this.idleSettled.has(terminalId)) return [];
13801
+ if (support.source === "app") return this.appQueue.list(terminalId);
13802
+ const follower = this.entries.get(terminalId)?.follower;
13803
+ return follower?.queuedPrompts === void 0 ? [] : [...follower.queuedPrompts(launchedAt)];
13804
+ }
13805
+ /** La cola si cambio desde el ultimo aviso, que pasa a ser esta; null si es la misma. */
13806
+ takeQueueChange(terminalId) {
13807
+ const queue = this.computeQueue(terminalId);
13808
+ const key = JSON.stringify(queue);
13809
+ if ((this.announcedQueues.get(terminalId) ?? "[]") === key) return null;
13810
+ if (queue.length === 0) this.announcedQueues.delete(terminalId);
13811
+ else this.announcedQueues.set(terminalId, key);
13812
+ return queue;
13813
+ }
13814
+ /** Recalcula la cola y avisa si cambio. */
13815
+ refreshQueue(terminalId) {
13816
+ const queue = this.takeQueueChange(terminalId);
13817
+ if (queue !== null) this.emit("queue", terminalId, queue);
13818
+ }
13819
+ /**
13820
+ * La cola de una pestana, para un `conversation.reset`: va dentro, y desde
13821
+ * ahi es lo avisado.
13822
+ */
13823
+ getQueue(terminalId) {
13824
+ this.takeQueueChange(terminalId);
13825
+ return this.computeQueue(terminalId);
13826
+ }
13827
+ /** true si la CLI de la pestana tiene algo en cola. Lo pregunta el socket antes de forzar el envio. */
13828
+ hasQueue(terminalId) {
13829
+ return this.computeQueue(terminalId).length > 0;
13830
+ }
13831
+ /**
13832
+ * La app termino de escribirle un mensaje a una CLI cuya cola no queda
13833
+ * escrita (`source: 'app'`), con el agente trabajando: queda en cola hasta
13834
+ * que aparezca en el hilo.
13835
+ */
13836
+ noteQueued(terminalId, input) {
13837
+ this.appQueue.note(terminalId, input);
13838
+ this.refreshQueue(terminalId);
13839
+ }
13840
+ /** Lo que la app llevaba en cola ya no esta ahi: Detener se lo devolvio al cuadro. */
13841
+ clearQueued(terminalId) {
13842
+ if (this.appQueue.clear(terminalId)) this.refreshQueue(terminalId);
13843
+ }
13844
+ /** Se forzo el envio: lo que la app llevaba en cola ya se le esta pasando al agente. */
13845
+ markQueueForced(terminalId) {
13846
+ if (this.appQueue.markForced(terminalId)) this.refreshQueue(terminalId);
13847
+ }
13848
+ /**
13849
+ * Cambio la actividad de una pestana. Libre un rato, no queda nada en cola;
13850
+ * sin proceso, tampoco. Otra cosa (trabajando, esperando) cancela la cuenta.
13851
+ */
13852
+ noteActivity(terminalId, activity) {
13853
+ if (this.lastActivity.get(terminalId) === activity) return;
13854
+ this.lastActivity.set(terminalId, activity);
13855
+ const timer = this.idleTimers.get(terminalId);
13856
+ if (timer !== void 0) clearTimeout(timer);
13857
+ this.idleTimers.delete(terminalId);
13858
+ const wasSettled = this.idleSettled.delete(terminalId);
13859
+ if (activity === "offline") {
13860
+ this.appQueue.clear(terminalId);
13861
+ this.refreshQueue(terminalId);
13862
+ return;
13863
+ }
13864
+ if (activity === "idle") {
13865
+ const idleTimer = setTimeout(() => {
13866
+ this.idleTimers.delete(terminalId);
13867
+ this.idleSettled.add(terminalId);
13868
+ this.appQueue.clear(terminalId);
13869
+ this.refreshQueue(terminalId);
13870
+ }, IDLE_CLEARS_QUEUE_MS);
13871
+ idleTimer.unref();
13872
+ this.idleTimers.set(terminalId, idleTimer);
13873
+ }
13874
+ if (wasSettled) this.refreshQueue(terminalId);
13875
+ }
12734
13876
  /**
12735
13877
  * La pestana cambio de sesion: se suelta el seguidor viejo y se sigue la
12736
13878
  * nueva, sin perder a quien estaba mirando.
@@ -12768,7 +13910,7 @@ var ConversationHub = class extends EventEmitter {
12768
13910
  this.startFollowTicker(terminalId, next);
12769
13911
  debugLog(
12770
13912
  "conversation",
12771
- `${terminalId.slice(0, 8)} changed session: following ${path38.basename(next.follower.label)}`
13913
+ `${terminalId.slice(0, 8)} changed session: following ${path39.basename(next.follower.label)}`
12772
13914
  );
12773
13915
  await next.follower.start();
12774
13916
  next.primed = true;
@@ -12863,10 +14005,70 @@ var ConversationHub = class extends EventEmitter {
12863
14005
  }
12864
14006
  /** La pestana se cerro: se suelta sin importar cuantos la miraban. */
12865
14007
  drop(terminalId) {
14008
+ this.withdrawals.delete(terminalId);
14009
+ this.appQueue.clear(terminalId);
14010
+ this.announcedQueues.delete(terminalId);
14011
+ this.lastActivity.delete(terminalId);
14012
+ this.idleSettled.delete(terminalId);
14013
+ const idleTimer = this.idleTimers.get(terminalId);
14014
+ if (idleTimer !== void 0) clearTimeout(idleTimer);
14015
+ this.idleTimers.delete(terminalId);
12866
14016
  const entry = this.entries.get(terminalId);
12867
14017
  if (entry === void 0) return;
12868
14018
  this.release(terminalId, entry);
12869
14019
  }
14020
+ /**
14021
+ * Esconde del hilo desde ese mensaje: la CLI lo deshizo y todavia no escribio
14022
+ * nada que lo diga (hito 44, §4.16). Quien mira recibe un `reset`. Se anota
14023
+ * aunque nadie mire: se aplica al suscribirse. false si la pestana no esta.
14024
+ */
14025
+ withdraw(terminalId, eventId) {
14026
+ const descriptor = this.registry.get(terminalId);
14027
+ if (descriptor === null) return false;
14028
+ this.withdrawals.set(terminalId, {
14029
+ sessionId: descriptor.sessionId,
14030
+ eventId,
14031
+ applied: false,
14032
+ until: Date.now() + WITHDRAWAL_PENDING_MS
14033
+ });
14034
+ const entry = this.entries.get(terminalId);
14035
+ if (entry === void 0) return true;
14036
+ if (!this.applyWithdrawal(terminalId, entry, true)) this.schedulePoll(terminalId, entry);
14037
+ return true;
14038
+ }
14039
+ /**
14040
+ * Esconde lo anotado en un seguidor (recien creado, o el de siempre). true si
14041
+ * quedo escondido. Si nunca se pudo, se sigue esperando a que el seguidor lea
14042
+ * el mensaje, con un tope; si ya se habia podido y ahora no, la CLI escribio
14043
+ * otra cosa (otra rama, u otra sesion) y se olvida.
14044
+ */
14045
+ applyWithdrawal(terminalId, entry, announce = false) {
14046
+ const withdrawal = this.withdrawals.get(terminalId);
14047
+ if (withdrawal === void 0) return false;
14048
+ if (withdrawal.sessionId !== entry.sessionId || entry.follower.withdrawFrom === void 0) {
14049
+ this.withdrawals.delete(terminalId);
14050
+ return false;
14051
+ }
14052
+ const changed = entry.follower.withdrawFrom(withdrawal.eventId);
14053
+ if (entry.follower.withdrawnFrom?.() === withdrawal.eventId) {
14054
+ withdrawal.applied = true;
14055
+ if (changed && announce) this.emit("reset", terminalId);
14056
+ return true;
14057
+ }
14058
+ if (withdrawal.applied || Date.now() > withdrawal.until) this.withdrawals.delete(terminalId);
14059
+ return false;
14060
+ }
14061
+ /**
14062
+ * Lo que el menu de rewind necesita del mensaje `eventId` (hito 44): su
14063
+ * texto, su primera linea y cuantos iguales hay despues. Sale de lo que se
14064
+ * ve, que es lo que la CLI lista. null si nadie mira la conversacion o el
14065
+ * mensaje no es uno que escribio el usuario.
14066
+ */
14067
+ rewindTarget(terminalId, eventId) {
14068
+ const entry = this.entries.get(terminalId);
14069
+ if (entry === void 0) return null;
14070
+ return rewindTargetFrom(entry.follower.getTail(Number.MAX_SAFE_INTEGER).events, eventId);
14071
+ }
12870
14072
  getPage(terminalId, beforeEventId, limit) {
12871
14073
  const entry = this.entries.get(terminalId);
12872
14074
  if (entry === void 0) return null;
@@ -13041,6 +14243,8 @@ var ConversationHub = class extends EventEmitter {
13041
14243
  entry.stopWatchingStatus?.();
13042
14244
  }
13043
14245
  this.entries.clear();
14246
+ for (const timer of this.idleTimers.values()) clearTimeout(timer);
14247
+ this.idleTimers.clear();
13044
14248
  }
13045
14249
  schedulePoll(terminalId, entry) {
13046
14250
  if (entry.timer !== null) clearTimeout(entry.timer);
@@ -13061,15 +14265,29 @@ var ConversationHub = class extends EventEmitter {
13061
14265
  entry.polling = entry.polling.then(async () => {
13062
14266
  if (this.entries.get(terminalId) !== entry) return;
13063
14267
  let result;
14268
+ const withdrawnBefore = entry.follower.withdrawnFrom?.() ?? null;
13064
14269
  try {
13065
14270
  result = await entry.follower.poll();
13066
14271
  } catch (error) {
13067
14272
  console.warn(`[conversation] couldn't read ${entry.follower.label}:`, error);
13068
14273
  return;
13069
14274
  }
14275
+ if (withdrawnBefore !== null && entry.follower.withdrawnFrom?.() !== withdrawnBefore) {
14276
+ if (this.withdrawals.get(terminalId)?.eventId === withdrawnBefore) this.withdrawals.delete(terminalId);
14277
+ }
13070
14278
  this.refreshOpenToolCall(terminalId, entry);
13071
14279
  this.refreshSubagents(terminalId, entry);
14280
+ if (entry.adapter.promptQueue?.source === "app") {
14281
+ this.appQueue.settle(terminalId, entry.follower.getTail(QUEUE_SETTLE_LOOKBACK).events);
14282
+ }
13072
14283
  if (options.silent) return;
14284
+ const queue = this.takeQueueChange(terminalId);
14285
+ if (result.reset || result.added.length > 0) this.noteGrowth(terminalId, entry);
14286
+ const pending = this.withdrawals.get(terminalId);
14287
+ if (pending !== void 0 && !pending.applied && this.applyWithdrawal(terminalId, entry)) {
14288
+ this.emit("reset", terminalId);
14289
+ return;
14290
+ }
13073
14291
  const observed = entry.follower.getPermissionMode();
13074
14292
  if (observed !== null) {
13075
14293
  entry.observedMode = observed;
@@ -13081,7 +14299,9 @@ var ConversationHub = class extends EventEmitter {
13081
14299
  return;
13082
14300
  }
13083
14301
  if (result.added.length > 0 || result.usageChanged === true) {
13084
- this.emit("append", terminalId, result.added, entry.follower.getUsage(), mode);
14302
+ this.emit("append", terminalId, result.added, entry.follower.getUsage(), mode, queue ?? void 0);
14303
+ } else if (queue !== null) {
14304
+ this.emit("queue", terminalId, queue);
13085
14305
  }
13086
14306
  if (result.turns.length > 0) this.emit("turns", terminalId, result.turns);
13087
14307
  const plans = entry.adapter.history.plans;
@@ -13132,18 +14352,18 @@ function openBrowser(url) {
13132
14352
  // packages/server/src/pty-helper.ts
13133
14353
  import { chmodSync, statSync } from "node:fs";
13134
14354
  import { createRequire } from "node:module";
13135
- import path39 from "node:path";
14355
+ import path40 from "node:path";
13136
14356
  function helperCandidates(packageDir, platform, arch) {
13137
14357
  return [
13138
- path39.join(packageDir, "build", "Release", "spawn-helper"),
13139
- path39.join(packageDir, "build", "Debug", "spawn-helper"),
13140
- path39.join(packageDir, "prebuilds", `${platform}-${arch}`, "spawn-helper")
14358
+ path40.join(packageDir, "build", "Release", "spawn-helper"),
14359
+ path40.join(packageDir, "build", "Debug", "spawn-helper"),
14360
+ path40.join(packageDir, "prebuilds", `${platform}-${arch}`, "spawn-helper")
13141
14361
  ];
13142
14362
  }
13143
14363
  function locateNodePty() {
13144
14364
  try {
13145
14365
  const require2 = createRequire(import.meta.url);
13146
- return path39.resolve(path39.dirname(require2.resolve("node-pty")), "..");
14366
+ return path40.resolve(path40.dirname(require2.resolve("node-pty")), "..");
13147
14367
  } catch {
13148
14368
  return null;
13149
14369
  }
@@ -13804,24 +15024,24 @@ var RemoteAccessService = class {
13804
15024
  };
13805
15025
 
13806
15026
  // packages/server/src/remote-devices-store.ts
13807
- import { chmod, mkdir as mkdir6, readFile as readFile12, stat as stat23 } from "node:fs/promises";
13808
- import path41 from "node:path";
15027
+ import { chmod, mkdir as mkdir6, readFile as readFile13, stat as stat25 } from "node:fs/promises";
15028
+ import path42 from "node:path";
13809
15029
 
13810
15030
  // packages/server/src/vault/write.ts
13811
15031
  import { randomBytes as randomBytes2 } from "node:crypto";
13812
- import { mkdir as mkdir5, rename as rename3, rm as rm2, stat as stat22, writeFile as writeFile3 } from "node:fs/promises";
15032
+ import { mkdir as mkdir5, rename as rename3, rm as rm2, stat as stat24, writeFile as writeFile3 } from "node:fs/promises";
13813
15033
 
13814
15034
  // packages/server/src/vault/paths.ts
13815
15035
  import { createHash as createHash2 } from "node:crypto";
13816
- import path40 from "node:path";
15036
+ import path41 from "node:path";
13817
15037
  function defaultVaultDir() {
13818
- return path40.join(appConfigDir(), "vault");
15038
+ return path41.join(appConfigDir(), "vault");
13819
15039
  }
13820
15040
  function vaultMarkerFile(dir) {
13821
- return path40.join(dir, "vault.json");
15041
+ return path41.join(dir, "vault.json");
13822
15042
  }
13823
15043
  function sessionsRoot2(dir) {
13824
- return path40.join(dir, "sessions");
15044
+ return path41.join(dir, "sessions");
13825
15045
  }
13826
15046
  function isSafeSessionId(sessionId) {
13827
15047
  return isVaultSessionId(sessionId);
@@ -13835,41 +15055,41 @@ function assertSessionKey(agent, sessionId) {
13835
15055
  }
13836
15056
  function agentSessionsDir(dir, agent) {
13837
15057
  if (!isSessionAgentId(agent)) throw new Error(`Unknown source for the local copy: ${JSON.stringify(agent)}`);
13838
- return path40.join(sessionsRoot2(dir), agent);
15058
+ return path41.join(sessionsRoot2(dir), agent);
13839
15059
  }
13840
15060
  function sessionFile(dir, agent, sessionId) {
13841
15061
  assertSessionKey(agent, sessionId);
13842
- return path40.join(sessionsRoot2(dir), agent, `${sessionId}.jsonl`);
15062
+ return path41.join(sessionsRoot2(dir), agent, `${sessionId}.jsonl`);
13843
15063
  }
13844
15064
  function assetsDir(dir, agent, sessionId) {
13845
15065
  assertSessionKey(agent, sessionId);
13846
- return path40.join(sessionsRoot2(dir), agent, `${sessionId}.assets`);
15066
+ return path41.join(sessionsRoot2(dir), agent, `${sessionId}.assets`);
13847
15067
  }
13848
15068
  function assetFile(dir, agent, sessionId, asset) {
13849
15069
  if (!VAULT_ASSET_NAME_PATTERN.test(asset)) throw new Error(`Invalid asset name: ${JSON.stringify(asset)}`);
13850
- return path40.join(assetsDir(dir, agent, sessionId), asset);
15070
+ return path41.join(assetsDir(dir, agent, sessionId), asset);
13851
15071
  }
13852
15072
  function projectFolderName2(cwd, platform) {
13853
15073
  const hash = createHash2("sha256").update(normalizeCwdKey(cwd, platform)).digest("hex").slice(0, 8);
13854
- const base = (platform === "win32" ? path40.win32 : path40.posix).basename(cwd);
15074
+ const base = (platform === "win32" ? path41.win32 : path41.posix).basename(cwd);
13855
15075
  const name = base.length === 0 ? "proyecto" : sanitizeFileName(base, 60);
13856
15076
  return `${name}-${hash}`;
13857
15077
  }
13858
15078
  function memoryDir(dir, cwd, platform) {
13859
- return path40.join(dir, "memory", projectFolderName2(cwd, platform));
15079
+ return path41.join(dir, "memory", projectFolderName2(cwd, platform));
13860
15080
  }
13861
15081
  function projectExportDir(dir, cwd, platform) {
13862
- return path40.join(dir, "export", projectFolderName2(cwd, platform));
15082
+ return path41.join(dir, "export", projectFolderName2(cwd, platform));
13863
15083
  }
13864
15084
  function projectExportDirFor(dir, project, platform) {
13865
15085
  if (project.cwd.length > 0) return projectExportDir(dir, project.cwd, platform);
13866
15086
  const hash = createHash2("sha256").update(project.key).digest("hex").slice(0, 8);
13867
15087
  const name = project.fallbackName.length > 0 ? sanitizeFileName(project.fallbackName, 60) : "proyecto";
13868
- return path40.join(dir, "export", `${name}-${hash}`);
15088
+ return path41.join(dir, "export", `${name}-${hash}`);
13869
15089
  }
13870
15090
  function sessionExportFile(dir, agent, sessionId) {
13871
15091
  assertSessionKey(agent, sessionId);
13872
- return path40.join(dir, "export", "sesiones", `${agent}-${sessionId}.md`);
15092
+ return path41.join(dir, "export", "sesiones", `${agent}-${sessionId}.md`);
13873
15093
  }
13874
15094
  var RESERVED_WINDOWS_NAME = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
13875
15095
  function sanitizeFileName(text, max = 80) {
@@ -13924,7 +15144,7 @@ async function writeFileAtomic(target, data, deps = DEFAULT_WRITE_DEPS) {
13924
15144
  async function ensureVaultMarker(dir, deps = DEFAULT_WRITE_DEPS) {
13925
15145
  const marker = vaultMarkerFile(dir);
13926
15146
  try {
13927
- await stat22(marker);
15147
+ await stat24(marker);
13928
15148
  return;
13929
15149
  } catch {
13930
15150
  }
@@ -13934,7 +15154,7 @@ async function ensureVaultMarker(dir, deps = DEFAULT_WRITE_DEPS) {
13934
15154
  }
13935
15155
  async function assetPresent(file, size) {
13936
15156
  try {
13937
- const info = await stat22(file);
15157
+ const info = await stat24(file);
13938
15158
  return info.isFile() && info.size === size;
13939
15159
  } catch {
13940
15160
  return false;
@@ -13976,7 +15196,7 @@ var RemoteDevicesStore = class {
13976
15196
  async load() {
13977
15197
  let raw;
13978
15198
  try {
13979
- raw = await readFile12(this.filePath, "utf8");
15199
+ raw = await readFile13(this.filePath, "utf8");
13980
15200
  } catch {
13981
15201
  return [];
13982
15202
  }
@@ -13997,7 +15217,7 @@ var RemoteDevicesStore = class {
13997
15217
  save(devices) {
13998
15218
  const text = serializeRemoteDevices(devices);
13999
15219
  const run = async () => {
14000
- await mkdir6(path41.dirname(this.filePath), { recursive: true });
15220
+ await mkdir6(path42.dirname(this.filePath), { recursive: true });
14001
15221
  await writeFileAtomic(this.filePath, text);
14002
15222
  await chmod(this.filePath, 384).catch(() => void 0);
14003
15223
  };
@@ -14016,7 +15236,7 @@ var RemoteDevicesStore = class {
14016
15236
  */
14017
15237
  async stamp() {
14018
15238
  try {
14019
- const info = await stat23(this.filePath);
15239
+ const info = await stat25(this.filePath);
14020
15240
  return `${info.mtimeMs}:${info.size}`;
14021
15241
  } catch {
14022
15242
  return "";
@@ -14130,8 +15350,8 @@ import chokidar from "chokidar";
14130
15350
 
14131
15351
  // packages/server/src/git-repo.ts
14132
15352
  import { execFile as execFile4 } from "node:child_process";
14133
- import { readFile as readFile13 } from "node:fs/promises";
14134
- import path42 from "node:path";
15353
+ import { readFile as readFile14 } from "node:fs/promises";
15354
+ import path43 from "node:path";
14135
15355
  function gitOutputText(output, command) {
14136
15356
  return output.length > 0 ? serverText("raw", { text: output }) : serverText("gitCommandFailed", { command });
14137
15357
  }
@@ -14192,7 +15412,7 @@ async function resolveRepo(cwd) {
14192
15412
  const result = await runGit(cwd, ["rev-parse", "--show-toplevel"]);
14193
15413
  if (result.code === 0) {
14194
15414
  const root = result.stdout.trim();
14195
- if (root.length > 0) return { state: "ready", root: path42.resolve(root), message: null };
15415
+ if (root.length > 0) return { state: "ready", root: path43.resolve(root), message: null };
14196
15416
  }
14197
15417
  const stderr = result.stderr.trim();
14198
15418
  if (/not a git repository|no es un repositorio/i.test(stderr)) {
@@ -14219,7 +15439,7 @@ async function findGitDir(cwd) {
14219
15439
  const result = await runGit(cwd, ["rev-parse", "--absolute-git-dir"]);
14220
15440
  if (result.code !== 0) return null;
14221
15441
  const dir = result.stdout.trim();
14222
- return dir.length > 0 ? path42.resolve(dir) : null;
15442
+ return dir.length > 0 ? path43.resolve(dir) : null;
14223
15443
  }
14224
15444
  function letterToKind(letter) {
14225
15445
  switch (letter) {
@@ -14322,7 +15542,7 @@ function parseWorktrees(raw, currentRoot) {
14322
15542
  worktrees.push({
14323
15543
  path: current.path,
14324
15544
  branch: current.branch,
14325
- isCurrent: path42.resolve(current.path) === path42.resolve(currentRoot)
15545
+ isCurrent: path43.resolve(current.path) === path43.resolve(currentRoot)
14326
15546
  });
14327
15547
  current = null;
14328
15548
  };
@@ -14465,7 +15685,7 @@ function looksBinary(buffer) {
14465
15685
  async function readUntrackedAsDiff(absolutePath, relativePath) {
14466
15686
  let content;
14467
15687
  try {
14468
- content = await readFile13(absolutePath);
15688
+ content = await readFile14(absolutePath);
14469
15689
  } catch {
14470
15690
  return {
14471
15691
  path: relativePath,
@@ -14513,7 +15733,7 @@ async function readUntrackedAsDiff(absolutePath, relativePath) {
14513
15733
  async function readDiff(options) {
14514
15734
  const { repoRoot: repoRoot2, relativePath, staged, untracked } = options;
14515
15735
  if (untracked && !staged) {
14516
- return readUntrackedAsDiff(path42.join(repoRoot2, relativePath), relativePath);
15736
+ return readUntrackedAsDiff(path43.join(repoRoot2, relativePath), relativePath);
14517
15737
  }
14518
15738
  const args = [
14519
15739
  "diff",
@@ -14733,33 +15953,74 @@ var RepoHub = class extends EventEmitter2 {
14733
15953
  // packages/server/src/memory-hub.ts
14734
15954
  import { EventEmitter as EventEmitter3 } from "node:events";
14735
15955
  import { homedir as homedir12 } from "node:os";
14736
- import path44 from "node:path";
15956
+ import path46 from "node:path";
14737
15957
  import chokidar2 from "chokidar";
14738
15958
 
14739
15959
  // packages/server/src/memory-bridge.ts
14740
- import { execFile as execFile5 } from "node:child_process";
14741
- import { randomBytes as randomBytes4 } from "node:crypto";
14742
- import {
14743
- mkdir as mkdir7,
14744
- open as open7,
14745
- readdir as readdir11,
14746
- readFile as readFile14,
14747
- realpath as realpath3,
14748
- rename as rename4,
14749
- stat as stat24,
14750
- unlink as unlink3
14751
- } from "node:fs/promises";
14752
- import { homedir as homedir11 } from "node:os";
14753
- import path43 from "node:path";
15960
+ import { execFile as execFile5 } from "node:child_process";
15961
+ import { mkdir as mkdir7, open as open7, readdir as readdir11, readFile as readFile15, realpath as realpath3, stat as stat26 } from "node:fs/promises";
15962
+ import { homedir as homedir11 } from "node:os";
15963
+ import path45 from "node:path";
15964
+
15965
+ // packages/server/src/atomic-write.ts
15966
+ import { randomBytes as randomBytes4 } from "node:crypto";
15967
+ import { open as open6, rename as rename4, unlink as unlink3 } from "node:fs/promises";
15968
+ import path44 from "node:path";
15969
+ var RENAME_ATTEMPTS = 5;
15970
+ var RENAME_BACKOFF_MS = 40;
15971
+ var TEMPORARY_ATTEMPTS = 5;
15972
+ async function writeAtomically(target, content, options = {}) {
15973
+ const { temporary, handle } = await createTemporary(target, options.mode === void 0 ? 438 : 384);
15974
+ try {
15975
+ try {
15976
+ await handle.writeFile(content);
15977
+ if (options.mode !== void 0 && process.platform !== "win32") await handle.chmod(options.mode & 4095);
15978
+ } finally {
15979
+ await handle.close();
15980
+ }
15981
+ await options.beforeRename?.();
15982
+ await renameWithRetry(temporary, target);
15983
+ } catch (error) {
15984
+ await unlink3(temporary).catch(() => void 0);
15985
+ throw error;
15986
+ }
15987
+ }
15988
+ async function createTemporary(target, mode) {
15989
+ for (let attempt = 1; ; attempt += 1) {
15990
+ const suffix = randomBytes4(6).toString("hex");
15991
+ const temporary = path44.join(
15992
+ path44.dirname(target),
15993
+ `.${path44.basename(target)}.${process.pid}.${suffix}.tmp`
15994
+ );
15995
+ try {
15996
+ return { temporary, handle: await open6(temporary, "wx", mode) };
15997
+ } catch (error) {
15998
+ const code = error.code;
15999
+ if (code !== "EEXIST" || attempt >= TEMPORARY_ATTEMPTS) throw error;
16000
+ }
16001
+ }
16002
+ }
16003
+ async function renameWithRetry(from, to) {
16004
+ for (let attempt = 1; ; attempt += 1) {
16005
+ try {
16006
+ await rename4(from, to);
16007
+ return;
16008
+ } catch (error) {
16009
+ const code = error.code;
16010
+ const transient = code === "EPERM" || code === "EACCES" || code === "EBUSY";
16011
+ if (!transient || attempt >= RENAME_ATTEMPTS) throw error;
16012
+ await new Promise((resolve) => setTimeout(resolve, RENAME_BACKOFF_MS * attempt));
16013
+ }
16014
+ }
16015
+ }
16016
+
16017
+ // packages/server/src/memory-bridge.ts
14754
16018
  var MemoryBridgeError = class extends ServerTextError {
14755
16019
  };
14756
16020
  var MAX_LISTED_NOTES = 500;
14757
16021
  var MAX_NOTE_BYTES = 256 * 1024;
14758
16022
  var NOTE_HEAD_BYTES = 16 * 1024;
14759
16023
  var GIT_TIMEOUT_MS2 = 15e3;
14760
- var RENAME_ATTEMPTS = 5;
14761
- var RENAME_BACKOFF_MS = 40;
14762
- var TEMPORARY_ATTEMPTS = 5;
14763
16024
  var INDEX_REL = `${MEMORY_DIR}/${MEMORY_INDEX_FILE}`;
14764
16025
  var AGENTS_DIR_REL = MEMORY_DIR.split("/")[0] ?? ".agents";
14765
16026
  var GITIGNORE_REL = ".gitignore";
@@ -14768,9 +16029,9 @@ var GITIGNORE_MEMORY_LINE = `${MEMORY_DIR}/`;
14768
16029
  var BOM2 = Buffer.from([239, 187, 191]);
14769
16030
  async function inspectMemory(cwd, home = homedir11()) {
14770
16031
  await assertMemoryDirInside(cwd);
14771
- const folder = path43.join(cwd, MEMORY_DIR);
16032
+ const folder = path45.join(cwd, MEMORY_DIR);
14772
16033
  const folderExists = await isDirectory2(folder);
14773
- const indexExists = folderExists && await isFile(path43.join(folder, MEMORY_INDEX_FILE));
16034
+ const indexExists = folderExists && await isFile(path45.join(folder, MEMORY_INDEX_FILE));
14774
16035
  const noteNames = folderExists ? await listNoteFiles(folder) : [];
14775
16036
  const notes = await describeNotes(folder, noteNames);
14776
16037
  const files = await Promise.all(
@@ -14802,7 +16063,7 @@ async function installMemory(cwd, options, home = homedir11()) {
14802
16063
  }
14803
16064
  async function importNativeMemory(cwd, home = homedir11()) {
14804
16065
  await assertMemoryDirInside(cwd);
14805
- if (!await isDirectory2(path43.join(cwd, MEMORY_DIR))) {
16066
+ if (!await isDirectory2(path45.join(cwd, MEMORY_DIR))) {
14806
16067
  throw new MemoryBridgeError(serverText("memoryNotInstalled"));
14807
16068
  }
14808
16069
  const plan = new PlanBuilder(cwd);
@@ -14820,9 +16081,9 @@ async function readMemoryNote(cwd, name) {
14820
16081
  }
14821
16082
  const absolute = await resolveInside(cwd, `${MEMORY_DIR}/${name}`);
14822
16083
  try {
14823
- const info = await stat24(absolute);
16084
+ const info = await stat26(absolute);
14824
16085
  if (!info.isFile()) return null;
14825
- const content = await readFile14(absolute);
16086
+ const content = await readFile15(absolute);
14826
16087
  return {
14827
16088
  name,
14828
16089
  text: content.subarray(0, MAX_NOTE_BYTES).toString("utf8"),
@@ -14837,7 +16098,7 @@ function isNoteName(name) {
14837
16098
  if (name.length === 0 || name.length > 255) return false;
14838
16099
  if (name.includes("\0")) return false;
14839
16100
  if (name === "." || name === "..") return false;
14840
- if (path43.basename(name) !== name) return false;
16101
+ if (path45.basename(name) !== name) return false;
14841
16102
  if (/[\\/:*?"<>|\u0000-\u001f]/.test(name)) return false;
14842
16103
  return name.toLowerCase().endsWith(".md");
14843
16104
  }
@@ -14874,7 +16135,7 @@ var PlanBuilder = class {
14874
16135
  /** Nombres de nota que habria en la carpeta, en minusculas. */
14875
16136
  async noteNames() {
14876
16137
  const names = /* @__PURE__ */ new Map();
14877
- for (const name of await listNoteFiles(path43.join(this.cwd, MEMORY_DIR))) {
16138
+ for (const name of await listNoteFiles(path45.join(this.cwd, MEMORY_DIR))) {
14878
16139
  names.set(name.toLowerCase(), name);
14879
16140
  }
14880
16141
  for (const rel of this.files.keys()) {
@@ -14917,7 +16178,7 @@ var PlanBuilder = class {
14917
16178
  continue;
14918
16179
  }
14919
16180
  const target = await realpath3(absolute);
14920
- const info = await stat24(target);
16181
+ const info = await stat26(target);
14921
16182
  if (info.nlink > 1) {
14922
16183
  throw new MemoryBridgeError(serverText("memoryHardLinks", { file: file.rel }));
14923
16184
  }
@@ -14936,7 +16197,7 @@ var PlanBuilder = class {
14936
16197
  );
14937
16198
  if (pending.length === 0) return;
14938
16199
  if (pending.some((file) => file.rel.startsWith(`${MEMORY_DIR}/`))) {
14939
- await mkdir7(path43.join(this.cwd, MEMORY_DIR), { recursive: true });
16200
+ await mkdir7(path45.join(this.cwd, MEMORY_DIR), { recursive: true });
14940
16201
  }
14941
16202
  for (const file of pending) {
14942
16203
  if (file.target === null) throw new Error(`Unvalidated plan: ${file.rel}`);
@@ -15047,7 +16308,7 @@ async function planNativeImport(plan, cwd, home) {
15047
16308
  const local = await plan.noteNames();
15048
16309
  for (const name of await listNoteFiles(folder)) {
15049
16310
  const key = name.toLowerCase();
15050
- const content = await readIfFile(path43.join(folder, name), name);
16311
+ const content = await readIfFile(path45.join(folder, name), name);
15051
16312
  if (content === null) continue;
15052
16313
  const already = chosen.get(key);
15053
16314
  if (already !== void 0) {
@@ -15061,7 +16322,7 @@ async function planNativeImport(plan, cwd, home) {
15061
16322
  if (existing !== null && !existing.equals(content)) skipped.push(name);
15062
16323
  continue;
15063
16324
  }
15064
- plan.put(`${MEMORY_DIR}/${name}`, null, content, "copy", "", path43.join(folder, name));
16325
+ plan.put(`${MEMORY_DIR}/${name}`, null, content, "copy", "", path45.join(folder, name));
15065
16326
  copied.push(name);
15066
16327
  }
15067
16328
  }
@@ -15071,7 +16332,7 @@ async function planNativeImport(plan, cwd, home) {
15071
16332
  const known = new Set(linkTargets(index.toString("utf8")));
15072
16333
  const additions = [];
15073
16334
  for (const folder of folders) {
15074
- const nativeIndex = await readIfFile(path43.join(folder, MEMORY_INDEX_FILE), MEMORY_INDEX_FILE);
16335
+ const nativeIndex = await readIfFile(path45.join(folder, MEMORY_INDEX_FILE), MEMORY_INDEX_FILE);
15075
16336
  if (nativeIndex === null) continue;
15076
16337
  for (const line of nativeIndex.toString("utf8").replace(/^\uFEFF/, "").split(/\r?\n/)) {
15077
16338
  const target = linkTargets(line)[0];
@@ -15300,7 +16561,7 @@ async function describeNotes(folder, names) {
15300
16561
  const stated = [];
15301
16562
  for (const name of names) {
15302
16563
  try {
15303
- const info = await stat24(path43.join(folder, name));
16564
+ const info = await stat26(path45.join(folder, name));
15304
16565
  if (info.isFile()) stated.push({ name, sizeBytes: info.size, modifiedAt: info.mtimeMs });
15305
16566
  } catch {
15306
16567
  }
@@ -15308,7 +16569,7 @@ async function describeNotes(folder, names) {
15308
16569
  stated.sort((a, b) => b.modifiedAt - a.modifiedAt || a.name.localeCompare(b.name));
15309
16570
  const notes = [];
15310
16571
  for (const item of stated.slice(0, MAX_LISTED_NOTES)) {
15311
- const head = await readHead(path43.join(folder, item.name));
16572
+ const head = await readHead(path45.join(folder, item.name));
15312
16573
  notes.push({ ...item, ...describeNoteText(head, item.name) });
15313
16574
  }
15314
16575
  return notes;
@@ -15366,29 +16627,29 @@ async function countNativeMemory(cwd, home, sharedNames) {
15366
16627
  return { available: names.size, pending };
15367
16628
  }
15368
16629
  async function nativeMemoryFolders(cwd, home) {
15369
- const root = path43.join(home, ".claude", "projects");
16630
+ const root = path45.join(home, ".claude", "projects");
15370
16631
  const slug = projectSlugFor(cwd).toLowerCase();
15371
16632
  try {
15372
16633
  const entries = await readdir11(root, { withFileTypes: true });
15373
- return entries.filter((entry) => entry.isDirectory() && entry.name.toLowerCase() === slug).map((entry) => entry.name).sort().map((name) => path43.join(root, name, "memory"));
16634
+ return entries.filter((entry) => entry.isDirectory() && entry.name.toLowerCase() === slug).map((entry) => entry.name).sort().map((name) => path45.join(root, name, "memory"));
15374
16635
  } catch {
15375
16636
  return [];
15376
16637
  }
15377
16638
  }
15378
16639
  function buildGlobalFragments(home) {
15379
16640
  const windows = process.platform === "win32";
15380
- const globalFile = path43.join(home, ...GLOBAL_MEMORY_RELATIVE.split("/"));
16641
+ const globalFile = path45.join(home, ...GLOBAL_MEMORY_RELATIVE.split("/"));
15381
16642
  const hardLink = (link) => windows ? `mklink /H "${link}" "${globalFile}"` : `ln "${globalFile}" "${link}"`;
15382
16643
  const hardLinkNote = windows ? serverText("memoryNoteHardLinkCmd", { path: GLOBAL_MEMORY_RELATIVE }) : serverText("memoryNoteHardLink", { path: GLOBAL_MEMORY_RELATIVE });
15383
- const codexTarget = path43.join(home, ".codex", "AGENTS.md");
15384
- const geminiTarget = path43.join(home, ".gemini", "GEMINI.md");
15385
- const piTarget = path43.join(home, ".pi", "agent", "AGENTS.md");
16644
+ const codexTarget = path45.join(home, ".codex", "AGENTS.md");
16645
+ const geminiTarget = path45.join(home, ".gemini", "GEMINI.md");
16646
+ const piTarget = path45.join(home, ".pi", "agent", "AGENTS.md");
15386
16647
  const homeSlashes = home.replace(/\\/g, "/").replace(/\/+$/, "");
15387
16648
  return [
15388
16649
  {
15389
16650
  agent: "claude-code",
15390
16651
  label: MEMORY_AGENT_LABELS["claude-code"],
15391
- target: path43.join(home, ".claude", "CLAUDE.md"),
16652
+ target: path45.join(home, ".claude", "CLAUDE.md"),
15392
16653
  kind: "line",
15393
16654
  text: `@~/${GLOBAL_MEMORY_RELATIVE}`,
15394
16655
  note: serverText("memoryNoteClaude")
@@ -15412,7 +16673,7 @@ function buildGlobalFragments(home) {
15412
16673
  {
15413
16674
  agent: "opencode",
15414
16675
  label: MEMORY_AGENT_LABELS.opencode,
15415
- target: path43.join(home, ".config", "opencode", "opencode.json"),
16676
+ target: path45.join(home, ".config", "opencode", "opencode.json"),
15416
16677
  kind: "json",
15417
16678
  text: `"instructions": ["${homeSlashes}/${GLOBAL_MEMORY_RELATIVE}"]`,
15418
16679
  note: serverText("memoryNoteOpenCode")
@@ -15452,33 +16713,33 @@ function runGit2(cwd, args) {
15452
16713
  });
15453
16714
  }
15454
16715
  async function readGit(cwd) {
15455
- const fail = (message) => ({ state: { kind: "error", message }, main: null });
16716
+ const fail2 = (message) => ({ state: { kind: "error", message }, main: null });
15456
16717
  const result = await runGit2(cwd, ["rev-parse", "--show-toplevel", "--git-dir", "--git-common-dir"]);
15457
- if (result.kind === "spawn-failed") return fail(result.text);
16718
+ if (result.kind === "spawn-failed") return fail2(result.text);
15458
16719
  if (result.code !== 0) {
15459
16720
  const stderr = result.stderr.trim();
15460
16721
  if (/not a git repository|no es un repositorio/i.test(stderr)) {
15461
16722
  return { state: { kind: "not-repo" }, main: null };
15462
16723
  }
15463
- return fail(
16724
+ return fail2(
15464
16725
  stderr.length > 0 ? serverText("raw", { text: stderr.slice(0, 400) }) : serverText("gitCommandFailed", { command: "rev-parse" })
15465
16726
  );
15466
16727
  }
15467
16728
  const [toplevel, gitDir, commonDir] = result.stdout.split(/\r?\n/).map((line) => line.trim());
15468
- if (!toplevel || !gitDir || !commonDir) return fail(serverText("gitUnexpected", { command: "rev-parse" }));
16729
+ if (!toplevel || !gitDir || !commonDir) return fail2(serverText("gitUnexpected", { command: "rev-parse" }));
15469
16730
  const ignored = await checkIgnored(cwd, INDEX_REL);
15470
- if (typeof ignored !== "boolean") return fail(ignored);
15471
- const absoluteGitDir = path43.resolve(cwd, gitDir);
15472
- const absoluteCommonDir = path43.resolve(cwd, commonDir);
16731
+ if (typeof ignored !== "boolean") return fail2(ignored);
16732
+ const absoluteGitDir = path45.resolve(cwd, gitDir);
16733
+ const absoluteCommonDir = path45.resolve(cwd, commonDir);
15473
16734
  let worktree = null;
15474
16735
  let main2 = null;
15475
- if (!await samePath(absoluteGitDir, absoluteCommonDir)) {
15476
- if (path43.basename(absoluteCommonDir).toLowerCase() === ".git") {
15477
- const mainRoot = path43.dirname(absoluteCommonDir);
15478
- const inner = path43.relative(path43.resolve(toplevel), path43.resolve(cwd));
15479
- const sameFolder = inner.length === 0 || inner.startsWith("..") || path43.isAbsolute(inner);
15480
- const mainPath = sameFolder ? mainRoot : path43.join(mainRoot, inner);
15481
- const memoryRel = sameFolder ? MEMORY_DIR : [...inner.split(path43.sep), MEMORY_DIR].join("/");
16736
+ if (!await samePath2(absoluteGitDir, absoluteCommonDir)) {
16737
+ if (path45.basename(absoluteCommonDir).toLowerCase() === ".git") {
16738
+ const mainRoot = path45.dirname(absoluteCommonDir);
16739
+ const inner = path45.relative(path45.resolve(toplevel), path45.resolve(cwd));
16740
+ const sameFolder = inner.length === 0 || inner.startsWith("..") || path45.isAbsolute(inner);
16741
+ const mainPath = sameFolder ? mainRoot : path45.join(mainRoot, inner);
16742
+ const memoryRel = sameFolder ? MEMORY_DIR : [...inner.split(path45.sep), MEMORY_DIR].join("/");
15482
16743
  main2 = { root: mainRoot, memoryRel };
15483
16744
  worktree = {
15484
16745
  mainPath,
@@ -15498,12 +16759,12 @@ async function checkIgnored(cwd, rel) {
15498
16759
  const stderr = result.stderr.trim().slice(0, 400);
15499
16760
  return stderr.length > 0 ? serverText("raw", { text: stderr }) : serverText("gitCommandFailed", { command: "check-ignore" });
15500
16761
  }
15501
- async function samePath(a, b) {
16762
+ async function samePath2(a, b) {
15502
16763
  const resolve = async (value) => {
15503
16764
  try {
15504
16765
  return await realpath3(value);
15505
16766
  } catch {
15506
- return path43.resolve(value);
16767
+ return path45.resolve(value);
15507
16768
  }
15508
16769
  };
15509
16770
  const [left, right] = await Promise.all([resolve(a), resolve(b)]);
@@ -15530,8 +16791,8 @@ async function existingInside(root, rel, kind) {
15530
16791
  if (error instanceof InvalidPathError) return null;
15531
16792
  throw error;
15532
16793
  }
15533
- const matches = kind === "file" ? await isFile(absolute) : await isDirectory2(absolute);
15534
- return matches ? absolute : null;
16794
+ const matches2 = kind === "file" ? await isFile(absolute) : await isDirectory2(absolute);
16795
+ return matches2 ? absolute : null;
15535
16796
  }
15536
16797
  async function listNoteFiles(folder) {
15537
16798
  try {
@@ -15545,9 +16806,9 @@ async function listNoteFiles(folder) {
15545
16806
  }
15546
16807
  async function readIfFile(absolute, label) {
15547
16808
  try {
15548
- const info = await stat24(absolute);
16809
+ const info = await stat26(absolute);
15549
16810
  if (!info.isFile()) throw new MemoryBridgeError(serverText("memoryNotAFile", { label }));
15550
- return await readFile14(absolute);
16811
+ return await readFile15(absolute);
15551
16812
  } catch (error) {
15552
16813
  if (isNotFound(error)) return null;
15553
16814
  throw error;
@@ -15569,14 +16830,14 @@ async function readHead(absolute) {
15569
16830
  }
15570
16831
  async function isFile(absolute) {
15571
16832
  try {
15572
- return (await stat24(absolute)).isFile();
16833
+ return (await stat26(absolute)).isFile();
15573
16834
  } catch {
15574
16835
  return false;
15575
16836
  }
15576
16837
  }
15577
16838
  async function isDirectory2(absolute) {
15578
16839
  try {
15579
- return (await stat24(absolute)).isDirectory();
16840
+ return (await stat26(absolute)).isDirectory();
15580
16841
  } catch {
15581
16842
  return false;
15582
16843
  }
@@ -15586,58 +16847,22 @@ function isNotFound(error) {
15586
16847
  return code === "ENOENT" || code === "ENOTDIR";
15587
16848
  }
15588
16849
  async function writeAtomic(target, content, mustNotExist) {
15589
- const { temporary, handle } = await createTemporary(target);
15590
- try {
15591
- try {
15592
- await handle.writeFile(content);
15593
- } finally {
15594
- await handle.close();
15595
- }
15596
- if (mustNotExist && await exists2(target)) {
15597
- throw new MemoryBridgeError(serverText("memoryAppeared", { file: path43.basename(target) }));
15598
- }
15599
- await renameWithRetry(temporary, target);
15600
- } catch (error) {
15601
- await unlink3(temporary).catch(() => void 0);
15602
- throw error;
15603
- }
15604
- }
15605
- async function createTemporary(target) {
15606
- for (let attempt = 1; ; attempt += 1) {
15607
- const suffix = randomBytes4(6).toString("hex");
15608
- const temporary = path43.join(
15609
- path43.dirname(target),
15610
- `.${path43.basename(target)}.${process.pid}.${suffix}.tmp`
15611
- );
15612
- try {
15613
- return { temporary, handle: await open7(temporary, "wx") };
15614
- } catch (error) {
15615
- const code = error.code;
15616
- if (code !== "EEXIST" || attempt >= TEMPORARY_ATTEMPTS) throw error;
15617
- }
15618
- }
16850
+ await writeAtomically(target, content, {
16851
+ beforeRename: mustNotExist ? async () => {
16852
+ if (await exists2(target)) {
16853
+ throw new MemoryBridgeError(serverText("memoryAppeared", { file: path45.basename(target) }));
16854
+ }
16855
+ } : void 0
16856
+ });
15619
16857
  }
15620
16858
  async function exists2(absolute) {
15621
16859
  try {
15622
- await stat24(absolute);
16860
+ await stat26(absolute);
15623
16861
  return true;
15624
16862
  } catch {
15625
16863
  return false;
15626
16864
  }
15627
16865
  }
15628
- async function renameWithRetry(from, to) {
15629
- for (let attempt = 1; ; attempt += 1) {
15630
- try {
15631
- await rename4(from, to);
15632
- return;
15633
- } catch (error) {
15634
- const code = error.code;
15635
- const transient = code === "EPERM" || code === "EACCES" || code === "EBUSY";
15636
- if (!transient || attempt >= RENAME_ATTEMPTS) throw error;
15637
- await new Promise((resolve) => setTimeout(resolve, RENAME_BACKOFF_MS * attempt));
15638
- }
15639
- }
15640
- }
15641
16866
 
15642
16867
  // packages/server/src/memory-hub.ts
15643
16868
  var WATCH_DEBOUNCE_MS2 = 300;
@@ -15845,13 +17070,13 @@ var MemoryHub = class extends EventEmitter3 {
15845
17070
  }
15846
17071
  };
15847
17072
  function keyFor(cwd) {
15848
- const resolved = path44.resolve(cwd);
17073
+ const resolved = path46.resolve(cwd);
15849
17074
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
15850
17075
  }
15851
17076
  function isRelevantPath(cwd, candidate) {
15852
- const relative = path44.relative(cwd, candidate);
17077
+ const relative = path46.relative(cwd, candidate);
15853
17078
  if (relative.length === 0) return true;
15854
- if (relative.startsWith("..") || path44.isAbsolute(relative)) return false;
17079
+ if (relative.startsWith("..") || path46.isAbsolute(relative)) return false;
15855
17080
  const same = (a, b) => process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
15856
17081
  const parts = relative.split(/[\\/]/);
15857
17082
  const [first = "", second = "", third = ""] = parts;
@@ -15869,8 +17094,8 @@ function isRelevantPath(cwd, candidate) {
15869
17094
 
15870
17095
  // packages/server/src/paste-store.ts
15871
17096
  import { createHash as createHash3, randomUUID as randomUUID3 } from "node:crypto";
15872
- import { mkdir as mkdir8, readdir as readdir12, rm as rm3, stat as stat25, writeFile as writeFile4 } from "node:fs/promises";
15873
- import path45 from "node:path";
17097
+ import { mkdir as mkdir8, readdir as readdir12, rm as rm3, stat as stat27, writeFile as writeFile4 } from "node:fs/promises";
17098
+ import path47 from "node:path";
15874
17099
 
15875
17100
  // packages/server/src/vault/markdown.ts
15876
17101
  var pad2 = (value) => String(value).padStart(2, "0");
@@ -16385,8 +17610,8 @@ function buildContinuationMessage(input) {
16385
17610
  }
16386
17611
  return sanitizeForPaste(lines.join("\n"));
16387
17612
  }
16388
- function transcriptReferenceFor(path60, style) {
16389
- return style === "at-quoted" ? fileReference(path60, "at-quoted") : `"${path60}"`;
17613
+ function transcriptReferenceFor(path63, style) {
17614
+ return style === "at-quoted" ? fileReference(path63, "at-quoted") : `"${path63}"`;
16390
17615
  }
16391
17616
  function planTranscript(input) {
16392
17617
  const turns = splitTurns(input.events);
@@ -16562,11 +17787,11 @@ var PasteStore = class {
16562
17787
  if (signature === null) {
16563
17788
  throw new PasteImageError(serverText("imageUnknownFormat"));
16564
17789
  }
16565
- const directory = path45.join(this.root, safeSegment(terminalId));
17790
+ const directory = path47.join(this.root, safeSegment(terminalId));
16566
17791
  await mkdir8(directory, { recursive: true, mode: DIRECTORY_MODE });
16567
17792
  this.counter += 1;
16568
17793
  const name = `pegada-${this.counter}-${randomUUID3().slice(0, 8)}.${signature.ext}`;
16569
- const file = path45.join(directory, name);
17794
+ const file = path47.join(directory, name);
16570
17795
  await writeFile4(file, bytes, { mode: FILE_MODE });
16571
17796
  return { path: file, bytes: bytes.length };
16572
17797
  }
@@ -16584,11 +17809,11 @@ var PasteStore = class {
16584
17809
  if (bytes.length > MAX_TEXT_BYTES) {
16585
17810
  throw new PasteTextError(`The text is ${bytes.length} bytes; the maximum is ${MAX_TEXT_BYTES}.`);
16586
17811
  }
16587
- const directory = path45.join(this.root, safeSegment(terminalId));
17812
+ const directory = path47.join(this.root, safeSegment(terminalId));
16588
17813
  await mkdir8(directory, { recursive: true, mode: DIRECTORY_MODE });
16589
17814
  this.counter += 1;
16590
17815
  const name = `${baseName}-${this.counter}-${randomUUID3().slice(0, 8)}.md`;
16591
- const file = path45.join(directory, name);
17816
+ const file = path47.join(directory, name);
16592
17817
  await writeFile4(file, bytes, { mode: FILE_MODE });
16593
17818
  return { path: file, bytes: bytes.length };
16594
17819
  }
@@ -16618,17 +17843,17 @@ var PasteStore = class {
16618
17843
  })
16619
17844
  );
16620
17845
  }
16621
- const directory = path45.join(this.root, safeSegment(terminalId));
17846
+ const directory = path47.join(this.root, safeSegment(terminalId));
16622
17847
  await mkdir8(directory, { recursive: true, mode: DIRECTORY_MODE });
16623
17848
  this.counter += 1;
16624
17849
  const name = `adjunto-${this.counter}-${randomUUID3().slice(0, 8)}-${safe}`;
16625
- const file = path45.join(directory, name);
17850
+ const file = path47.join(directory, name);
16626
17851
  await writeFile4(file, bytes, { mode: FILE_MODE });
16627
17852
  return { path: file, bytes: bytes.length, name };
16628
17853
  }
16629
17854
  /** Borra lo de una pestana. Se llama al cerrarla. */
16630
17855
  async clearTerminal(terminalId) {
16631
- await rm3(path45.join(this.root, safeSegment(terminalId)), {
17856
+ await rm3(path47.join(this.root, safeSegment(terminalId)), {
16632
17857
  recursive: true,
16633
17858
  force: true
16634
17859
  }).catch(() => {
@@ -16650,9 +17875,9 @@ var PasteStore = class {
16650
17875
  }
16651
17876
  await Promise.all(
16652
17877
  entries.map(async (entry) => {
16653
- const full = path45.join(this.root, entry);
17878
+ const full = path47.join(this.root, entry);
16654
17879
  try {
16655
- const info = await stat25(full);
17880
+ const info = await stat27(full);
16656
17881
  if (now - info.mtimeMs > STALE_AFTER_MS) {
16657
17882
  await rm3(full, { recursive: true, force: true });
16658
17883
  }
@@ -16669,8 +17894,8 @@ function safeSegment(value) {
16669
17894
 
16670
17895
  // packages/server/src/session-index.ts
16671
17896
  import { EventEmitter as EventEmitter4 } from "node:events";
16672
- import { mkdir as mkdir9, readFile as readFile15, stat as stat26, writeFile as writeFile5 } from "node:fs/promises";
16673
- import path46 from "node:path";
17897
+ import { mkdir as mkdir9, readFile as readFile16, stat as stat28, writeFile as writeFile5 } from "node:fs/promises";
17898
+ import path48 from "node:path";
16674
17899
  var CACHE_VERSION = 4;
16675
17900
  var vaultPairKey = (agent, sessionId) => `${agent}/${sessionId}`;
16676
17901
  function vaultOnlySessions(natives, vault, listState, ready) {
@@ -16722,7 +17947,7 @@ function cacheKey(agent, ref) {
16722
17947
  }
16723
17948
  async function pathExists3(candidate) {
16724
17949
  try {
16725
- await stat26(candidate);
17950
+ await stat28(candidate);
16726
17951
  return true;
16727
17952
  } catch {
16728
17953
  return false;
@@ -16772,7 +17997,7 @@ function migrateEntryV3(filePath, value) {
16772
17997
  return {
16773
17998
  agent: "claude-code",
16774
17999
  ref: filePath,
16775
- group: path46.basename(path46.dirname(filePath)),
18000
+ group: path48.basename(path48.dirname(filePath)),
16776
18001
  mtimeMs,
16777
18002
  sizeBytes,
16778
18003
  cwd,
@@ -17037,7 +18262,7 @@ var SessionIndex = class extends EventEmitter4 {
17037
18262
  }
17038
18263
  async loadCache() {
17039
18264
  try {
17040
- const raw = await readFile15(sessionIndexCachePath(), "utf8");
18265
+ const raw = await readFile16(sessionIndexCachePath(), "utf8");
17041
18266
  const parsed = JSON.parse(raw);
17042
18267
  const migrated = migrateIndexCache(parsed);
17043
18268
  if (migrated === null) return;
@@ -17265,8 +18490,8 @@ var SessionIndex = class extends EventEmitter4 {
17265
18490
  };
17266
18491
 
17267
18492
  // packages/server/src/settings-store.ts
17268
- import { mkdir as mkdir10, readFile as readFile16, stat as stat27 } from "node:fs/promises";
17269
- import path47 from "node:path";
18493
+ import { mkdir as mkdir10, readFile as readFile17, stat as stat29 } from "node:fs/promises";
18494
+ import path49 from "node:path";
17270
18495
  var SETTINGS_VERSION = 1;
17271
18496
  var VAULT_TOOL_RESULT_DEFAULT_CHARS = 64e3;
17272
18497
  var VAULT_TOOL_RESULT_MIN_CHARS = 4e3;
@@ -17283,7 +18508,7 @@ function defaultAppSettings() {
17283
18508
  function isAbsoluteDir(dir, platform) {
17284
18509
  if (dir.length === 0 || dir.includes("\0")) return false;
17285
18510
  if (platform === "win32") return /^[A-Za-z]:[\\/]/.test(dir) || /^\\\\[^\\/]+[\\/][^\\/]+/.test(dir);
17286
- return path47.posix.isAbsolute(dir);
18511
+ return path49.posix.isAbsolute(dir);
17287
18512
  }
17288
18513
  function clampToolResultMaxChars(value) {
17289
18514
  if (typeof value !== "number" || !Number.isFinite(value)) return VAULT_TOOL_RESULT_DEFAULT_CHARS;
@@ -17342,7 +18567,7 @@ var SettingsStore = class {
17342
18567
  async load() {
17343
18568
  let raw;
17344
18569
  try {
17345
- raw = await readFile16(this.filePath, "utf8");
18570
+ raw = await readFile17(this.filePath, "utf8");
17346
18571
  } catch {
17347
18572
  this.current = frozen(defaultAppSettings());
17348
18573
  return;
@@ -17370,7 +18595,7 @@ var SettingsStore = class {
17370
18595
  async reloadRemote() {
17371
18596
  let parsed;
17372
18597
  try {
17373
- parsed = JSON.parse(await readFile16(this.filePath, "utf8"));
18598
+ parsed = JSON.parse(await readFile17(this.filePath, "utf8"));
17374
18599
  } catch {
17375
18600
  return;
17376
18601
  }
@@ -17381,7 +18606,7 @@ var SettingsStore = class {
17381
18606
  /** Una marca que cambia cuando el archivo cambia (fecha y tamano), o '' si no esta. */
17382
18607
  async stamp() {
17383
18608
  try {
17384
- const info = await stat27(this.filePath);
18609
+ const info = await stat29(this.filePath);
17385
18610
  return `${info.mtimeMs}:${info.size}`;
17386
18611
  } catch {
17387
18612
  return "";
@@ -17425,7 +18650,7 @@ var SettingsStore = class {
17425
18650
  if (!isRemotePort(remote.port)) throw new Error(`The remote access port isn't valid: ${JSON.stringify(remote.port)}`);
17426
18651
  next.remote.port = remote.port;
17427
18652
  }
17428
- await mkdir10(path47.dirname(this.filePath), { recursive: true });
18653
+ await mkdir10(path49.dirname(this.filePath), { recursive: true });
17429
18654
  await writeFileAtomic(this.filePath, `${JSON.stringify(next, null, 2)}
17430
18655
  `);
17431
18656
  this.current = frozen(next);
@@ -17513,8 +18738,8 @@ function supportStartupLineColored() {
17513
18738
 
17514
18739
  // packages/server/src/notes-store.ts
17515
18740
  import { randomUUID as randomUUID4 } from "node:crypto";
17516
- import { mkdir as mkdir11, readFile as readFile17, rename as rename5, rm as rm4, writeFile as writeFile6 } from "node:fs/promises";
17517
- import path48 from "node:path";
18741
+ import { mkdir as mkdir11, readFile as readFile18, rename as rename5, rm as rm4, writeFile as writeFile6 } from "node:fs/promises";
18742
+ import path50 from "node:path";
17518
18743
  var STATE_VERSION2 = 1;
17519
18744
  var WRITE_DEBOUNCE_MS2 = 500;
17520
18745
  var NotesError = class extends ServerTextError {
@@ -17557,7 +18782,7 @@ var NotesStore = class {
17557
18782
  }
17558
18783
  async load() {
17559
18784
  try {
17560
- const raw = await readFile17(this.statePath, "utf8");
18785
+ const raw = await readFile18(this.statePath, "utf8");
17561
18786
  this.notes = parseState2(raw) ?? [];
17562
18787
  } catch {
17563
18788
  this.notes = [];
@@ -17674,7 +18899,7 @@ var NotesStore = class {
17674
18899
  const image = this.findImage(imageId);
17675
18900
  if (image === null) return null;
17676
18901
  try {
17677
- const bytes = await readFile17(this.imagePath(image));
18902
+ const bytes = await readFile18(this.imagePath(image));
17678
18903
  return { mediaType: image.mediaType, data: bytes.toString("base64") };
17679
18904
  } catch {
17680
18905
  return null;
@@ -17694,7 +18919,7 @@ var NotesStore = class {
17694
18919
  const images = [];
17695
18920
  for (const image of note.images) {
17696
18921
  try {
17697
- const bytes = await readFile17(this.imagePath(image));
18922
+ const bytes = await readFile18(this.imagePath(image));
17698
18923
  images.push({ mediaType: image.mediaType, data: bytes.toString("base64") });
17699
18924
  } catch {
17700
18925
  }
@@ -17718,7 +18943,7 @@ var NotesStore = class {
17718
18943
  }
17719
18944
  imagePath(image) {
17720
18945
  const safe = image.imageId.replace(/[^a-zA-Z0-9-]/g, "");
17721
- return path48.join(this.imagesDir, `${safe}.${extensionFor(image.mediaType)}`);
18946
+ return path50.join(this.imagesDir, `${safe}.${extensionFor(image.mediaType)}`);
17722
18947
  }
17723
18948
  async unlinkImage(image) {
17724
18949
  await rm4(this.imagePath(image), { force: true }).catch(() => {
@@ -17736,8 +18961,8 @@ var NotesStore = class {
17736
18961
  async writeNow() {
17737
18962
  this.dirty = false;
17738
18963
  try {
17739
- await mkdir11(path48.dirname(this.statePath), { recursive: true });
17740
- const temporary = path48.join(path48.dirname(this.statePath), `notes.${process.pid}.tmp`);
18964
+ await mkdir11(path50.dirname(this.statePath), { recursive: true });
18965
+ const temporary = path50.join(path50.dirname(this.statePath), `notes.${process.pid}.tmp`);
17741
18966
  await writeFile6(
17742
18967
  temporary,
17743
18968
  JSON.stringify({ version: STATE_VERSION2, notes: this.notes }, null, 2),
@@ -17751,8 +18976,8 @@ var NotesStore = class {
17751
18976
  };
17752
18977
 
17753
18978
  // packages/server/src/draft-store.ts
17754
- import { mkdir as mkdir12, readFile as readFile18, rename as rename6, writeFile as writeFile7 } from "node:fs/promises";
17755
- import path49 from "node:path";
18979
+ import { mkdir as mkdir12, readFile as readFile19, rename as rename6, writeFile as writeFile7 } from "node:fs/promises";
18980
+ import path51 from "node:path";
17756
18981
  var STATE_VERSION3 = 1;
17757
18982
  var WRITE_DEBOUNCE_MS3 = 500;
17758
18983
  var MAX_STORED_DRAFTS = 100;
@@ -17858,7 +19083,7 @@ var DraftStore = class {
17858
19083
  }
17859
19084
  async load() {
17860
19085
  try {
17861
- this.drafts = parseState3(await readFile18(this.statePath, "utf8"));
19086
+ this.drafts = parseState3(await readFile19(this.statePath, "utf8"));
17862
19087
  } catch {
17863
19088
  this.drafts = /* @__PURE__ */ new Map();
17864
19089
  }
@@ -17949,7 +19174,7 @@ var DraftStore = class {
17949
19174
  async writeNow() {
17950
19175
  let onDisk = /* @__PURE__ */ new Map();
17951
19176
  try {
17952
- onDisk = parseState3(await readFile18(this.statePath, "utf8"));
19177
+ onDisk = parseState3(await readFile19(this.statePath, "utf8"));
17953
19178
  } catch {
17954
19179
  }
17955
19180
  const changed = this.changed;
@@ -17963,8 +19188,8 @@ var DraftStore = class {
17963
19188
  trimOldest(merged);
17964
19189
  this.drafts = new Map(merged);
17965
19190
  try {
17966
- await mkdir12(path49.dirname(this.statePath), { recursive: true });
17967
- const temporary = path49.join(path49.dirname(this.statePath), `composer-drafts.${process.pid}.tmp`);
19191
+ await mkdir12(path51.dirname(this.statePath), { recursive: true });
19192
+ const temporary = path51.join(path51.dirname(this.statePath), `composer-drafts.${process.pid}.tmp`);
17968
19193
  const payload = {
17969
19194
  version: STATE_VERSION3,
17970
19195
  drafts: [...merged.values()].map(({ agent, sessionId, updatedAt, draft }) => ({ agent, sessionId, updatedAt, draft }))
@@ -17981,7 +19206,7 @@ var DraftStore = class {
17981
19206
  // packages/server/src/shell-locator.ts
17982
19207
  import { access as access2 } from "node:fs/promises";
17983
19208
  import { constants as fsConstants2 } from "node:fs";
17984
- import path50 from "node:path";
19209
+ import path52 from "node:path";
17985
19210
  async function isExecutable(candidate) {
17986
19211
  try {
17987
19212
  await access2(candidate, process.platform === "win32" ? fsConstants2.R_OK : fsConstants2.X_OK);
@@ -17999,21 +19224,21 @@ var SYSTEM_PROBE = {
17999
19224
  function windowsPowerShellPath(env) {
18000
19225
  const systemRoot = env["SystemRoot"] ?? env["windir"];
18001
19226
  if (systemRoot === void 0 || systemRoot.length === 0) return null;
18002
- return path50.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
19227
+ return path52.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
18003
19228
  }
18004
19229
  function gitBashCandidates(gitPath, env) {
18005
19230
  const candidates = [];
18006
19231
  if (gitPath !== null) {
18007
- let dir = path50.win32.dirname(gitPath);
19232
+ let dir = path52.win32.dirname(gitPath);
18008
19233
  for (let level = 0; level < 3; level++) {
18009
- candidates.push(path50.win32.join(dir, "bin", "bash.exe"));
18010
- const parent = path50.win32.dirname(dir);
19234
+ candidates.push(path52.win32.join(dir, "bin", "bash.exe"));
19235
+ const parent = path52.win32.dirname(dir);
18011
19236
  if (parent === dir) break;
18012
19237
  dir = parent;
18013
19238
  }
18014
19239
  }
18015
- for (const base of [env["ProgramFiles"], env["LOCALAPPDATA"] === void 0 ? void 0 : path50.win32.join(env["LOCALAPPDATA"], "Programs")]) {
18016
- if (base !== void 0 && base.length > 0) candidates.push(path50.win32.join(base, "Git", "bin", "bash.exe"));
19240
+ for (const base of [env["ProgramFiles"], env["LOCALAPPDATA"] === void 0 ? void 0 : path52.win32.join(env["LOCALAPPDATA"], "Programs")]) {
19241
+ if (base !== void 0 && base.length > 0) candidates.push(path52.win32.join(base, "Git", "bin", "bash.exe"));
18017
19242
  }
18018
19243
  const systemRoot = (env["SystemRoot"] ?? env["windir"] ?? "").toLowerCase();
18019
19244
  return [...new Set(candidates)].filter((candidate) => systemRoot.length === 0 || !candidate.toLowerCase().startsWith(systemRoot));
@@ -18044,7 +19269,7 @@ async function locatePosixShells(probe) {
18044
19269
  const found = [];
18045
19270
  const preferred = probe.env["SHELL"];
18046
19271
  if (preferred !== void 0 && preferred.length > 0 && await probe.isExecutable(preferred)) {
18047
- const name = path50.posix.basename(preferred);
19272
+ const name = path52.posix.basename(preferred);
18048
19273
  found.push({ id: "login", label: name, location: { file: preferred, args: [], name } });
18049
19274
  }
18050
19275
  for (const [id, file] of [["bash", "/bin/bash"], ["sh", "/bin/sh"], ["zsh", "/bin/zsh"]]) {
@@ -18116,13 +19341,13 @@ var AppSettingsService = class extends EventEmitter5 {
18116
19341
  };
18117
19342
 
18118
19343
  // packages/server/src/session-watcher.ts
18119
- import { stat as stat28 } from "node:fs/promises";
19344
+ import { stat as stat30 } from "node:fs/promises";
18120
19345
  import chokidar3 from "chokidar";
18121
19346
  var INDEX_DEBOUNCE_MS = 750;
18122
19347
  var ROOT_PROBE_MS = 5e3;
18123
19348
  async function isDirectory3(target) {
18124
19349
  try {
18125
- return (await stat28(target)).isDirectory();
19350
+ return (await stat30(target)).isDirectory();
18126
19351
  } catch {
18127
19352
  return false;
18128
19353
  }
@@ -18225,7 +19450,7 @@ function watchSessions(index, hub, agents, options = {}) {
18225
19450
  // packages/server/src/terminal-registry.ts
18226
19451
  import { randomUUID as randomUUID5 } from "node:crypto";
18227
19452
  import { EventEmitter as EventEmitter6 } from "node:events";
18228
- import { stat as stat29 } from "node:fs/promises";
19453
+ import { stat as stat31 } from "node:fs/promises";
18229
19454
 
18230
19455
  // packages/server/src/launch-hook-slot.ts
18231
19456
  var LaunchHookSlot = class {
@@ -18426,6 +19651,45 @@ var PtySession = class {
18426
19651
  }
18427
19652
  };
18428
19653
 
19654
+ // packages/server/src/pty-screen.ts
19655
+ import headless from "@xterm/headless";
19656
+ var { Terminal } = headless;
19657
+ function createPtyScreen(cols, rows, replay) {
19658
+ const term = new Terminal({ cols, rows, scrollback: 0, allowProposedApi: true });
19659
+ let disposed = false;
19660
+ if (replay.length > 0) term.write(replay);
19661
+ return {
19662
+ feed(chunk) {
19663
+ if (!disposed) term.write(chunk);
19664
+ },
19665
+ lines() {
19666
+ return new Promise((resolve) => {
19667
+ if (disposed) {
19668
+ resolve([]);
19669
+ return;
19670
+ }
19671
+ term.write("", () => {
19672
+ try {
19673
+ const buffer = term.buffer.active;
19674
+ const lines = [];
19675
+ for (let y = 0; y < term.rows; y += 1) {
19676
+ lines.push(buffer.getLine(buffer.viewportY + y)?.translateToString(true) ?? "");
19677
+ }
19678
+ resolve(lines);
19679
+ } catch {
19680
+ resolve([]);
19681
+ }
19682
+ });
19683
+ });
19684
+ },
19685
+ dispose() {
19686
+ if (disposed) return;
19687
+ disposed = true;
19688
+ term.dispose();
19689
+ }
19690
+ };
19691
+ }
19692
+
18429
19693
  // packages/server/src/terminal-activity.ts
18430
19694
  var ActivityBook = class {
18431
19695
  entries = /* @__PURE__ */ new Map();
@@ -18487,8 +19751,8 @@ async function resolveLaunchPlan(launch) {
18487
19751
  }
18488
19752
 
18489
19753
  // packages/server/src/workspace-store.ts
18490
- import { mkdir as mkdir13, readFile as readFile19, writeFile as writeFile8 } from "node:fs/promises";
18491
- import path51 from "node:path";
19754
+ import { mkdir as mkdir13, readFile as readFile20, writeFile as writeFile8 } from "node:fs/promises";
19755
+ import path53 from "node:path";
18492
19756
  var STATE_VERSION4 = 1;
18493
19757
  var WRITE_DEBOUNCE_MS4 = 400;
18494
19758
  var LEGACY_TAB_AGENT = "claude-code";
@@ -18610,7 +19874,7 @@ var WorkspaceStore = class {
18610
19874
  pending = null;
18611
19875
  async load() {
18612
19876
  try {
18613
- const raw = await readFile19(workspaceStatePath(), "utf8");
19877
+ const raw = await readFile20(workspaceStatePath(), "utf8");
18614
19878
  return parseState4(raw) ?? EMPTY_STATE();
18615
19879
  } catch {
18616
19880
  return EMPTY_STATE();
@@ -18642,7 +19906,7 @@ var WorkspaceStore = class {
18642
19906
  try {
18643
19907
  await mkdir13(appConfigDir(), { recursive: true });
18644
19908
  const target = workspaceStatePath();
18645
- const temporary = path51.join(path51.dirname(target), `workspace.${process.pid}.tmp`);
19909
+ const temporary = path53.join(path53.dirname(target), `workspace.${process.pid}.tmp`);
18646
19910
  await writeFile8(temporary, JSON.stringify(serializeState(state), null, 2), "utf8");
18647
19911
  const { rename: rename7 } = await import("node:fs/promises");
18648
19912
  await rename7(temporary, target);
@@ -18880,7 +20144,7 @@ var TerminalRegistry = class extends EventEmitter6 {
18880
20144
  async assertDirectory(cwd) {
18881
20145
  let info;
18882
20146
  try {
18883
- info = await stat29(cwd);
20147
+ info = await stat31(cwd);
18884
20148
  } catch {
18885
20149
  throw new TerminalOpenError("invalid-cwd", serverText("cwdMissing", { cwd }));
18886
20150
  }
@@ -19245,6 +20509,27 @@ var TerminalRegistry = class extends EventEmitter6 {
19245
20509
  * normal de una pestana restaurada. Quien llama decide que hacer —el socket
19246
20510
  * lo dice, el cliente ofrece abrir la CLI.
19247
20511
  */
20512
+ /**
20513
+ * La pantalla de la CLI de esa pestana, emulada, para leerla mientras se le
20514
+ * escribe (hito 44: el menu de rewind, §4.16). Arranca con la salida reciente
20515
+ * y sigue con la que llegue, al tamano actual de la pty. `dispose` la suelta.
20516
+ * null sin proceso.
20517
+ */
20518
+ watchScreen(terminalId) {
20519
+ const entry = this.terminals.get(terminalId);
20520
+ if (entry === void 0 || entry.session === null) return null;
20521
+ const screen = createPtyScreen(entry.size.cols, entry.size.rows, entry.buffer.read());
20522
+ const listener = (_terminalId, chunk) => screen.feed(chunk);
20523
+ entry.listeners.add(listener);
20524
+ return {
20525
+ feed: (chunk) => screen.feed(chunk),
20526
+ lines: () => screen.lines(),
20527
+ dispose: () => {
20528
+ entry.listeners.delete(listener);
20529
+ screen.dispose();
20530
+ }
20531
+ };
20532
+ }
19248
20533
  write(terminalId, data) {
19249
20534
  const entry = this.terminals.get(terminalId);
19250
20535
  if (entry === void 0 || entry.session === null) return false;
@@ -19311,26 +20596,26 @@ import { WebSocketServer } from "ws";
19311
20596
 
19312
20597
  // packages/server/src/directory-picker.ts
19313
20598
  import { randomUUID as randomUUID6 } from "node:crypto";
19314
- import { lstat, mkdir as mkdir14, readdir as readdir13, stat as stat30 } from "node:fs/promises";
20599
+ import { lstat, mkdir as mkdir14, readdir as readdir13, stat as stat32 } from "node:fs/promises";
19315
20600
  import { homedir as homedir13 } from "node:os";
19316
- import path52 from "node:path";
20601
+ import path54 from "node:path";
19317
20602
  var MAX_ENTRIES = 500;
19318
20603
  var MAX_PICKERS = 8;
19319
20604
  var SKIPPED = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "bin", "obj", "$RECYCLE.BIN"]);
19320
20605
  var DirectoryPickerError = class extends ServerTextError {
19321
20606
  };
19322
20607
  function isInsideProtected(target, dirs) {
19323
- const resolved = path52.resolve(target);
20608
+ const resolved = path54.resolve(target);
19324
20609
  return dirs.some((dir) => {
19325
- const protectedDir = path52.resolve(dir);
19326
- return resolved === protectedDir || resolved.startsWith(protectedDir + path52.sep);
20610
+ const protectedDir = path54.resolve(dir);
20611
+ return resolved === protectedDir || resolved.startsWith(protectedDir + path54.sep);
19327
20612
  });
19328
20613
  }
19329
20614
  async function isListedDirectory(parent, entry, readLink = lstat) {
19330
20615
  if (entry.isDirectory()) return true;
19331
20616
  if (!entry.isSymbolicLink()) return false;
19332
20617
  try {
19333
- const info = await readLink(path52.join(parent, entry.name));
20618
+ const info = await readLink(path54.join(parent, entry.name));
19334
20619
  return info.isDirectory() && !info.isSymbolicLink();
19335
20620
  } catch {
19336
20621
  return false;
@@ -19345,7 +20630,7 @@ async function listRoots() {
19345
20630
  letters.map(async (letter) => {
19346
20631
  const drive = `${letter}:\\`;
19347
20632
  try {
19348
- await stat30(drive);
20633
+ await stat32(drive);
19349
20634
  return drive;
19350
20635
  } catch {
19351
20636
  return null;
@@ -19397,14 +20682,14 @@ var DirectoryPickers = class {
19397
20682
  async enter(pickerId, name) {
19398
20683
  const picker = this.require(pickerId);
19399
20684
  if (name === "..") {
19400
- const parent = path52.dirname(picker.current);
20685
+ const parent = path54.dirname(picker.current);
19401
20686
  if (parent !== picker.current) picker.current = parent;
19402
20687
  return this.listing(picker);
19403
20688
  }
19404
20689
  if (!picker.entries.has(name)) {
19405
20690
  throw new DirectoryPickerError(serverText("pickerNotListed"));
19406
20691
  }
19407
- const target = path52.join(picker.current, name);
20692
+ const target = path54.join(picker.current, name);
19408
20693
  if (isInsideProtected(target, this.protectedDirs)) {
19409
20694
  throw new DirectoryPickerError(serverText("pickerCliFolder"));
19410
20695
  }
@@ -19444,7 +20729,7 @@ var DirectoryPickers = class {
19444
20729
  throw new DirectoryPickerError(serverText("pickerNameChars"));
19445
20730
  }
19446
20731
  }
19447
- const target = path52.join(picker.current, clean);
20732
+ const target = path54.join(picker.current, clean);
19448
20733
  if (isInsideProtected(target, this.protectedDirs)) {
19449
20734
  throw new DirectoryPickerError(serverText("pickerNoProjectsHere"));
19450
20735
  }
@@ -19482,7 +20767,7 @@ var DirectoryPickers = class {
19482
20767
  try {
19483
20768
  const found = await readdir13(picker.current, { withFileTypes: true });
19484
20769
  const listed = await Promise.all(found.map((entry) => isListedDirectory(picker.current, entry)));
19485
- const dirs = found.filter((_, index) => listed[index] === true).map((entry) => entry.name).filter((name) => !SKIPPED.has(name)).filter((name) => !isInsideProtected(path52.join(picker.current, name), this.protectedDirs)).sort((a, b) => a.localeCompare(b));
20770
+ const dirs = found.filter((_, index) => listed[index] === true).map((entry) => entry.name).filter((name) => !SKIPPED.has(name)).filter((name) => !isInsideProtected(path54.join(picker.current, name), this.protectedDirs)).sort((a, b) => a.localeCompare(b));
19486
20771
  truncated = dirs.length > MAX_ENTRIES;
19487
20772
  names = dirs.slice(0, MAX_ENTRIES);
19488
20773
  } catch {
@@ -19493,15 +20778,51 @@ var DirectoryPickers = class {
19493
20778
  path: picker.current,
19494
20779
  entries: names,
19495
20780
  roots,
19496
- canGoUp: path52.dirname(picker.current) !== picker.current,
20781
+ canGoUp: path54.dirname(picker.current) !== picker.current,
19497
20782
  truncated
19498
20783
  };
19499
20784
  }
19500
20785
  };
19501
20786
 
20787
+ // packages/server/src/cli-folders.ts
20788
+ import { realpath as realpath4 } from "node:fs/promises";
20789
+ import path55 from "node:path";
20790
+ function isSameOrInside(child, parent, platform) {
20791
+ const childKey = normalizeCwdKey(child, platform);
20792
+ const parentKey = normalizeCwdKey(parent, platform);
20793
+ if (childKey.length === 0 || parentKey.length === 0) return false;
20794
+ if (childKey === parentKey) return true;
20795
+ const separator = platform === "win32" ? "\\" : "/";
20796
+ return childKey.startsWith(parentKey.endsWith(separator) ? parentKey : `${parentKey}${separator}`);
20797
+ }
20798
+ async function cliFolders(dirs, platform = process.platform) {
20799
+ const known = [];
20800
+ for (const dir of dirs) {
20801
+ known.push(path55.resolve(dir));
20802
+ try {
20803
+ known.push(await realpath4(dir));
20804
+ } catch {
20805
+ }
20806
+ }
20807
+ const holds = (candidate) => known.some((dir) => isSameOrInside(candidate, dir, platform));
20808
+ return {
20809
+ holds,
20810
+ async contains(absolute) {
20811
+ if (known.length === 0) return false;
20812
+ if (holds(absolute)) return true;
20813
+ try {
20814
+ return holds(await realpath4(absolute));
20815
+ } catch {
20816
+ return false;
20817
+ }
20818
+ }
20819
+ };
20820
+ }
20821
+
19502
20822
  // packages/server/src/file-browser.ts
19503
- import { readFile as readFile20, readdir as readdir14, stat as stat31 } from "node:fs/promises";
19504
- import path53 from "node:path";
20823
+ import { createHash as createHash4 } from "node:crypto";
20824
+ import { readFile as readFile21, readdir as readdir14, realpath as realpath5, stat as stat33 } from "node:fs/promises";
20825
+ import path56 from "node:path";
19505
20826
  var MAX_ENTRIES2 = 1e3;
19506
20827
  var SEARCH_MAX_RESULTS = 200;
19507
20828
  var SEARCH_MAX_DIRECTORIES = 4e3;
@@ -19564,7 +20885,7 @@ function languageFor(fileName) {
19564
20885
  const lower = fileName.toLowerCase();
19565
20886
  const byName = LANGUAGE_BY_NAME[lower];
19566
20887
  if (byName !== void 0) return byName;
19567
- const extension = path53.extname(lower);
20888
+ const extension = path56.extname(lower);
19568
20889
  return LANGUAGE_BY_EXTENSION[extension] ?? null;
19569
20890
  }
19570
20891
  function looksBinary2(buffer) {
@@ -19577,10 +20898,15 @@ function looksBinary2(buffer) {
19577
20898
  function joinRelative(parent, name) {
19578
20899
  return parent.length === 0 ? name : `${parent}/${name}`;
19579
20900
  }
19580
- async function listDirectory(cwd, relativePath, options = {}) {
20901
+ async function resolveForFiles(cwd, relativePath, folders) {
19581
20902
  const absolute = await resolveInside(cwd, relativePath, { mustExist: true });
20903
+ if (await folders.contains(absolute)) throw new InvalidPathError(serverText("cliFolderClosed"));
20904
+ return absolute;
20905
+ }
20906
+ async function listDirectory(cwd, relativePath, folders, options = {}) {
20907
+ const absolute = await resolveForFiles(cwd, relativePath, folders);
19582
20908
  const repoRoot2 = await findRepoRoot(cwd).catch(() => null);
19583
- const scanned = await scanLevel(absolute, repoRoot2);
20909
+ const scanned = await scanLevel(absolute, repoRoot2, folders);
19584
20910
  const entries = [];
19585
20911
  let hiddenCount = scanned.skipped;
19586
20912
  let truncated = false;
@@ -19596,7 +20922,7 @@ async function listDirectory(cwd, relativePath, options = {}) {
19596
20922
  let sizeBytes = 0;
19597
20923
  let modifiedAt = 0;
19598
20924
  try {
19599
- const info = await stat31(path53.join(absolute, candidate.name));
20925
+ const info = await stat33(path56.join(absolute, candidate.name));
19600
20926
  sizeBytes = candidate.isDirectory ? 0 : info.size;
19601
20927
  modifiedAt = info.mtimeMs;
19602
20928
  } catch {
@@ -19616,20 +20942,23 @@ async function listDirectory(cwd, relativePath, options = {}) {
19616
20942
  });
19617
20943
  return { path: relativePath, entries, truncated, hiddenCount };
19618
20944
  }
19619
- async function scanLevel(absolute, repoRoot2) {
19620
- const level = await readLevel(absolute);
20945
+ async function scanLevel(absolute, repoRoot2, folders) {
20946
+ const level = await readLevel(absolute, folders);
19621
20947
  await markIgnored(repoRoot2, [{ absolute, entries: level.entries }]);
19622
20948
  return level;
19623
20949
  }
19624
- async function readLevel(absolute) {
20950
+ async function readLevel(absolute, folders) {
19625
20951
  const dirents = await readdir14(absolute, { withFileTypes: true });
20952
+ const realLevel = await realpath5(absolute);
19626
20953
  let skipped = 0;
19627
20954
  const candidates = [];
19628
20955
  for (const dirent of dirents) {
19629
20956
  const always = ALWAYS_SKIP.has(dirent.name.toLowerCase());
19630
20957
  if (dirent.isSymbolicLink()) {
19631
20958
  try {
19632
- const target = await stat31(path53.join(absolute, dirent.name));
20959
+ const linked = path56.join(absolute, dirent.name);
20960
+ const target = await stat33(linked);
20961
+ if (folders.holds(await realpath5(linked))) continue;
19633
20962
  candidates.push({
19634
20963
  name: dirent.name,
19635
20964
  isDirectory: target.isDirectory(),
@@ -19644,6 +20973,7 @@ async function readLevel(absolute) {
19644
20973
  skipped += 1;
19645
20974
  continue;
19646
20975
  }
20976
+ if (folders.holds(path56.join(realLevel, dirent.name))) continue;
19647
20977
  candidates.push({
19648
20978
  name: dirent.name,
19649
20979
  isDirectory: dirent.isDirectory(),
@@ -19658,7 +20988,7 @@ async function markIgnored(repoRoot2, levels) {
19658
20988
  for (const level of levels) {
19659
20989
  for (const entry of level.entries) {
19660
20990
  if (entry.hidden !== null) continue;
19661
- keys.set(entry, toPosixPath(path53.relative(repoRoot2, path53.join(level.absolute, entry.name))));
20991
+ keys.set(entry, toPosixPath(path56.relative(repoRoot2, path56.join(level.absolute, entry.name))));
19662
20992
  }
19663
20993
  }
19664
20994
  if (keys.size === 0) return;
@@ -19669,9 +20999,10 @@ async function markIgnored(repoRoot2, levels) {
19669
20999
  if (ignored.has(key)) entry.hidden = "ignored";
19670
21000
  }
19671
21001
  }
19672
- async function searchFiles(cwd, query, options = {}) {
21002
+ async function searchFiles(cwd, query, folders, options = {}) {
19673
21003
  const needle = query.trim().toLowerCase();
19674
21004
  if (needle.length === 0) return { query, entries: [], truncated: false };
21005
+ await resolveForFiles(cwd, "", folders);
19675
21006
  const repoRoot2 = await findRepoRoot(cwd).catch(() => null);
19676
21007
  const deadline = Date.now() + SEARCH_TIME_BUDGET_MS;
19677
21008
  const found = [];
@@ -19701,7 +21032,7 @@ async function searchFiles(cwd, query, options = {}) {
19701
21032
  continue;
19702
21033
  }
19703
21034
  try {
19704
- const level = await readLevel(absolute);
21035
+ const level = await readLevel(absolute, folders);
19705
21036
  wave.push({ relative, absolute, entries: level.entries });
19706
21037
  } catch {
19707
21038
  }
@@ -19719,7 +21050,7 @@ async function searchFiles(cwd, query, options = {}) {
19719
21050
  let sizeBytes = 0;
19720
21051
  let modifiedAt = 0;
19721
21052
  try {
19722
- const info = await stat31(path53.join(level.absolute, candidate.name));
21053
+ const info = await stat33(path56.join(level.absolute, candidate.name));
19723
21054
  sizeBytes = candidate.isDirectory ? 0 : info.size;
19724
21055
  modifiedAt = info.mtimeMs;
19725
21056
  } catch {
@@ -19751,25 +21082,31 @@ async function searchFiles(cwd, query, options = {}) {
19751
21082
  truncated: truncated || found.length > SEARCH_MAX_RESULTS
19752
21083
  };
19753
21084
  }
19754
- async function existingFiles(cwd, relativePaths) {
21085
+ async function existingFiles(cwd, relativePaths, folders) {
19755
21086
  const existing = [];
19756
21087
  for (const relativePath of relativePaths) {
19757
21088
  try {
19758
- const absolute = await resolveInside(cwd, relativePath, { mustExist: true });
19759
- if ((await stat31(absolute)).isFile()) existing.push(relativePath);
21089
+ const absolute = await resolveForFiles(cwd, relativePath, folders);
21090
+ if ((await stat33(absolute)).isFile()) existing.push(relativePath);
19760
21091
  } catch {
19761
21092
  }
19762
21093
  }
19763
21094
  return existing;
19764
21095
  }
19765
- async function readPreview(cwd, relativePath) {
19766
- const absolute = await resolveInside(cwd, relativePath, { mustExist: true });
19767
- const info = await stat31(absolute);
21096
+ function contentHash(content) {
21097
+ return createHash4("sha256").update(content).digest("hex");
21098
+ }
21099
+ function isEditableText(content, text) {
21100
+ return !/\r(?!\n)/.test(text) && Buffer.from(text, "utf8").equals(content);
21101
+ }
21102
+ async function readPreview(cwd, relativePath, folders) {
21103
+ const absolute = await resolveForFiles(cwd, relativePath, folders);
21104
+ const info = await stat33(absolute);
19768
21105
  if (info.isDirectory()) {
19769
21106
  throw new Error("It's a directory, not a file.");
19770
21107
  }
19771
- const content = await readFile20(absolute);
19772
- const fileName = path53.basename(absolute);
21108
+ const content = await readFile21(absolute);
21109
+ const fileName = path56.basename(absolute);
19773
21110
  if (looksBinary2(content)) {
19774
21111
  return {
19775
21112
  path: relativePath,
@@ -19781,14 +21118,71 @@ async function readPreview(cwd, relativePath) {
19781
21118
  };
19782
21119
  }
19783
21120
  const truncated = content.length > MAX_PREVIEW_BYTES;
19784
- return {
21121
+ const text = content.subarray(0, MAX_PREVIEW_BYTES).toString("utf8");
21122
+ const preview = {
19785
21123
  path: relativePath,
19786
21124
  language: languageFor(fileName),
19787
- text: content.subarray(0, MAX_PREVIEW_BYTES).toString("utf8"),
21125
+ text,
19788
21126
  sizeBytes: info.size,
19789
21127
  truncated,
19790
21128
  binary: false
19791
21129
  };
21130
+ return truncated || !isEditableText(content, text) ? preview : { ...preview, editable: true, hash: contentHash(content) };
21131
+ }
21132
+
21133
+ // packages/server/src/file-write.ts
21134
+ import { constants } from "node:fs";
21135
+ import { access as access3, readFile as readFile22, realpath as realpath6, stat as stat34 } from "node:fs/promises";
21136
+ var FileWriteError = class extends ServerTextError {
21137
+ };
21138
+ function withLineEndings(text, original) {
21139
+ const crlf = original.match(/\r\n/g)?.length ?? 0;
21140
+ const lf = original.match(/(?<!\r)\n/g)?.length ?? 0;
21141
+ const plain = text.replace(/\r\n/g, "\n");
21142
+ return crlf > lf ? plain.replace(/\n/g, "\r\n") : plain;
21143
+ }
21144
+ function isMissing(error) {
21145
+ return error instanceof InvalidPathError && error.text.key === "pathMissing";
21146
+ }
21147
+ function errorCode(error) {
21148
+ return error?.code;
21149
+ }
21150
+ async function saveProjectFile(cwd, relativePath, text, baseHash, folders) {
21151
+ let absolute;
21152
+ try {
21153
+ absolute = await resolveInside(cwd, relativePath, { mustExist: true });
21154
+ } catch (error) {
21155
+ if (isMissing(error)) return { outcome: "changed", preview: null };
21156
+ throw error;
21157
+ }
21158
+ if (await folders.contains(absolute)) throw new InvalidPathError(serverText("cliFolderClosed"));
21159
+ const target = await realpath6(absolute);
21160
+ const info = await stat34(target);
21161
+ if (!info.isFile()) throw new FileWriteError(serverText("fileSaveFailed"));
21162
+ const current = await readFile22(target);
21163
+ if (contentHash(current) !== baseHash) {
21164
+ return { outcome: "changed", preview: await readPreview(cwd, relativePath, folders).catch(() => null) };
21165
+ }
21166
+ if (current.length > MAX_PREVIEW_BYTES || !isEditableText(current, current.toString("utf8"))) {
21167
+ throw new FileWriteError(serverText("fileSaveFailed"));
21168
+ }
21169
+ try {
21170
+ await access3(target, constants.W_OK);
21171
+ } catch {
21172
+ throw new FileWriteError(serverText("fileSaveBlocked"));
21173
+ }
21174
+ const content = Buffer.from(withLineEndings(text, current.toString("utf8")), "utf8");
21175
+ if (content.length > MAX_PREVIEW_BYTES) {
21176
+ throw new FileWriteError(serverText("fileSaveTooLarge", { max: `${MAX_PREVIEW_BYTES / 1024} KB` }));
21177
+ }
21178
+ try {
21179
+ await writeAtomically(target, content, { mode: info.mode });
21180
+ } catch (error) {
21181
+ const code = errorCode(error);
21182
+ if (code === "EPERM" || code === "EACCES" || code === "EBUSY") throw new FileWriteError(serverText("fileSaveBlocked"));
21183
+ throw error;
21184
+ }
21185
+ return { outcome: "saved", preview: await readPreview(cwd, relativePath, folders) };
19792
21186
  }
19793
21187
 
19794
21188
  // packages/server/src/global-search.ts
@@ -20265,11 +21659,15 @@ var RECLAIM_POLL_MS = 250;
20265
21659
  var QueuedSubmitBook = class {
20266
21660
  submissions = /* @__PURE__ */ new Map();
20267
21661
  clearKeyAt = /* @__PURE__ */ new Map();
20268
- /** Un mensaje termino de escribirse en la pty, Enter incluido. */
20269
- note(terminalId, text, at) {
21662
+ /**
21663
+ * Un mensaje termino de escribirse en la pty, Enter incluido. `original`: el
21664
+ * texto del cuadro sin las referencias a adjuntos (hito 44), si difiere.
21665
+ */
21666
+ note(terminalId, text, at, original) {
20270
21667
  if (text.trim().length === 0) return;
20271
21668
  const kept = (this.submissions.get(terminalId) ?? []).filter((each) => at - each.at < SUBMISSION_MAX_AGE_MS2);
20272
- this.submissions.set(terminalId, [...kept, { text, at }].slice(-SUBMISSIONS_KEPT2));
21669
+ const submission = original === void 0 || original === text ? { text, at } : { text, original, at };
21670
+ this.submissions.set(terminalId, [...kept, submission].slice(-SUBMISSIONS_KEPT2));
20273
21671
  }
20274
21672
  /** Se lleva lo anotado de esa pestana. */
20275
21673
  take(terminalId) {
@@ -20305,13 +21703,15 @@ async function reclaimQueuedSubmits(deps) {
20305
21703
  const pollMs = deps.pollMs ?? RECLAIM_POLL_MS;
20306
21704
  const deadline = deps.now() + timeoutMs;
20307
21705
  let undelivered = [];
21706
+ let withdrawn = [];
20308
21707
  for (; ; ) {
20309
21708
  const target = deps.target();
20310
21709
  if (target === null) return "gone";
20311
21710
  const result = await deps.source.check(target, submissions, deps.interruptedAt);
20312
- if (result.undelivered.length === 0) return "nothing";
20313
- if (result.settled) {
21711
+ if (result.undelivered.length === 0 && result.pending !== true) return "nothing";
21712
+ if (result.settled && result.undelivered.length > 0) {
20314
21713
  undelivered = result.undelivered;
21714
+ withdrawn = result.withdrawn ?? [];
20315
21715
  break;
20316
21716
  }
20317
21717
  if (deps.now() >= deadline) return "not-settled";
@@ -20324,7 +21724,8 @@ async function reclaimQueuedSubmits(deps) {
20324
21724
  }
20325
21725
  if (deps.target() === null) return "gone";
20326
21726
  if (!deps.writeClearKey()) return "refused";
20327
- deps.prefill(undelivered.join("\n\n"));
21727
+ await deps.prefill(undelivered.join("\n\n"));
21728
+ if (withdrawn.length > 0) deps.withdraw?.(withdrawn);
20328
21729
  return "reclaimed";
20329
21730
  }
20330
21731
 
@@ -20429,14 +21830,14 @@ var TerminalWriteQueue = class {
20429
21830
  };
20430
21831
 
20431
21832
  // packages/server/src/vault/service.ts
20432
- import { mkdir as mkdir17, stat as stat35 } from "node:fs/promises";
21833
+ import { mkdir as mkdir17, stat as stat38 } from "node:fs/promises";
20433
21834
  import { homedir as homedir14 } from "node:os";
20434
- import path57 from "node:path";
21835
+ import path60 from "node:path";
20435
21836
 
20436
21837
  // packages/server/src/vault/move.ts
20437
- import { constants } from "node:fs";
20438
- import { copyFile as copyFile2, lstat as lstat2, mkdir as mkdir15, readdir as readdir15, rm as rm5, stat as stat32, utimes, writeFile as writeFile9 } from "node:fs/promises";
20439
- import path54 from "node:path";
21838
+ import { constants as constants2 } from "node:fs";
21839
+ import { copyFile as copyFile2, lstat as lstat2, mkdir as mkdir15, readdir as readdir15, rm as rm5, stat as stat35, utimes, writeFile as writeFile9 } from "node:fs/promises";
21840
+ import path57 from "node:path";
20440
21841
  var MOVED_ENTRIES = ["vault.json", "sessions", "memory"];
20441
21842
  var OWN_TEMP_FILE = /\.\d+\.[0-9a-f]{12}\.tmp$/;
20442
21843
  async function copyTree(source, target, result) {
@@ -20449,14 +21850,14 @@ async function copyTree(source, target, result) {
20449
21850
  if (info.isDirectory()) {
20450
21851
  await mkdir15(target, { recursive: true });
20451
21852
  for (const name of await readdir15(source)) {
20452
- await copyTree(path54.join(source, name), path54.join(target, name), result);
21853
+ await copyTree(path57.join(source, name), path57.join(target, name), result);
20453
21854
  }
20454
21855
  return;
20455
21856
  }
20456
21857
  if (!info.isFile() || OWN_TEMP_FILE.test(source)) return;
20457
- await mkdir15(path54.dirname(target), { recursive: true });
21858
+ await mkdir15(path57.dirname(target), { recursive: true });
20458
21859
  try {
20459
- await copyFile2(source, target, constants.COPYFILE_EXCL);
21860
+ await copyFile2(source, target, constants2.COPYFILE_EXCL);
20460
21861
  } catch (error) {
20461
21862
  if (error.code === "EEXIST") {
20462
21863
  result.skipped += 1;
@@ -20471,11 +21872,11 @@ async function moveVault(from, to) {
20471
21872
  const result = { copied: 0, skipped: 0 };
20472
21873
  await mkdir15(to, { recursive: true });
20473
21874
  for (const entry of MOVED_ENTRIES) {
20474
- await copyTree(path54.join(from, entry), path54.join(to, entry), result);
21875
+ await copyTree(path57.join(from, entry), path57.join(to, entry), result);
20475
21876
  }
20476
21877
  return result;
20477
21878
  }
20478
- function isSameOrInside(child, parent, platform) {
21879
+ function isSameOrInside2(child, parent, platform) {
20479
21880
  const childKey = normalizeCwdKey(child, platform);
20480
21881
  const parentKey = normalizeCwdKey(parent, platform);
20481
21882
  if (childKey.length === 0 || parentKey.length === 0) return false;
@@ -20487,26 +21888,26 @@ function checkVaultTarget(current, target, options) {
20487
21888
  if (!isAbsoluteDir(target, options.platform)) {
20488
21889
  return { kind: "refused", text: serverText("vaultTargetNotAbsolute") };
20489
21890
  }
20490
- const isProtected = isInsideProtected(target, options.protectedDirs) || options.protectedDirs.some((dir) => isSameOrInside(target, dir, options.platform));
21891
+ const isProtected = isInsideProtected(target, options.protectedDirs) || options.protectedDirs.some((dir) => isSameOrInside2(target, dir, options.platform));
20491
21892
  if (isProtected) {
20492
21893
  return { kind: "refused", text: serverText("vaultTargetCliFolder") };
20493
21894
  }
20494
21895
  if (normalizeCwdKey(current, options.platform) === normalizeCwdKey(target, options.platform)) {
20495
21896
  return { kind: "same" };
20496
21897
  }
20497
- if (isSameOrInside(target, current, options.platform)) {
21898
+ if (isSameOrInside2(target, current, options.platform)) {
20498
21899
  return { kind: "refused", text: serverText("vaultTargetInsideCurrent") };
20499
21900
  }
20500
- if (isSameOrInside(current, target, options.platform)) {
21901
+ if (isSameOrInside2(current, target, options.platform)) {
20501
21902
  return { kind: "refused", text: serverText("vaultCurrentInsideTarget") };
20502
21903
  }
20503
21904
  return { kind: "ok" };
20504
21905
  }
20505
21906
  async function canWriteInto(dir) {
20506
- const probe = temporaryPathFor(path54.join(dir, ".escritura"));
21907
+ const probe = temporaryPathFor(path57.join(dir, ".escritura"));
20507
21908
  try {
20508
21909
  await mkdir15(dir, { recursive: true });
20509
- if (!(await stat32(dir)).isDirectory()) return false;
21910
+ if (!(await stat35(dir)).isDirectory()) return false;
20510
21911
  await writeFile9(probe, "");
20511
21912
  return true;
20512
21913
  } catch {
@@ -20517,12 +21918,12 @@ async function canWriteInto(dir) {
20517
21918
  }
20518
21919
 
20519
21920
  // packages/server/src/vault/serialize.ts
20520
- import { createHash as createHash4 } from "node:crypto";
21921
+ import { createHash as createHash5 } from "node:crypto";
20521
21922
  function imageKey(eventId, source, index) {
20522
21923
  return `${eventId}|${source}|${index}`;
20523
21924
  }
20524
21925
  function assetName(bytes) {
20525
- const hash = createHash4("sha256").update(bytes).digest("hex").slice(0, 32);
21926
+ const hash = createHash5("sha256").update(bytes).digest("hex").slice(0, 32);
20526
21927
  return `${hash}.${detectImageFormat(bytes)?.ext ?? "bin"}`;
20527
21928
  }
20528
21929
  function buildHeader(input) {
@@ -20609,12 +22010,12 @@ function serializeSession(input) {
20609
22010
  }
20610
22011
 
20611
22012
  // packages/server/src/vault/writer.ts
20612
- import { readdir as readdir17, rm as rm6, stat as stat34 } from "node:fs/promises";
20613
- import path56 from "node:path";
22013
+ import { readdir as readdir17, rm as rm6, stat as stat37 } from "node:fs/promises";
22014
+ import path59 from "node:path";
20614
22015
 
20615
22016
  // packages/server/src/vault/memory-copy.ts
20616
- import { mkdir as mkdir16, readdir as readdir16, readFile as readFile21, stat as stat33, utimes as utimes2 } from "node:fs/promises";
20617
- import path55 from "node:path";
22017
+ import { mkdir as mkdir16, readdir as readdir16, readFile as readFile23, stat as stat36, utimes as utimes2 } from "node:fs/promises";
22018
+ import path58 from "node:path";
20618
22019
  var MEMORY_FILE_MAX_BYTES = 1024 * 1024;
20619
22020
  var MEMORY_MAX_FILES = 500;
20620
22021
  var MEMORY_SOURCE_FILE = "source.json";
@@ -20639,7 +22040,7 @@ async function listMemoryNotes(cwd) {
20639
22040
  let absolute;
20640
22041
  try {
20641
22042
  absolute = await resolveInside(cwd, `${MEMORY_DIR}/${name}`, { mustExist: true });
20642
- const info = await stat33(absolute);
22043
+ const info = await stat36(absolute);
20643
22044
  if (!info.isFile() || info.size > MEMORY_FILE_MAX_BYTES) continue;
20644
22045
  notes.push({ name, absolute, size: info.size, mtimeMs: info.mtimeMs });
20645
22046
  } catch {
@@ -20653,7 +22054,7 @@ async function measureProjectMemory(cwd) {
20653
22054
  }
20654
22055
  async function isUpToDate(target, note) {
20655
22056
  try {
20656
- const info = await stat33(target);
22057
+ const info = await stat36(target);
20657
22058
  return info.isFile() && info.size === note.size && Math.abs(info.mtimeMs - note.mtimeMs) < SAME_MTIME_MS;
20658
22059
  } catch {
20659
22060
  return false;
@@ -20661,7 +22062,7 @@ async function isUpToDate(target, note) {
20661
22062
  }
20662
22063
  async function exists3(file) {
20663
22064
  try {
20664
- await stat33(file);
22065
+ await stat36(file);
20665
22066
  return true;
20666
22067
  } catch {
20667
22068
  return false;
@@ -20680,11 +22081,11 @@ async function copyProjectMemory(dir, cwd, platform, deps = DEFAULT_WRITE_DEPS)
20680
22081
  await mkdir16(target, { recursive: true });
20681
22082
  };
20682
22083
  for (const note of notes) {
20683
- const destination = path55.join(target, note.name);
22084
+ const destination = path58.join(target, note.name);
20684
22085
  if (await isUpToDate(destination, note)) continue;
20685
22086
  let content;
20686
22087
  try {
20687
- content = await readFile21(note.absolute);
22088
+ content = await readFile23(note.absolute);
20688
22089
  } catch {
20689
22090
  continue;
20690
22091
  }
@@ -20696,7 +22097,7 @@ async function copyProjectMemory(dir, cwd, platform, deps = DEFAULT_WRITE_DEPS)
20696
22097
  result.files += 1;
20697
22098
  result.bytes += content.length;
20698
22099
  }
20699
- const source = path55.join(target, MEMORY_SOURCE_FILE);
22100
+ const source = path58.join(target, MEMORY_SOURCE_FILE);
20700
22101
  if (result.files > 0 || !await exists3(source)) {
20701
22102
  await prepare();
20702
22103
  await writeFileAtomic(source, `${JSON.stringify({ cwd, copiedAt: deps.now() })}
@@ -21190,7 +22591,7 @@ var VaultWriter = class {
21190
22591
  }
21191
22592
  async assetOnDisk(dir, agent, sessionId, asset, bytes) {
21192
22593
  try {
21193
- const info = await stat34(assetFile(dir, agent, sessionId, asset));
22594
+ const info = await stat37(assetFile(dir, agent, sessionId, asset));
21194
22595
  return info.isFile() && info.size === bytes;
21195
22596
  } catch {
21196
22597
  return false;
@@ -21290,17 +22691,17 @@ var VaultWriter = class {
21290
22691
  const folders = [dir];
21291
22692
  const subfolders = async (parent) => {
21292
22693
  try {
21293
- return (await readdir17(parent, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => path56.join(parent, entry.name));
22694
+ return (await readdir17(parent, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => path59.join(parent, entry.name));
21294
22695
  } catch {
21295
22696
  return [];
21296
22697
  }
21297
22698
  };
21298
22699
  for (const agentDir of await subfolders(sessionsRoot2(dir))) {
21299
- if (!SESSION_AGENT_IDS.includes(path56.basename(agentDir))) continue;
22700
+ if (!SESSION_AGENT_IDS.includes(path59.basename(agentDir))) continue;
21300
22701
  folders.push(agentDir);
21301
22702
  folders.push(...(await subfolders(agentDir)).filter((folder) => folder.endsWith(".assets")));
21302
22703
  }
21303
- folders.push(...await subfolders(path56.join(dir, "memory")));
22704
+ folders.push(...await subfolders(path59.join(dir, "memory")));
21304
22705
  for (const folder of folders) {
21305
22706
  let names;
21306
22707
  try {
@@ -21310,9 +22711,9 @@ var VaultWriter = class {
21310
22711
  }
21311
22712
  for (const name of names) {
21312
22713
  if (!OWN_TEMP_FILE2.test(name)) continue;
21313
- const file = path56.join(folder, name);
22714
+ const file = path59.join(folder, name);
21314
22715
  try {
21315
- const info = await stat34(file);
22716
+ const info = await stat37(file);
21316
22717
  if (info.isFile() && now - info.mtimeMs > TEMP_MAX_AGE_MS) await rm6(file, { force: true });
21317
22718
  } catch {
21318
22719
  }
@@ -21500,14 +22901,14 @@ var VaultService = class {
21500
22901
  async setDir(target) {
21501
22902
  const { platform, agents, settings: settings2, catalog } = this.options;
21502
22903
  const current = this.dir();
21503
- const protectedDirs = [...agents.protectedDirs(), path57.join(this.options.homeDir ?? homedir14(), ".gemini")];
22904
+ const protectedDirs = [...agents.protectedDirs(), path60.join(this.options.homeDir ?? homedir14(), ".gemini")];
21504
22905
  const check = checkVaultTarget(current, target, { protectedDirs, platform });
21505
22906
  if (check.kind === "refused") throw new VaultError(check.text);
21506
22907
  if (check.kind === "same") return;
21507
22908
  try {
21508
22909
  await this.writer.exclusive("moving", async () => {
21509
22910
  if (!await canWriteInto(target)) throw new VaultError(serverText("vaultCannotWrite"));
21510
- const hadCopy = await stat35(vaultMarkerFile(current)).then(
22911
+ const hadCopy = await stat38(vaultMarkerFile(current)).then(
21511
22912
  () => true,
21512
22913
  () => false
21513
22914
  );
@@ -21541,7 +22942,7 @@ var VaultService = class {
21541
22942
  /** Abre la carpeta de la copia con el explorador del sistema. */
21542
22943
  async reveal() {
21543
22944
  const dir = this.activeDir();
21544
- const isFolder = await stat35(dir).then(
22945
+ const isFolder = await stat38(dir).then(
21545
22946
  (info) => info.isDirectory(),
21546
22947
  () => false
21547
22948
  );
@@ -21565,7 +22966,7 @@ var VaultService = class {
21565
22966
  fromCopy: true
21566
22967
  });
21567
22968
  const file = sessionExportFile(dir, agent, sessionId);
21568
- await mkdir17(path57.dirname(file), { recursive: true });
22969
+ await mkdir17(path60.dirname(file), { recursive: true });
21569
22970
  await writeFileAtomic(file, text, this.writeDeps);
21570
22971
  this.openWithSystem(file);
21571
22972
  return file;
@@ -21669,7 +23070,7 @@ var VaultService = class {
21669
23070
  continue;
21670
23071
  }
21671
23072
  const fileName = this.exportFileName(summary, names);
21672
- await writeFileAtomic(path57.join(folder, fileName), text, this.writeDeps);
23073
+ await writeFileAtomic(path60.join(folder, fileName), text, this.writeDeps);
21673
23074
  entries.push({
21674
23075
  updatedAt: summary.updatedAt,
21675
23076
  agentLabel: this.labelOf(summary.agent),
@@ -21680,9 +23081,9 @@ var VaultService = class {
21680
23081
  });
21681
23082
  await nextTurn3();
21682
23083
  }
21683
- const name = project.cwd.length > 0 ? path57.basename(project.cwd) || project.cwd : project.fallbackName;
23084
+ const name = project.cwd.length > 0 ? path60.basename(project.cwd) || project.cwd : project.fallbackName;
21684
23085
  const index = renderProjectIndex({ name, cwd: project.cwd, exportedAt: this.now(), entries, skipped });
21685
- await writeFileAtomic(path57.join(folder, "index.md"), index, this.writeDeps);
23086
+ await writeFileAtomic(path60.join(folder, "index.md"), index, this.writeDeps);
21686
23087
  this.openWithSystem(folder);
21687
23088
  return { sessions: entries.length, skipped, folder };
21688
23089
  }
@@ -21819,6 +23220,7 @@ var VaultService = class {
21819
23220
 
21820
23221
  // packages/server/src/terminal-socket.ts
21821
23222
  var NOTE_SEND_TIMEOUT_MS = 15e3;
23223
+ var REWIND_EXTRA_STEPS = 30;
21822
23224
  var NO_DEFAULTS = { model: null, effort: null, contextWindow: null };
21823
23225
  var PLAIN_INPUT = {
21824
23226
  imageReference: null,
@@ -21938,8 +23340,10 @@ function attachTerminalSocket(options) {
21938
23340
  };
21939
23341
  const onIndexStatus = (status) => broadcast({ type: "index.status", status });
21940
23342
  const onIndexProjects = (projects, replace) => broadcast({ type: "index.projects", projects, replace });
21941
- const onConversationAppend = (terminalId, events, usage, permissionMode) => {
21942
- broadcast({ type: "conversation.append", terminalId, events, usage, permissionMode });
23343
+ const onConversationAppend = (terminalId, events, usage, permissionMode, queue) => {
23344
+ broadcast(
23345
+ queue === void 0 ? { type: "conversation.append", terminalId, events, usage, permissionMode } : { type: "conversation.append", terminalId, events, usage, permissionMode, queue }
23346
+ );
21943
23347
  repos.onConversationActivity(terminalId);
21944
23348
  };
21945
23349
  const onGitStatus = (terminalId, status) => broadcast({ type: "git.status", terminalId, status });
@@ -21949,6 +23353,7 @@ function attachTerminalSocket(options) {
21949
23353
  const onConversationWaiting = (terminalId, waitingFor) => broadcast({ type: "conversation.waiting", terminalId, waitingFor });
21950
23354
  const onConversationToolCall = (terminalId, open9) => broadcast({ type: "conversation.toolCall", terminalId, open: open9 });
21951
23355
  const onConversationSubagents = (terminalId, subagents) => broadcast({ type: "conversation.subagents", terminalId, subagents });
23356
+ const onConversationQueue = (terminalId, queue) => broadcast({ type: "conversation.queue", terminalId, queue });
21952
23357
  const onConversationPlans = (terminalId, plans) => broadcast({ type: "conversation.plans", terminalId, plans });
21953
23358
  const onConversationTurns = (terminalId, turns) => broadcast({ type: "conversation.turns", terminalId, turns });
21954
23359
  const onConversationParts = (terminalId, updates) => broadcast({ type: "conversation.parts", terminalId, updates });
@@ -21959,6 +23364,76 @@ function attachTerminalSocket(options) {
21959
23364
  return await adapter.defaults(descriptor.cwd, sessionModel) ?? NO_DEFAULTS;
21960
23365
  };
21961
23366
  const pieceWriter = (socket, terminalId) => (piece) => writeToTerminal(socket, terminalId, piece);
23367
+ const interruptAgent = (socket, terminalId, options2) => {
23368
+ writeQueue.interrupt(terminalId);
23369
+ const [first, ...rest] = buildInterruptKeys(inputOf(terminalId).interruptPresses);
23370
+ if (first === void 0 || !writeToTerminal(socket, terminalId, first)) return false;
23371
+ const adapter = adapterOf(terminalId);
23372
+ if (!options2.resend && adapter?.queuedSubmits !== void 0 && adapter.promptQueue?.source === "app") {
23373
+ conversations.clearQueued(terminalId);
23374
+ }
23375
+ const reclaimFrom = adapter?.queuedSubmits;
23376
+ if (reclaimFrom !== void 0) {
23377
+ const taken = queuedSubmits.take(terminalId);
23378
+ const interruptedAt = Date.now();
23379
+ if (taken.length > 0) {
23380
+ void writeQueue.enqueue(
23381
+ terminalId,
23382
+ async (lane) => {
23383
+ const outcome = await reclaimQueuedSubmits({
23384
+ source: reclaimFrom,
23385
+ target: () => {
23386
+ const tab = registry.get(terminalId);
23387
+ return tab === null || !tab.alive || tab.sessionId.length === 0 ? null : { cwd: tab.cwd, sessionId: tab.sessionId };
23388
+ },
23389
+ submissions: taken,
23390
+ interruptedAt,
23391
+ lastClearKeyAt: () => queuedSubmits.lastClearKeyAt(terminalId),
23392
+ writeClearKey: () => {
23393
+ if (!registry.write(terminalId, CLEAR_EDITOR)) return false;
23394
+ queuedSubmits.noteClearKey(terminalId, Date.now());
23395
+ return true;
23396
+ },
23397
+ prefill: options2.resend ? async (returned) => {
23398
+ const input = inputOf(terminalId);
23399
+ const pieces = buildSubmissionWrites(returned, [], input, { send: true });
23400
+ if (pieces === null) return;
23401
+ registry.noteSubmitted(terminalId, returned);
23402
+ const written = await lane.writePieces(pieces, input.pieceGapMs, pieceWriter(socket, terminalId));
23403
+ if (written === "written") queuedSubmits.note(terminalId, returned, Date.now());
23404
+ debugLog("write", `force send to ${terminalId.slice(0, 8)}: resent, ${written}`);
23405
+ } : (returned) => send(socket, {
23406
+ type: "composer.prefill",
23407
+ terminalId,
23408
+ text: returned,
23409
+ reason: reclaimFrom.returnReason
23410
+ }),
23411
+ // La CLI deshizo el mensaje sin escribirlo (hito 44): el hilo lo esconde desde el primero.
23412
+ withdraw: (eventIds) => {
23413
+ const first2 = eventIds[0];
23414
+ if (first2 !== void 0) conversations.withdraw(terminalId, first2);
23415
+ },
23416
+ wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
23417
+ now: Date.now
23418
+ });
23419
+ debugLog("write", `queued messages of ${terminalId.slice(0, 8)} after an interrupt: ${outcome}`);
23420
+ },
23421
+ { interruptible: false }
23422
+ );
23423
+ }
23424
+ }
23425
+ if (rest.length > 0) {
23426
+ void writeQueue.enqueue(
23427
+ terminalId,
23428
+ async (lane) => {
23429
+ await new Promise((resolve) => setTimeout(resolve, ANSWER_KEY_INTERVAL_MS));
23430
+ await lane.writePieces(rest, ANSWER_KEY_INTERVAL_MS, pieceWriter(socket, terminalId));
23431
+ },
23432
+ { interruptible: false }
23433
+ );
23434
+ }
23435
+ return true;
23436
+ };
21962
23437
  const dropTab = (terminalId) => {
21963
23438
  const draftOwner = draftOwnerOf(registry.get(terminalId));
21964
23439
  if (!registry.close(terminalId)) return false;
@@ -22014,7 +23489,8 @@ function attachTerminalSocket(options) {
22014
23489
  // El de ahora y no el del snapshot: leer la configuracion es asincrono,
22015
23490
  // y un aviso que salio mientras tanto no puede quedar pisado por uno viejo.
22016
23491
  openToolCall: conversations.isToolCallOpen(terminalId),
22017
- subagents: conversations.getSubagents(terminalId)
23492
+ subagents: conversations.getSubagents(terminalId),
23493
+ queue: conversations.getQueue(terminalId)
22018
23494
  });
22019
23495
  });
22020
23496
  };
@@ -22030,6 +23506,7 @@ function attachTerminalSocket(options) {
22030
23506
  conversations.on("waiting", onConversationWaiting);
22031
23507
  conversations.on("toolCall", onConversationToolCall);
22032
23508
  conversations.on("subagents", onConversationSubagents);
23509
+ conversations.on("queue", onConversationQueue);
22033
23510
  conversations.on("turns", onConversationTurns);
22034
23511
  conversations.on("plans", onConversationPlans);
22035
23512
  conversations.on("parts", onConversationParts);
@@ -22099,6 +23576,7 @@ function attachTerminalSocket(options) {
22099
23576
  const gitSubscriptions = /* @__PURE__ */ new Set();
22100
23577
  const memorySubscriptions = /* @__PURE__ */ new Set();
22101
23578
  const pickers = new DirectoryPickers(agents.protectedDirs());
23579
+ const folders = cliFolders(agents.protectedDirs());
22102
23580
  const globalSearch = new GlobalSearchSlot();
22103
23581
  const cwdOf = (terminalId) => {
22104
23582
  const descriptor = registry.get(terminalId);
@@ -22156,7 +23634,8 @@ function attachTerminalSocket(options) {
22156
23634
  defaultCwd,
22157
23635
  shellName: appSettings.currentShell()?.name ?? null,
22158
23636
  remoteClient: deviceId !== null,
22159
- fileCheck: true
23637
+ fileCheck: true,
23638
+ fileWrite: true
22160
23639
  });
22161
23640
  send(socket, terminalListMessage());
22162
23641
  for (const entry of registry.activitySnapshot()) {
@@ -22330,11 +23809,15 @@ function attachTerminalSocket(options) {
22330
23809
  { startAfterMs }
22331
23810
  );
22332
23811
  settle(outcome === "written");
23812
+ if (outcome === "written" && message.send !== false && submitting?.promptQueue?.source === "app" && registry.activityOf(terminalId) === "busy" && !text2.trimStart().startsWith("/")) {
23813
+ conversations.noteQueued(terminalId, { text: text2, images: images.length, at: Date.now() });
23814
+ }
22333
23815
  if (outcome === "written" && message.send !== false && submitting?.queuedSubmits !== void 0) {
22334
23816
  queuedSubmits.note(
22335
23817
  terminalId,
22336
23818
  submissionPayload(fullText, imagePaths, input.imageReference) ?? fullText,
22337
- Date.now()
23819
+ Date.now(),
23820
+ fullText
22338
23821
  );
22339
23822
  }
22340
23823
  if (outcome === "interrupted") interrupted();
@@ -22505,59 +23988,156 @@ function attachTerminalSocket(options) {
22505
23988
  terminal se descarta antes, para que no caiga detras del Esc un
22506
23989
  Enter que mande lo que el usuario acaba de interrumpir.
22507
23990
  */
22508
- case "agent.interrupt": {
22509
- writeQueue.interrupt(message.terminalId);
22510
- const [first, ...rest] = buildInterruptKeys(inputOf(message.terminalId).interruptPresses);
22511
- if (first === void 0 || !writeToTerminal(socket, message.terminalId, first)) break;
22512
- const reclaimFrom = adapterOf(message.terminalId)?.queuedSubmits;
22513
- if (reclaimFrom !== void 0) {
22514
- const reclaimId = message.terminalId;
22515
- const taken = queuedSubmits.take(reclaimId);
22516
- const interruptedAt = Date.now();
22517
- if (taken.length > 0) {
22518
- void writeQueue.enqueue(
22519
- reclaimId,
22520
- async () => {
22521
- const outcome = await reclaimQueuedSubmits({
22522
- source: reclaimFrom,
22523
- target: () => {
22524
- const tab = registry.get(reclaimId);
22525
- return tab === null || !tab.alive || tab.sessionId.length === 0 ? null : { cwd: tab.cwd, sessionId: tab.sessionId };
22526
- },
22527
- submissions: taken,
22528
- interruptedAt,
22529
- lastClearKeyAt: () => queuedSubmits.lastClearKeyAt(reclaimId),
22530
- writeClearKey: () => {
22531
- if (!registry.write(reclaimId, CLEAR_EDITOR)) return false;
22532
- queuedSubmits.noteClearKey(reclaimId, Date.now());
22533
- return true;
23991
+ case "agent.interrupt":
23992
+ interruptAgent(socket, message.terminalId, { resend: false });
23993
+ break;
23994
+ /*
23995
+ "Forzar envio ahora" (hito 45, §6.35): que el agente lea ya lo que
23996
+ espera en la cola de la CLI. Es para cuando va por mal camino. Se
23997
+ comprueba todo antes de escribir nada: escribir de mas en una pty no
23998
+ se deshace. Con su tecla de "enviar ya" (Claude Code, Antigravity), por
23999
+ la fila de la pestana: no puede caer en medio de un pegado. Sin tecla
24000
+ (Pi), se detiene y se vuelve a mandar lo que la CLI devuelve.
24001
+ */
24002
+ case "agent.forceSend": {
24003
+ const { terminalId, requestId } = message;
24004
+ let replied = false;
24005
+ const reply = (sent) => {
24006
+ if (replied) return;
24007
+ replied = true;
24008
+ send(socket, { type: "agent.forceSent", terminalId, requestId, sent });
24009
+ };
24010
+ const refuse = (code, text2) => {
24011
+ sendError(socket, code, text2, void 0, requestId);
24012
+ reply(false);
24013
+ };
24014
+ const adapter = adapterOf(terminalId);
24015
+ const method = adapter !== null && adapter.capabilities.forceSend ? adapter.promptQueue?.forceSend ?? null : null;
24016
+ if (method === null) {
24017
+ refuse("agent-unsupported", serverText("forceSendUnsupported"));
24018
+ break;
24019
+ }
24020
+ const tab = registry.get(terminalId);
24021
+ if (tab === null || !tab.alive) {
24022
+ refuse("force-send-failed", serverText("forceSendNoCli"));
24023
+ break;
24024
+ }
24025
+ if (registry.activityOf(terminalId) === "waiting") {
24026
+ refuse("force-send-failed", serverText("forceSendWaiting"));
24027
+ break;
24028
+ }
24029
+ if (!conversations.hasQueue(terminalId)) {
24030
+ refuse("force-send-failed", serverText("forceSendNothing"));
24031
+ break;
24032
+ }
24033
+ conversations.markQueueForced(terminalId);
24034
+ if (method === "interrupt-and-resend") {
24035
+ if (interruptAgent(socket, terminalId, { resend: true })) reply(true);
24036
+ else refuse("force-send-failed", serverText("forceSendFailed"));
24037
+ break;
24038
+ }
24039
+ const keys = buildForceSendKeys(method);
24040
+ void writeQueue.enqueue(
24041
+ terminalId,
24042
+ async (lane) => {
24043
+ const outcome = await lane.writePieces([keys], 0, pieceWriter(socket, terminalId));
24044
+ debugLog("write", `force send to ${terminalId.slice(0, 8)}: ${method}, ${outcome}`);
24045
+ if (outcome === "written") reply(true);
24046
+ else refuse("force-send-failed", serverText("forceSendFailed"));
24047
+ },
24048
+ { interruptible: false }
24049
+ ).then(() => reply(false));
24050
+ break;
24051
+ }
24052
+ /*
24053
+ Volver a antes de un mensaje (hito 44, §4.16): la app maneja el menu de
24054
+ rewind de la CLI mirando su pantalla en cada paso, y el mensaje vuelve
24055
+ al cuadro. Por la fila de la pestana y sin dejarse cortar: un envio que
24056
+ llegara en el medio caeria como teclas sobre el menu.
24057
+ */
24058
+ case "agent.rewind": {
24059
+ const { terminalId, eventId, requestId } = message;
24060
+ const adapter = adapterOf(terminalId);
24061
+ const driver = adapter !== null && adapter.capabilities.rewind ? adapter.rewind ?? null : null;
24062
+ if (driver === null) {
24063
+ sendError(socket, "agent-unsupported", serverText("rewindUnsupported"), void 0, requestId);
24064
+ break;
24065
+ }
24066
+ const tab = registry.get(terminalId);
24067
+ if (tab === null || !tab.alive) {
24068
+ sendError(socket, "rewind-failed", serverText("rewindNoCli"), void 0, requestId);
24069
+ break;
24070
+ }
24071
+ if (registry.activityOf(terminalId) !== "idle") {
24072
+ sendError(socket, "rewind-failed", serverText("rewindBusy"), void 0, requestId);
24073
+ break;
24074
+ }
24075
+ const target = conversations.rewindTarget(terminalId, eventId);
24076
+ if (target === null) {
24077
+ sendError(socket, "rewind-failed", serverText("rewindUnknownMessage"), void 0, requestId);
24078
+ break;
24079
+ }
24080
+ const restoreCode = message.restore === "conversation-and-code";
24081
+ const session = { cwd: tab.cwd, sessionId: tab.sessionId };
24082
+ void writeQueue.enqueue(
24083
+ terminalId,
24084
+ async () => {
24085
+ const screen = registry.watchScreen(terminalId);
24086
+ if (screen === null) {
24087
+ sendError(socket, "rewind-failed", serverText("rewindNoCli"), void 0, requestId);
24088
+ return;
24089
+ }
24090
+ const seen = [];
24091
+ let outcome;
24092
+ try {
24093
+ outcome = await driver.run(
24094
+ {
24095
+ write: (data) => registry.write(terminalId, data),
24096
+ screen: async () => {
24097
+ const lines = await screen.lines();
24098
+ const text3 = lines.join("\n");
24099
+ if (seen.at(-1) !== text3) seen.push(text3);
24100
+ if (seen.length > 4) seen.shift();
24101
+ return lines;
22534
24102
  },
22535
- prefill: (returned) => send(socket, {
22536
- type: "composer.prefill",
22537
- terminalId: reclaimId,
22538
- text: returned,
22539
- reason: "returned-by-cli"
22540
- }),
22541
24103
  wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
22542
24104
  now: Date.now
22543
- });
22544
- debugLog("write", `queued messages of ${reclaimId.slice(0, 8)} after an interrupt: ${outcome}`);
22545
- },
22546
- { interruptible: false }
22547
- );
22548
- }
22549
- }
22550
- if (rest.length > 0) {
22551
- const terminalId = message.terminalId;
22552
- void writeQueue.enqueue(
22553
- terminalId,
22554
- async (lane) => {
22555
- await new Promise((resolve) => setTimeout(resolve, ANSWER_KEY_INTERVAL_MS));
22556
- await lane.writePieces(rest, ANSWER_KEY_INTERVAL_MS, pieceWriter(socket, terminalId));
22557
- },
22558
- { interruptible: false }
22559
- );
22560
- }
24105
+ },
24106
+ {
24107
+ firstLine: target.firstLine,
24108
+ occurrence: Math.max(1, target.occurrence),
24109
+ maxSteps: target.promptsFromHere + REWIND_EXTRA_STEPS,
24110
+ restoreCode
24111
+ }
24112
+ );
24113
+ } catch (error) {
24114
+ console.warn(`[rewind] ${terminalId.slice(0, 8)}:`, error);
24115
+ outcome = { kind: "failed", reason: "gone" };
24116
+ } finally {
24117
+ screen.dispose();
24118
+ }
24119
+ debugLog("write", `rewind of ${terminalId.slice(0, 8)}: ${JSON.stringify(outcome)}`);
24120
+ if (outcome.kind === "failed") debugLog("rewind", `last screens:
24121
+ ${seen.join("\n-----\n")}`);
24122
+ if (outcome.kind === "failed") {
24123
+ sendError(
24124
+ socket,
24125
+ "rewind-failed",
24126
+ outcome.reason === "gone" ? serverText("rewindNoCli") : serverText("rewindFailed"),
24127
+ outcome.reason,
24128
+ requestId
24129
+ );
24130
+ return;
24131
+ }
24132
+ const text2 = target.truncated ? await driver.fullText(session, eventId) ?? target.text : target.text;
24133
+ if (text2.trim().length > 0) {
24134
+ send(socket, { type: "composer.prefill", terminalId, text: text2, reason: "rewound" });
24135
+ }
24136
+ conversations.withdraw(terminalId, eventId);
24137
+ send(socket, { type: "agent.rewound", terminalId, requestId, codeRestored: outcome.codeRestored });
24138
+ },
24139
+ { interruptible: false }
24140
+ );
22561
24141
  break;
22562
24142
  }
22563
24143
  case "resize":
@@ -22705,7 +24285,8 @@ function attachTerminalSocket(options) {
22705
24285
  defaults: await defaultsFor(message.terminalId, snapshot.usage.lastModel),
22706
24286
  waitingFor: snapshot.waitingFor,
22707
24287
  openToolCall: conversations.isToolCallOpen(message.terminalId),
22708
- subagents: conversations.getSubagents(message.terminalId)
24288
+ subagents: conversations.getSubagents(message.terminalId),
24289
+ queue: conversations.getQueue(message.terminalId)
22709
24290
  });
22710
24291
  if (snapshot.plans.length > 0) {
22711
24292
  send(socket, {
@@ -22918,7 +24499,7 @@ function attachTerminalSocket(options) {
22918
24499
  return;
22919
24500
  }
22920
24501
  try {
22921
- const listing = await listDirectory(cwd, message.path, {
24502
+ const listing = await listDirectory(cwd, message.path, await folders, {
22922
24503
  includeHidden: message.includeHidden === true
22923
24504
  });
22924
24505
  send(socket, { type: "files.listing", terminalId: message.terminalId, listing });
@@ -22941,7 +24522,7 @@ function attachTerminalSocket(options) {
22941
24522
  return;
22942
24523
  }
22943
24524
  try {
22944
- const result = await searchFiles(cwd, message.query, {
24525
+ const result = await searchFiles(cwd, message.query, await folders, {
22945
24526
  includeHidden: message.includeHidden === true
22946
24527
  });
22947
24528
  send(socket, { type: "files.results", terminalId: message.terminalId, result });
@@ -22958,13 +24539,54 @@ function attachTerminalSocket(options) {
22958
24539
  return;
22959
24540
  }
22960
24541
  try {
22961
- const preview = await readPreview(cwd, message.path);
24542
+ const preview = await readPreview(cwd, message.path, await folders);
22962
24543
  send(socket, { type: "files.preview", terminalId: message.terminalId, preview });
22963
24544
  } catch (error) {
22964
24545
  sendPathError(error, serverText("fileReadFailed"));
22965
24546
  }
22966
24547
  })();
22967
24548
  break;
24549
+ /*
24550
+ Guardar un archivo editado en el visor (Hito 46, §6.36): la segunda
24551
+ escritura de la app dentro de un proyecto. Lo que llega es la ruta
24552
+ relativa, el texto y la huella de lo que se abrio; que se puede
24553
+ escribir lo decide `saveProjectFile`. Un remoto tambien guarda: ya
24554
+ puede pedirle al agente que lo haga (§14.4).
24555
+ */
24556
+ case "files.write":
24557
+ void (async () => {
24558
+ const { requestId } = message;
24559
+ const cwd = cwdOf(message.terminalId);
24560
+ if (cwd === null) {
24561
+ sendError(socket, "unknown-terminal", serverText("terminalGone"), void 0, requestId);
24562
+ return;
24563
+ }
24564
+ try {
24565
+ const result = await saveProjectFile(cwd, message.path, message.text, message.baseHash, await folders);
24566
+ send(socket, {
24567
+ type: "files.saved",
24568
+ terminalId: message.terminalId,
24569
+ requestId,
24570
+ path: message.path,
24571
+ outcome: result.outcome,
24572
+ preview: result.preview
24573
+ });
24574
+ } catch (error) {
24575
+ if (error instanceof InvalidPathError) {
24576
+ sendError(socket, "invalid-path", error.text, void 0, requestId);
24577
+ return;
24578
+ }
24579
+ const detail = error instanceof Error ? error.message : String(error);
24580
+ sendError(
24581
+ socket,
24582
+ "write-failed",
24583
+ error instanceof FileWriteError ? error.text : serverText("fileSaveFailed"),
24584
+ detail,
24585
+ requestId
24586
+ );
24587
+ }
24588
+ })();
24589
+ break;
22968
24590
  /*
22969
24591
  Cuales de esas rutas son archivos del proyecto (§6.33): lo que el
22970
24592
  hilo vuelve enlace. Sin error para ningun caso —una pestana que ya no
@@ -22975,7 +24597,7 @@ function attachTerminalSocket(options) {
22975
24597
  case "files.check":
22976
24598
  void (async () => {
22977
24599
  const cwd = cwdOf(message.terminalId);
22978
- const existing = cwd === null ? [] : await existingFiles(cwd, message.paths).catch(() => []);
24600
+ const existing = cwd === null ? [] : await existingFiles(cwd, message.paths, await folders).catch(() => []);
22979
24601
  send(socket, { type: "files.checked", terminalId: message.terminalId, requestId: message.requestId, existing });
22980
24602
  })();
22981
24603
  break;
@@ -22987,7 +24609,9 @@ function attachTerminalSocket(options) {
22987
24609
  return;
22988
24610
  }
22989
24611
  try {
22990
- revealPath(await resolveInside(cwd, message.path, { mustExist: true }));
24612
+ const absolute = await resolveInside(cwd, message.path, { mustExist: true });
24613
+ if (await (await folders).contains(absolute)) throw new InvalidPathError(serverText("cliFolderClosed"));
24614
+ revealPath(absolute);
22991
24615
  } catch (error) {
22992
24616
  sendPathError(error, serverText("pathOpenFailed"));
22993
24617
  }
@@ -23407,6 +25031,7 @@ function attachTerminalSocket(options) {
23407
25031
  conversations.off("waiting", onConversationWaiting);
23408
25032
  conversations.off("toolCall", onConversationToolCall);
23409
25033
  conversations.off("subagents", onConversationSubagents);
25034
+ conversations.off("queue", onConversationQueue);
23410
25035
  repos.off("status", onGitStatus);
23411
25036
  memory.off("status", onMemoryStatus);
23412
25037
  stopAgentChanges();
@@ -23420,9 +25045,9 @@ function attachTerminalSocket(options) {
23420
25045
  }
23421
25046
 
23422
25047
  // packages/server/src/vault/catalog.ts
23423
- import { open as open8, readdir as readdir18, stat as stat36 } from "node:fs/promises";
23424
- import path58 from "node:path";
23425
- import { createInterface as createInterface3 } from "node:readline";
25048
+ import { open as open8, readdir as readdir18, stat as stat39 } from "node:fs/promises";
25049
+ import path61 from "node:path";
25050
+ import { createInterface as createInterface4 } from "node:readline";
23426
25051
  var HEADER_MAX_BYTES = 64 * 1024;
23427
25052
  var LOAD_CONCURRENCY = 16;
23428
25053
  var keyOf2 = (agent, sessionId) => `${agent}/${sessionId}`;
@@ -23441,7 +25066,7 @@ async function assetBytesOf(dir, agent, sessionId) {
23441
25066
  for (const name of names) {
23442
25067
  if (!VAULT_ASSET_NAME_PATTERN.test(name)) continue;
23443
25068
  try {
23444
- const info = await stat36(path58.join(folder, name));
25069
+ const info = await stat39(path61.join(folder, name));
23445
25070
  if (info.isFile()) total += info.size;
23446
25071
  } catch {
23447
25072
  }
@@ -23452,7 +25077,7 @@ async function readEntry2(dir, agent, sessionId) {
23452
25077
  const file = sessionFile(dir, agent, sessionId);
23453
25078
  let sizeBytes;
23454
25079
  try {
23455
- const info = await stat36(file);
25080
+ const info = await stat39(file);
23456
25081
  if (!info.isFile()) return { kind: "absent" };
23457
25082
  sizeBytes = info.size;
23458
25083
  } catch {
@@ -23609,7 +25234,7 @@ var VaultCatalog = class {
23609
25234
  const handle = await open8(sessionFile(dir, agent, sessionId), "r").catch(() => null);
23610
25235
  if (handle === null) return;
23611
25236
  const input = handle.createReadStream({ encoding: "utf8" });
23612
- const lines = createInterface3({ input, crlfDelay: Number.POSITIVE_INFINITY });
25237
+ const lines = createInterface4({ input, crlfDelay: Number.POSITIVE_INFINITY });
23613
25238
  let sawHeader = false;
23614
25239
  try {
23615
25240
  for await (const raw of lines) {
@@ -23661,15 +25286,15 @@ var VaultCatalog = class {
23661
25286
  };
23662
25287
 
23663
25288
  // packages/server/src/index.ts
23664
- var serverDir = path59.dirname(fileURLToPath2(import.meta.url));
25289
+ var serverDir = path62.dirname(fileURLToPath2(import.meta.url));
23665
25290
  var isPackaged = true;
23666
- var webRoot = isPackaged ? serverDir : path59.resolve(serverDir, "../../web");
23667
- var webDist = isPackaged ? path59.join(serverDir, "web") : path59.join(webRoot, "dist");
23668
- var repoRoot = path59.resolve(serverDir, "../../..");
25291
+ var webRoot = isPackaged ? serverDir : path62.resolve(serverDir, "../../web");
25292
+ var webDist = isPackaged ? path62.join(serverDir, "web") : path62.join(webRoot, "dist");
25293
+ var repoRoot = path62.resolve(serverDir, "../../..");
23669
25294
  var isProduction = isPackaged || process.argv.includes("--prod");
23670
25295
  function resolveDefaultCwd() {
23671
25296
  const override = process.env["AGENT_WORKBENCH_CWD"];
23672
- if (override !== void 0 && override.length > 0) return path59.resolve(override);
25297
+ if (override !== void 0 && override.length > 0) return path62.resolve(override);
23673
25298
  if (isPackaged) return process.cwd();
23674
25299
  return repoRoot;
23675
25300
  }
@@ -23744,7 +25369,7 @@ async function mountDevUi(app, httpServer) {
23744
25369
  }
23745
25370
  var LOOKS_LIKE_FILE = /\.[a-zA-Z0-9]{1,8}$/;
23746
25371
  function mountBuiltUi(app) {
23747
- const indexHtml = path59.join(webDist, "index.html");
25372
+ const indexHtml = path62.join(webDist, "index.html");
23748
25373
  if (!existsSync4(indexHtml)) {
23749
25374
  throw new Error(
23750
25375
  `There's no compiled interface in ${webDist}.
@@ -23886,6 +25511,7 @@ async function main() {
23886
25511
  void pasteStore.purgeStale();
23887
25512
  void index.start();
23888
25513
  const stopWatching = watchSessions(index, conversations, agents);
25514
+ conversations.on("historyGrew", (agent, filePath) => void index.refreshPath(agent, filePath));
23889
25515
  if (process.env["AGENT_WORKBENCH_NO_RESTORE"] !== "1") {
23890
25516
  void store.load().then((state) => {
23891
25517
  drafts.retainOnly(workspaceDraftOwners(state));