agent-workbench 0.8.1 → 0.9.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 path75 from "node:path";
6
+ import path77 from "node:path";
7
7
  import { fileURLToPath as fileURLToPath2 } from "node:url";
8
8
  import express from "express";
9
9
 
@@ -83,6 +83,27 @@ function serverText(key, ...params) {
83
83
  const [values] = params;
84
84
  return values === void 0 ? { key } : { key, params: values };
85
85
  }
86
+ function parseServerText(value, depth = 0) {
87
+ const record = asRecord(value);
88
+ if (record === null) return null;
89
+ const key = asString(record["key"]);
90
+ if (key === null || key.length === 0) return null;
91
+ const rawParams = record["params"];
92
+ if (rawParams === void 0) return { key };
93
+ const paramsRecord = asRecord(rawParams);
94
+ if (paramsRecord === null) return null;
95
+ const params = {};
96
+ for (const [name, param] of Object.entries(paramsRecord)) {
97
+ if (typeof param === "string" || typeof param === "number" && Number.isFinite(param)) {
98
+ params[name] = param;
99
+ continue;
100
+ }
101
+ const nested = depth === 0 ? parseServerText(param, depth + 1) : null;
102
+ if (nested === null) return null;
103
+ params[name] = nested;
104
+ }
105
+ return { key, params };
106
+ }
86
107
  var ServerTextError = class extends Error {
87
108
  text;
88
109
  constructor(text, options) {
@@ -241,6 +262,9 @@ function sameQuestionItem(a, b) {
241
262
  return left.length > 0 && right.length > 0 && (left.startsWith(right) || right.startsWith(left));
242
263
  });
243
264
  }
265
+ function askedBeforeLaunch(askedAt, launchedAt) {
266
+ return askedAt > 0 && launchedAt !== null && askedAt < launchedAt;
267
+ }
244
268
  var CONVERSATION_IMAGE_SOURCES = [
245
269
  "content",
246
270
  "attachment",
@@ -498,6 +522,17 @@ function parseComposerDraft(value) {
498
522
  return { text, pasted: pasted2 };
499
523
  }
500
524
 
525
+ // packages/shared/src/scheduled-messages.ts
526
+ var MAX_SCHEDULED_MESSAGES = 50;
527
+ var MAX_SCHEDULED_TEXT_CHARS = 2e5;
528
+ var MAX_SCHEDULE_AHEAD_MS = 30 * 24 * 60 * 60 * 1e3;
529
+ var SCHEDULE_LATE_LIMIT_MS = 60 * 60 * 1e3;
530
+ function scheduleTimeProblem(sendAt, now) {
531
+ if (!Number.isFinite(sendAt) || sendAt <= now) return "past";
532
+ if (sendAt - now > MAX_SCHEDULE_AHEAD_MS) return "too-far";
533
+ return null;
534
+ }
535
+
501
536
  // packages/shared/src/memory.ts
502
537
  var MEMORY_DIR = ".agents/memory";
503
538
  var MEMORY_INDEX_FILE = "MEMORY.md";
@@ -857,13 +892,16 @@ function asAgentIdList(value) {
857
892
  function parseAppSettingsChange(record) {
858
893
  const rawShell = record["consoleShell"];
859
894
  const rawAgents = record["disabledAgents"];
895
+ const rawScheduled = record["scheduledMessages"];
860
896
  const consoleShell = rawShell === void 0 ? void 0 : asConsoleShellId(rawShell);
861
897
  const disabledAgents = rawAgents === void 0 ? void 0 : asAgentIdList(rawAgents);
862
- if (consoleShell === null || disabledAgents === null) return null;
863
- if (consoleShell === void 0 && disabledAgents === void 0) return null;
898
+ const scheduledMessages = rawScheduled === void 0 ? void 0 : typeof rawScheduled === "boolean" ? rawScheduled : null;
899
+ if (consoleShell === null || disabledAgents === null || scheduledMessages === null) return null;
900
+ if (consoleShell === void 0 && disabledAgents === void 0 && scheduledMessages === void 0) return null;
864
901
  return {
865
902
  ...consoleShell !== void 0 ? { consoleShell } : {},
866
- ...disabledAgents !== void 0 ? { disabledAgents } : {}
903
+ ...disabledAgents !== void 0 ? { disabledAgents } : {},
904
+ ...scheduledMessages !== void 0 ? { scheduledMessages } : {}
867
905
  };
868
906
  }
869
907
 
@@ -1300,6 +1338,21 @@ function parseClientMessage(raw) {
1300
1338
  const draft = parseComposerDraft(record["draft"]);
1301
1339
  return terminalId === null || draft === null ? null : { type: "composer.draft", terminalId, draft };
1302
1340
  }
1341
+ case "schedule.create": {
1342
+ const requestId = asNonEmptyString(record["requestId"]);
1343
+ const terminalId = asNonEmptyString(record["terminalId"]);
1344
+ const text = asString(record["text"]);
1345
+ const sendAt = asFiniteNumber(record["sendAt"]);
1346
+ return requestId === null || terminalId === null || text === null || sendAt === null ? null : { type: "schedule.create", requestId, terminalId, text, sendAt };
1347
+ }
1348
+ case "schedule.cancel": {
1349
+ const id = asNonEmptyString(record["id"]);
1350
+ return id === null ? null : { type: "schedule.cancel", id };
1351
+ }
1352
+ case "schedule.sendNow": {
1353
+ const id = asNonEmptyString(record["id"]);
1354
+ return id === null ? null : { type: "schedule.sendNow", id };
1355
+ }
1303
1356
  case "settings.update": {
1304
1357
  const change = parseAppSettingsChange(record);
1305
1358
  return change === null ? null : { type: "settings.update", ...change };
@@ -1382,8 +1435,8 @@ function parseClientMessage(raw) {
1382
1435
  case "worktree.changesDiff": {
1383
1436
  const requestId = asNonEmptyString(record["requestId"]);
1384
1437
  const worktreeId = asNonEmptyString(record["worktreeId"]);
1385
- const path76 = asNonEmptyString(record["path"]);
1386
- return requestId === null || worktreeId === null || path76 === null ? null : { type: "worktree.changesDiff", requestId, worktreeId, path: path76 };
1438
+ const path78 = asNonEmptyString(record["path"]);
1439
+ return requestId === null || worktreeId === null || path78 === null ? null : { type: "worktree.changesDiff", requestId, worktreeId, path: path78 };
1387
1440
  }
1388
1441
  case "worktree.requestReview": {
1389
1442
  const requestId = asNonEmptyString(record["requestId"]);
@@ -1521,6 +1574,15 @@ function notesImagesDir() {
1521
1574
  function composerDraftsPath() {
1522
1575
  return path.join(appConfigDir(), "composer-drafts.json");
1523
1576
  }
1577
+ function scheduledMessagesPath() {
1578
+ return path.join(appConfigDir(), "scheduled-messages.json");
1579
+ }
1580
+ function scheduledClaimsDir() {
1581
+ return path.join(appConfigDir(), "scheduled-claims");
1582
+ }
1583
+ function runningInstancesDir() {
1584
+ return path.join(appConfigDir(), "instances");
1585
+ }
1524
1586
  function catalogTempDir() {
1525
1587
  return path.join(tmpdir(), "agent-workbench", "agy-catalog");
1526
1588
  }
@@ -2970,9 +3032,9 @@ function toTitle(raw) {
2970
3032
  return text;
2971
3033
  }
2972
3034
  function withoutPastedText(raw) {
2973
- const normalized = raw.replace(/\r\n?/g, "\n");
2974
- const typed = normalized.replace(pastedBlockPattern(), "$1");
2975
- return typed.trim().length > 0 ? typed : normalized.replace(pastedBoundaryPattern(), " ");
3035
+ const normalized2 = raw.replace(/\r\n?/g, "\n");
3036
+ const typed = normalized2.replace(pastedBlockPattern(), "$1");
3037
+ return typed.trim().length > 0 ? typed : normalized2.replace(pastedBoundaryPattern(), " ");
2976
3038
  }
2977
3039
 
2978
3040
  // packages/server/src/agents/antigravity/session-follower.ts
@@ -3240,12 +3302,12 @@ function blockPattern(name) {
3240
3302
  return new RegExp(`<${name}>([\\s\\S]*?)</${name}>`, "g");
3241
3303
  }
3242
3304
  function unwrapUserInput(content) {
3243
- const open9 = content.indexOf(REQUEST_OPEN);
3244
- const close = open9 === -1 ? -1 : content.indexOf(REQUEST_CLOSE, open9 + REQUEST_OPEN.length);
3305
+ const open10 = content.indexOf(REQUEST_OPEN);
3306
+ const close = open10 === -1 ? -1 : content.indexOf(REQUEST_CLOSE, open10 + REQUEST_OPEN.length);
3245
3307
  let text;
3246
3308
  let rest;
3247
- if (open9 !== -1 && close !== -1) {
3248
- text = content.slice(open9 + REQUEST_OPEN.length, close).trim();
3309
+ if (open10 !== -1 && close !== -1) {
3310
+ text = content.slice(open10 + REQUEST_OPEN.length, close).trim();
3249
3311
  rest = content.slice(close + REQUEST_CLOSE.length);
3250
3312
  } else {
3251
3313
  text = content.replace(blockPattern("ADDITIONAL_METADATA"), "").replace(blockPattern("USER_SETTINGS_CHANGE"), "").trim();
@@ -4068,6 +4130,15 @@ function createAntigravityQueueEdit() {
4068
4130
  return { run: (io, context) => runAntigravityQueueEdit(io, context) };
4069
4131
  }
4070
4132
 
4133
+ // packages/server/src/agents/antigravity/ready-screen.ts
4134
+ function antigravityReadyScreen(lines) {
4135
+ const screen = readAntigravityQueueScreen(lines);
4136
+ if (screen === null || screen.queued) return false;
4137
+ const tail = lines.filter((line) => line.trim() !== "").slice(-3);
4138
+ if (tail.some((line) => /esc to cancel|Generating/.test(line))) return false;
4139
+ return tail.some((line) => /^\s*\? for shortcuts/.test(line));
4140
+ }
4141
+
4071
4142
  // packages/server/src/agents/antigravity/settings.ts
4072
4143
  import { readFile as readFile4, stat as stat7 } from "node:fs/promises";
4073
4144
  var SETTINGS_MAX_BYTES = 256 * 1024;
@@ -4652,6 +4723,8 @@ var ANTIGRAVITY_BASE_CAPABILITIES = {
4652
4723
  forceSend: true,
4653
4724
  // La flecha arriba saca la cola al editor, y Ctrl+U Ctrl+K lo vacian sin cortar el turno (1.4.4, §6.35.1).
4654
4725
  editQueue: true,
4726
+ // Hito 52: sin status line no se sabe si trabaja, y no se le programa nada.
4727
+ scheduledSend: false,
4655
4728
  /*
4656
4729
  Hito 49: `--mode plan`. Medido con la 1.4.0 al lanzar: arranca en "Plan
4657
4730
  mode: research & plan only" y el ciclo sigue desde ahi. Que hace con
@@ -4663,7 +4736,13 @@ var ANTIGRAVITY_STATUS_LINE_CAPABILITIES = {
4663
4736
  ...ANTIGRAVITY_BASE_CAPABILITIES,
4664
4737
  statusSource: true,
4665
4738
  readySignal: READY_SIGNAL_SAFE,
4666
- contextWindowSource: "status-line"
4739
+ contextWindowSource: "status-line",
4740
+ /*
4741
+ Hito 52 (§12.17): con la status line si. Al retomar una conversacion, el
4742
+ primer "libre" escrito despues de lanzarla llego cuando su pantalla ya
4743
+ aceptaba un pegado (3 de 3), y la caja vacia lo confirma.
4744
+ */
4745
+ scheduledSend: true
4667
4746
  };
4668
4747
  var ANTIGRAVITY_INPUT = {
4669
4748
  imageReference: null,
@@ -4858,6 +4937,8 @@ function createAntigravityAdapter(options = {}) {
4858
4937
  es lo que la app le mando con el agente trabajando. "Enviar ya" es Alt+M.
4859
4938
  */
4860
4939
  promptQueue: { source: "app", forceSend: "alt-m" },
4940
+ // Hito 52 (§12.17): con la status line, que dice si trabaja; lo escrito se confirma cuando pasa a trabajando.
4941
+ scheduled: { kind: "screen", readyScreen: antigravityReadyScreen, confirmTimeoutMs: 6e4 },
4861
4942
  // Sacar la cola para editarla en el cuadro (§6.35.1): vuelve lo que la app le mando.
4862
4943
  queueEdit: createAntigravityQueueEdit(),
4863
4944
  /**
@@ -5087,6 +5168,17 @@ async function readAgentDefaults(cwd, sessionModel = null) {
5087
5168
  import { readdir as readdir7, readFile as readFile8 } from "node:fs/promises";
5088
5169
  import path14 from "node:path";
5089
5170
 
5171
+ // packages/server/src/process-alive.ts
5172
+ function processAlive(pid) {
5173
+ if (!Number.isInteger(pid) || pid <= 0) return false;
5174
+ try {
5175
+ process.kill(pid, 0);
5176
+ return true;
5177
+ } catch (error) {
5178
+ return error.code === "EPERM";
5179
+ }
5180
+ }
5181
+
5090
5182
  // packages/server/src/agents/claude-code/paths.ts
5091
5183
  import { homedir as homedir4, tmpdir as tmpdir2 } from "node:os";
5092
5184
  import path13 from "node:path";
@@ -5127,7 +5219,16 @@ function readStatus(value) {
5127
5219
  statusUpdatedAt: typeof updatedAt === "number" && Number.isFinite(updatedAt) ? updatedAt : null
5128
5220
  };
5129
5221
  }
5222
+ function fileCounts(record, isAlive) {
5223
+ const pid = record["pid"];
5224
+ return typeof pid !== "number" || isAlive(pid);
5225
+ }
5130
5226
  var CliStatusWatcher = class {
5227
+ /** Inyectable para el chequeo, que no puede inventar procesos vivos. */
5228
+ constructor(isAlive = processAlive) {
5229
+ this.isAlive = isAlive;
5230
+ }
5231
+ isAlive;
5131
5232
  subscriptions = /* @__PURE__ */ new Set();
5132
5233
  timer = null;
5133
5234
  reading = false;
@@ -5176,6 +5277,34 @@ var CliStatusWatcher = class {
5176
5277
  await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
5177
5278
  }
5178
5279
  }
5280
+ /**
5281
+ * true si algun proceso vivo de la CLI tiene esa conversacion (hito 51,
5282
+ * §6.39). Cuenta tambien el archivo que todavia no escribio su estado: la CLI
5283
+ * lo crea antes de decir si esta libre (medido con la 2.1.310), y ese proceso
5284
+ * ya tiene la conversacion abierta.
5285
+ */
5286
+ async isOpen(sessionId) {
5287
+ const root = cliSessionsStateRoot();
5288
+ let names;
5289
+ try {
5290
+ names = await readdir7(root);
5291
+ } catch {
5292
+ return false;
5293
+ }
5294
+ for (const name of names) {
5295
+ if (!name.endsWith(".json")) continue;
5296
+ let parsed;
5297
+ try {
5298
+ parsed = JSON.parse(await readFile8(path14.join(root, name), "utf8"));
5299
+ } catch {
5300
+ continue;
5301
+ }
5302
+ if (typeof parsed !== "object" || parsed === null) continue;
5303
+ const record = parsed;
5304
+ if (record["sessionId"] === sessionId && fileCounts(record, this.isAlive)) return true;
5305
+ }
5306
+ return false;
5307
+ }
5179
5308
  dispose() {
5180
5309
  this.subscriptions.clear();
5181
5310
  this.stop();
@@ -5214,8 +5343,10 @@ var CliStatusWatcher = class {
5214
5343
  continue;
5215
5344
  }
5216
5345
  if (typeof parsed !== "object" || parsed === null) continue;
5217
- const sessionId = parsed["sessionId"];
5346
+ const record = parsed;
5347
+ const sessionId = record["sessionId"];
5218
5348
  if (typeof sessionId !== "string" || sessionId.length === 0) continue;
5349
+ if (!fileCounts(record, this.isAlive)) continue;
5219
5350
  const status = readStatus(parsed);
5220
5351
  if (status !== null) found.set(sessionId, status);
5221
5352
  }
@@ -5896,11 +6027,11 @@ function relativeInside(root, absolute) {
5896
6027
  }
5897
6028
  function planFileNameOf(value) {
5898
6029
  if (typeof value !== "string" || value.length === 0) return null;
5899
- const normalized = value.replace(/\\/g, "/");
6030
+ const normalized2 = value.replace(/\\/g, "/");
5900
6031
  const marker = "/.claude/plans/";
5901
- const at = normalized.toLowerCase().lastIndexOf(marker);
6032
+ const at = normalized2.toLowerCase().lastIndexOf(marker);
5902
6033
  if (at === -1) return null;
5903
- const name = normalized.slice(at + marker.length);
6034
+ const name = normalized2.slice(at + marker.length);
5904
6035
  if (name.length === 0 || name.includes("/")) return null;
5905
6036
  if (name === "." || name === "..") return null;
5906
6037
  return name;
@@ -5980,18 +6111,18 @@ var BranchTracker = class {
5980
6111
  }
5981
6112
  /** La linea y todas las de las que cuelga. */
5982
6113
  pathFrom(leaf) {
5983
- const path76 = /* @__PURE__ */ new Set();
6114
+ const path78 = /* @__PURE__ */ new Set();
5984
6115
  let current = leaf;
5985
- while (current !== null && !path76.has(current)) {
6116
+ while (current !== null && !path78.has(current)) {
5986
6117
  const node = this.nodes.get(current);
5987
6118
  if (node === void 0) break;
5988
- path76.add(current);
6119
+ path78.add(current);
5989
6120
  current = node.parent;
5990
6121
  }
5991
- return path76;
6122
+ return path78;
5992
6123
  }
5993
6124
  recompute() {
5994
- const path76 = this.pathFrom(this.leaf());
6125
+ const path78 = this.pathFrom(this.leaf());
5995
6126
  const children = /* @__PURE__ */ new Map();
5996
6127
  for (const [uuid, node] of this.nodes) {
5997
6128
  const key = node.parent !== null && this.nodes.has(node.parent) ? node.parent : null;
@@ -6000,9 +6131,9 @@ var BranchTracker = class {
6000
6131
  else list.push(uuid);
6001
6132
  }
6002
6133
  const roots = [];
6003
- for (const uuid of children.get(null) ?? []) if (!path76.has(uuid)) roots.push(uuid);
6004
- for (const uuid of path76) {
6005
- for (const child of children.get(uuid) ?? []) if (!path76.has(child)) roots.push(child);
6134
+ for (const uuid of children.get(null) ?? []) if (!path78.has(uuid)) roots.push(uuid);
6135
+ for (const uuid of path78) {
6136
+ for (const child of children.get(uuid) ?? []) if (!path78.has(child)) roots.push(child);
6006
6137
  }
6007
6138
  const hidden = /* @__PURE__ */ new Set();
6008
6139
  for (const root of roots) {
@@ -8019,6 +8150,12 @@ var CLAUDE_CODE_CAPABILITIES = {
8019
8150
  forceSend: true,
8020
8151
  // La flecha arriba saca la cola a su caja, y Ctrl+U Ctrl+K la vacian sin cortar el turno (2.1.309, §6.35.1).
8021
8152
  editQueue: true,
8153
+ /*
8154
+ Hito 51: su archivo de `sessions/` dice si esta libre, si otro proceso tiene
8155
+ la conversacion (con el pid vivo: el de una CLI matada queda, medido con la
8156
+ 2.1.310) y cuando quedo lista despues de abrirla (§6.39).
8157
+ */
8158
+ scheduledSend: true,
8022
8159
  /*
8023
8160
  Hito 49: `--permission-mode plan`. Medido con la 2.1.304 y el pedido de
8024
8161
  revision: corrio git log, diff y status sin pedir permiso, no ofrecio salir
@@ -8052,6 +8189,8 @@ function createStatusSource(watcher) {
8052
8189
  )
8053
8190
  ),
8054
8191
  waitUntilReady: (sessionId, timeoutMs) => watcher.waitUntilIdle(sessionId, timeoutMs),
8192
+ // Hito 51: antes de abrir una pestana dormida para un mensaje programado.
8193
+ isOpen: (sessionId) => watcher.isOpen(sessionId),
8055
8194
  dispose: () => watcher.dispose()
8056
8195
  };
8057
8196
  }
@@ -8672,9 +8811,9 @@ var CodexSessionDiscovery = class {
8672
8811
  onInput: () => void 0,
8673
8812
  onSubmitted: (text) => {
8674
8813
  if (released || this.pending.get(terminalId) !== tab) return;
8675
- const normalized = normalizeMessageText(text);
8676
- if (normalized.length === 0) return;
8677
- tab.submitted.push(normalized);
8814
+ const normalized2 = normalizeMessageText(text);
8815
+ if (normalized2.length === 0) return;
8816
+ tab.submitted.push(normalized2);
8678
8817
  if (tab.submitted.length > MAX_SUBMITTED) tab.submitted.splice(0, tab.submitted.length - MAX_SUBMITTED);
8679
8818
  }
8680
8819
  };
@@ -9089,6 +9228,13 @@ var CodexRolloutSink = class {
9089
9228
  turns = [];
9090
9229
  /** Llamadas del turno en curso sin resultado todavia: `call_id` -> epoch ms. */
9091
9230
  openCalls = /* @__PURE__ */ new Map();
9231
+ /**
9232
+ * Desde cuando hay un turno abierto (epoch ms; 0 si su linea no trae hora), o
9233
+ * null. Hito 52: Codex no publica si trabaja, y el reloj de los mensajes
9234
+ * programados no le escribe con un turno abierto (§10.13: le cortaria la
9235
+ * respuesta).
9236
+ */
9237
+ turnOpenSince = null;
9092
9238
  warnedForeignMeta = false;
9093
9239
  getUsage() {
9094
9240
  return this.usage;
@@ -9114,7 +9260,13 @@ var CodexRolloutSink = class {
9114
9260
  }
9115
9261
  return false;
9116
9262
  }
9263
+ /** Ver `SessionFollower.hasOpenTurn`. Un turno sin hora no se sabe de que proceso es: cuenta. */
9264
+ hasOpenTurn(launchedAt) {
9265
+ if (this.turnOpenSince === null) return false;
9266
+ return this.turnOpenSince === 0 || this.turnOpenSince >= launchedAt;
9267
+ }
9117
9268
  reset() {
9269
+ this.turnOpenSince = null;
9118
9270
  this.model = null;
9119
9271
  this.effort = null;
9120
9272
  this.window = null;
@@ -9196,6 +9348,7 @@ var CodexRolloutSink = class {
9196
9348
  if (TURN_START_TYPES.includes(kind)) {
9197
9349
  this.lastAssistantEventId = null;
9198
9350
  this.openCalls.clear();
9351
+ this.turnOpenSince = at;
9199
9352
  const window = readWindow(payload["model_context_window"]);
9200
9353
  if (window !== null) {
9201
9354
  this.window = window;
@@ -9217,6 +9370,7 @@ var CodexRolloutSink = class {
9217
9370
  }
9218
9371
  if (TURN_END_TYPES.includes(kind)) {
9219
9372
  this.openCalls.clear();
9373
+ this.turnOpenSince = null;
9220
9374
  const duration = payload["duration_ms"];
9221
9375
  const eventId = this.lastAssistantEventId;
9222
9376
  this.lastAssistantEventId = null;
@@ -9450,6 +9604,9 @@ var CodexSessionFollower = class {
9450
9604
  hasOpenToolCall(launchedAt) {
9451
9605
  return this.sink.hasOpenToolCall(launchedAt);
9452
9606
  }
9607
+ hasOpenTurn(launchedAt) {
9608
+ return this.sink.hasOpenTurn(launchedAt);
9609
+ }
9453
9610
  getState() {
9454
9611
  return this.jsonl.getState();
9455
9612
  }
@@ -9597,6 +9754,18 @@ function createCodexHistory(options = {}) {
9597
9754
  };
9598
9755
  }
9599
9756
 
9757
+ // packages/server/src/agents/codex/ready-screen.ts
9758
+ var BUSY = /esc to interrupt|Working \(|Waiting for startup|esc cancel/;
9759
+ var FOOTER = /\? for shortcuts/;
9760
+ var COMPOSER = /^\s*›\s/;
9761
+ function codexReadyScreen(lines) {
9762
+ const tail = lines.filter((line) => line.trim() !== "").slice(-8);
9763
+ if (tail.some((line) => BUSY.test(line))) return false;
9764
+ const footer = tail.findIndex((line) => FOOTER.test(line));
9765
+ if (footer <= 0) return false;
9766
+ return tail.slice(0, footer).some((line) => COMPOSER.test(line));
9767
+ }
9768
+
9600
9769
  // packages/server/src/agents/codex/index.ts
9601
9770
  var CODEX_COMMAND = "codex";
9602
9771
  var CODEX_INSTALL_URL = "https://learn.chatgpt.com/docs/codex/cli";
@@ -9632,6 +9801,11 @@ var CODEX_CAPABILITIES = {
9632
9801
  // No hace cola: un mensaje con el agente trabajando le llega al modelo en el acto (hito 45).
9633
9802
  forceSend: false,
9634
9803
  editQueue: false,
9804
+ /*
9805
+ Hito 52 (§10.18): por su pantalla, cuando muestra la caja vacia y el
9806
+ historial no tiene un turno abierto. Codex no publica si trabaja.
9807
+ */
9808
+ scheduledSend: true,
9635
9809
  /*
9636
9810
  Hito 49: `--sandbox read-only`, al abrir y al reanudar. Medido con la 0.162.1
9637
9811
  y un proveedor falso: con la sandbox de Windows del usuario, escribir fallo
@@ -9718,6 +9892,13 @@ function createCodexAdapter() {
9718
9892
  },
9719
9893
  history: createCodexHistory({ planLimits }),
9720
9894
  status: null,
9895
+ /*
9896
+ Hito 52 (§10.18): un mensaje programado se escribe cuando la pantalla
9897
+ muestra la caja vacia. Con el daemon en frio lo escrito espera en la caja
9898
+ ("Waiting for startup") y llego a los 40 s: la confirmacion espera dos
9899
+ minutos.
9900
+ */
9901
+ scheduled: { kind: "screen", readyScreen: codexReadyScreen, confirmTimeoutMs: 12e4 },
9721
9902
  planLimits,
9722
9903
  defaults: () => Promise.resolve(null),
9723
9904
  /**
@@ -10379,7 +10560,22 @@ FROM part WHERE id = $id AND json_extract(data, '$.type') = 'file'`,
10379
10560
  * `question`; `$id` y `$s`.
10380
10561
  */
10381
10562
  partCallById: `SELECT message_id, json_extract(data, '$.callID') AS call_id
10382
- FROM part WHERE id = $id AND session_id = $s AND json_extract(data, '$.tool') = 'question'`
10563
+ FROM part WHERE id = $id AND session_id = $s AND json_extract(data, '$.tool') = 'question'`,
10564
+ /**
10565
+ * Hito 52 (§11.16). El agente, el modelo y la variante del ultimo mensaje del
10566
+ * usuario de UNA sesion, para mandar un mensaje programado por `prompt_async`
10567
+ * con los de la conversacion: sin agente, OpenCode usa el predeterminado
10568
+ * (medido con la 1.19.4). Solo esos cuatro campos; `$s`. Los del ultimo
10569
+ * instante, en el orden de siempre (regla 3): el ultimo de la lista es el que
10570
+ * vale.
10571
+ */
10572
+ lastUserPrompt: `SELECT json_extract(data, '$.agent') AS agent,
10573
+ json_extract(data, '$.model.providerID') AS provider_id,
10574
+ json_extract(data, '$.model.modelID') AS model_id,
10575
+ json_extract(data, '$.variant') AS variant
10576
+ FROM message WHERE session_id = $s AND json_extract(data, '$.role') = 'user'
10577
+ AND time_created = (SELECT max(time_created) FROM message WHERE session_id = $s AND json_extract(data, '$.role') = 'user')
10578
+ ORDER BY time_created, id`
10383
10579
  });
10384
10580
  var OPENCODE_REQUIRED_COLUMNS = Object.freeze([
10385
10581
  { table: "session", columns: ["id", "parent_id", "directory", "title", "time_created", "time_updated", "revert"] },
@@ -11246,6 +11442,29 @@ var OpenCodeServeClient = class {
11246
11442
  if (!OPENCODE_SESSION_ID_PATTERN.test(sessionId)) throw new Error("Invalid OpenCode session id.");
11247
11443
  await this.request("POST", `/session/${sessionId}/abort`, directory, {});
11248
11444
  }
11445
+ /**
11446
+ * Hito 52 (§11.16). Manda un mensaje a la sesion sin esperar la respuesta:
11447
+ * `POST /session/{id}/prompt_async`, que contesta 204 y deja el mensaje en la
11448
+ * base en el acto; el TUI enganchado lo muestra como uno mas (medido con la
11449
+ * 1.19.4, a los ~2 s). Sin agente, OpenCode usa el predeterminado y no el de
11450
+ * la conversacion: por eso se le pasan el agente, el modelo y la variante del
11451
+ * ultimo mensaje del usuario, si se conocen. `not-sent` si la sesion no existe
11452
+ * (404): no se mando nada. Otro fallo lanza: no se sabe si lo tomo.
11453
+ */
11454
+ async promptAsync(directory, sessionId, text, choice) {
11455
+ if (!OPENCODE_SESSION_ID_PATTERN.test(sessionId)) throw new Error("Invalid OpenCode session id.");
11456
+ const body = { parts: [{ type: "text", text }] };
11457
+ if (choice?.agent) body["agent"] = choice.agent;
11458
+ if (choice?.providerID && choice.modelID) body["model"] = { providerID: choice.providerID, modelID: choice.modelID };
11459
+ if (choice?.variant) body["variant"] = choice.variant;
11460
+ try {
11461
+ await this.request("POST", `/session/${sessionId}/prompt_async`, directory, body);
11462
+ return "sent";
11463
+ } catch (error) {
11464
+ if (error instanceof ServeRequestError && error.status === 404) return "not-sent";
11465
+ throw error;
11466
+ }
11467
+ }
11249
11468
  /**
11250
11469
  * El flujo de `/global/event`. Reconecta con las esperas de
11251
11470
  * `SERVE_RECONNECT_DELAYS_MS` y llama `onReconnect` **cada vez que la conexion
@@ -12286,6 +12505,22 @@ var OpenCodeServeStatus = class {
12286
12505
  }
12287
12506
  };
12288
12507
 
12508
+ // packages/server/src/agents/opencode/scheduled.ts
12509
+ var OPENCODE_SCHEDULED_CONFIRM_MS = 3e4;
12510
+ function shortText2(value) {
12511
+ return typeof value === "string" && value.length > 0 && value.length <= 200 ? value : null;
12512
+ }
12513
+ function readLastUserPrompt(db, sessionId) {
12514
+ const row = db.all(OPENCODE_SQL.lastUserPrompt, { $s: sessionId }).at(-1);
12515
+ if (row === void 0) return null;
12516
+ return {
12517
+ agent: shortText2(row.agent),
12518
+ providerID: shortText2(row.provider_id),
12519
+ modelID: shortText2(row.model_id),
12520
+ variant: shortText2(row.variant)
12521
+ };
12522
+ }
12523
+
12289
12524
  // packages/server/src/agents/opencode/index.ts
12290
12525
  var OPENCODE_COMMAND = "opencode";
12291
12526
  var OPENCODE_INSTALL_URL = "https://opencode.ai/docs/";
@@ -12336,6 +12571,11 @@ var OPENCODE_CAPABILITIES = {
12336
12571
  forceSend: false,
12337
12572
  // El mensaje en cola ya esta escrito en su base (§11.13): no hay de donde sacarlo.
12338
12573
  editQueue: false,
12574
+ /*
12575
+ Hito 52 (§11.16): por la API de su `serve` (`prompt_async`), sin tocar la
12576
+ pantalla del TUI, con el agente y el modelo del ultimo mensaje.
12577
+ */
12578
+ scheduledSend: true,
12339
12579
  /*
12340
12580
  Hito 49: ninguno. Su agente `plan` pide permiso antes de editar, pero la app
12341
12581
  abre el TUI con `opencode attach`, que segun su documentacion no acepta
@@ -12461,6 +12701,27 @@ function createOpenCodeAdapter(options = {}) {
12461
12701
  questions,
12462
12702
  // La cola la dice la base: un mensaje sin respuesta detras de uno que se esta contestando (hito 45, §11.13).
12463
12703
  promptQueue: { source: "session", forceSend: null },
12704
+ /*
12705
+ Hito 52 (§11.16): un mensaje programado va por `prompt_async` del `serve`
12706
+ que la pestana abierta ya levanto, con el agente, el modelo y la variante
12707
+ del ultimo mensaje del usuario. Sin `serve`, o sin poder leer la base, no
12708
+ se manda nada: sin el agente, OpenCode usaria el predeterminado.
12709
+ */
12710
+ scheduled: {
12711
+ kind: "api",
12712
+ confirmTimeoutMs: OPENCODE_SCHEDULED_CONFIRM_MS,
12713
+ deliver: async (target, text) => {
12714
+ const client = current?.client ?? null;
12715
+ if (client === null) return "not-sent";
12716
+ let choice;
12717
+ try {
12718
+ choice = readLastUserPrompt(db, target.sessionId);
12719
+ } catch {
12720
+ return "not-sent";
12721
+ }
12722
+ return client.promptAsync(target.cwd, target.sessionId, text, choice);
12723
+ }
12724
+ },
12464
12725
  /**
12465
12726
  * La base que se lee, para el arranque. Solo si hay algo que decir, para que
12466
12727
  * quien no usa OpenCode no vea una linea nueva:
@@ -13086,8 +13347,8 @@ function textOf2(message) {
13086
13347
  }
13087
13348
  function sameFile(a, b) {
13088
13349
  const normalize3 = (value) => {
13089
- const normalized = path36.normalize(value);
13090
- return process.platform === "win32" ? normalized.toLowerCase() : normalized;
13350
+ const normalized2 = path36.normalize(value);
13351
+ return process.platform === "win32" ? normalized2.toLowerCase() : normalized2;
13091
13352
  };
13092
13353
  return normalize3(a) === normalize3(b);
13093
13354
  }
@@ -14086,6 +14347,23 @@ function createPiQueueEdit(timing = DEFAULT_TIMING3) {
14086
14347
  return { run: (io, context) => runPiQueueEdit(io, context, piDequeueKeys(), timing) };
14087
14348
  }
14088
14349
 
14350
+ // packages/server/src/agents/pi/ready-screen.ts
14351
+ function isBorder2(line) {
14352
+ return /^─{2}.*─{6,}$/.test(line.trim());
14353
+ }
14354
+ function piReadyScreen(lines) {
14355
+ const screen = readPiQueueScreen(lines);
14356
+ if (screen === null || screen.editor.length > 0 || screen.queued) return false;
14357
+ const borders = [];
14358
+ lines.forEach((line, index) => {
14359
+ if (isBorder2(line)) borders.push(index);
14360
+ });
14361
+ const top = borders.at(-2);
14362
+ if (top === void 0) return false;
14363
+ if (/[A-Za-z]/.test(lines[top] ?? "")) return false;
14364
+ return lines.slice(0, top).some((line) => line.trim() !== "" && !isBorder2(line));
14365
+ }
14366
+
14089
14367
  // packages/server/src/agents/pi/tab-watch.ts
14090
14368
  import { readdir as readdir11, stat as stat25 } from "node:fs/promises";
14091
14369
  import path40 from "node:path";
@@ -14208,9 +14486,9 @@ var PiTabWatch = class {
14208
14486
  checkTrust() {
14209
14487
  if (this.closed || this.sessionId.length === 0) return;
14210
14488
  const output = this.context.readOutput();
14211
- const open9 = trustDialogOpen(output);
14212
- this.live.setBlocked(this.sessionId, open9);
14213
- if (open9) return;
14489
+ const open10 = trustDialogOpen(output);
14490
+ this.live.setBlocked(this.sessionId, open10);
14491
+ if (open10) return;
14214
14492
  if (interfaceStarted(output) || this.now() - this.context.launchedAt > STARTUP_DEADLINE_MS) {
14215
14493
  if (this.startupTimer !== null) clearInterval(this.startupTimer);
14216
14494
  this.startupTimer = null;
@@ -14380,6 +14658,11 @@ var PI_BASE_CAPABILITIES = {
14380
14658
  (§6.35.1). Medido en Windows; fuera, la tecla es Alt+Up y no esta medida.
14381
14659
  */
14382
14660
  editQueue: process.platform === "win32",
14661
+ /*
14662
+ Hito 52 (§16.21): por su pantalla, cuando muestra el editor vacio con la
14663
+ cabecera ya dibujada (un Enter antes, Pi no lo manda), y libre por su estado.
14664
+ */
14665
+ scheduledSend: true,
14383
14666
  // Hito 49: sin `edit` ni `write` (`PI_REVIEW_TOOLS`), pero su consola puede escribir: a medias (§16.19).
14384
14667
  readOnlyLaunch: { mode: null, guard: "tools" }
14385
14668
  };
@@ -14489,6 +14772,8 @@ function createPiAdapter(options = {}) {
14489
14772
  a mandar lo que devolvio a su editor (hito 45, §16.18).
14490
14773
  */
14491
14774
  promptQueue: { source: "app", forceSend: "interrupt-and-resend" },
14775
+ // Hito 52 (§16.21): el mensaje entra a la sesion unos 50 ms despues del Enter.
14776
+ scheduled: { kind: "screen", readyScreen: piReadyScreen, confirmTimeoutMs: 6e4 },
14492
14777
  // Sacar la cola para editarla en el cuadro (§6.35.1): vuelve lo que la app le mando.
14493
14778
  queueEdit: createPiQueueEdit(),
14494
14779
  // `settings.json` no se abre: sin modelo por defecto que anunciar. Al reanudar,
@@ -15214,12 +15499,13 @@ var ConversationHub = class extends EventEmitter {
15214
15499
  /**
15215
15500
  * Avisa la pregunta leida de la pantalla si cambio. No se avisa la que el
15216
15501
  * historial ya tiene abierta: esa tarjeta ya esta en el hilo, con el id de la
15217
- * CLI, y verla dos veces seria peor que no verla.
15502
+ * CLI, y verla dos veces seria peor que no verla. Una igual que hizo un
15503
+ * proceso anterior no cuenta: esa ya no espera (§5.5.3).
15218
15504
  */
15219
15505
  refreshLiveQuestion(terminalId, entry) {
15220
15506
  if (this.entries.get(terminalId) !== entry) return;
15221
15507
  const read2 = entry.live?.current()?.part ?? null;
15222
- const pending = read2 === null ? null : this.pendingQuestionOf(entry);
15508
+ const pending = read2 === null ? null : this.pendingQuestionOf(terminalId, entry);
15223
15509
  const item = read2?.questions[0];
15224
15510
  const shown = read2 !== null && item !== void 0 && !(pending?.questions.some((each) => sameQuestionItem(each, item)) ?? false) ? read2 : null;
15225
15511
  if (shown?.toolUseId === entry.liveShown?.toolUseId) return;
@@ -15308,10 +15594,10 @@ var ConversationHub = class extends EventEmitter {
15308
15594
  */
15309
15595
  refreshOpenToolCall(terminalId, entry) {
15310
15596
  const launchedAt = this.isBlind(entry) ? this.registry.launchedAtOf(terminalId) : null;
15311
- const open9 = launchedAt !== null && entry.follower.hasOpenToolCall(launchedAt);
15312
- if (open9 === entry.openToolCall) return;
15313
- entry.openToolCall = open9;
15314
- this.emit("toolCall", terminalId, open9);
15597
+ const open10 = launchedAt !== null && entry.follower.hasOpenToolCall(launchedAt);
15598
+ if (open10 === entry.openToolCall) return;
15599
+ entry.openToolCall = open10;
15600
+ this.emit("toolCall", terminalId, open10);
15315
15601
  }
15316
15602
  /**
15317
15603
  * Recalcula los subagentes que siguen trabajando y avisa si cambiaron
@@ -15759,10 +16045,15 @@ var ConversationHub = class extends EventEmitter {
15759
16045
  * Se recorre desde el final: la ultima pregunta sin su `tool_result` es la
15760
16046
  * abierta. Si ya tiene resultado —porque se contesto desde la solapa CLI—
15761
16047
  * devuelve null, y esa es la unica defensa contra responder dos veces.
16048
+ *
16049
+ * Tambien devuelve null si la hizo un proceso anterior de la CLI (24-10-2026,
16050
+ * §5.5.3): sin resultado en el archivo, pero la CLI de ahora no tiene ese
16051
+ * menu, y las teclas caerian en su prompt. Sin proceso la pregunta sigue
16052
+ * aca, y la escritura falla con el aviso de la pestana sin CLI.
15762
16053
  */
15763
16054
  getPendingQuestion(terminalId) {
15764
16055
  const entry = this.entries.get(terminalId);
15765
- return entry === void 0 ? null : this.pendingQuestionOf(entry);
16056
+ return entry === void 0 ? null : this.pendingQuestionOf(terminalId, entry);
15766
16057
  }
15767
16058
  /**
15768
16059
  * La pregunta leida de la pantalla que el hilo esta mostrando (§5.5.4), o
@@ -15792,7 +16083,7 @@ var ConversationHub = class extends EventEmitter {
15792
16083
  if (entry === void 0 || entry.live === null || item === void 0) return "unknown";
15793
16084
  return entry.live.check(item);
15794
16085
  }
15795
- pendingQuestionOf(entry) {
16086
+ pendingQuestionOf(terminalId, entry) {
15796
16087
  const { events } = entry.follower.getTail(PENDING_QUESTION_LOOKBACK);
15797
16088
  const answered = /* @__PURE__ */ new Set();
15798
16089
  for (const event2 of events) {
@@ -15801,11 +16092,13 @@ var ConversationHub = class extends EventEmitter {
15801
16092
  }
15802
16093
  }
15803
16094
  for (let index = events.length - 1; index >= 0; index -= 1) {
15804
- const parts = events[index]?.parts ?? [];
16095
+ const event2 = events[index];
16096
+ const parts = event2?.parts ?? [];
15805
16097
  for (let at = parts.length - 1; at >= 0; at -= 1) {
15806
16098
  const part = parts[at];
15807
16099
  if (part?.kind !== "question") continue;
15808
- return answered.has(part.toolUseId) ? null : part;
16100
+ if (answered.has(part.toolUseId)) return null;
16101
+ return askedBeforeLaunch(event2?.at ?? 0, this.registry.launchedAtOf(terminalId)) ? null : part;
15809
16102
  }
15810
16103
  }
15811
16104
  return null;
@@ -18099,9 +18392,9 @@ function findMarkers(content, lines, ignoredFences) {
18099
18392
  }
18100
18393
  continue;
18101
18394
  }
18102
- const open9 = FENCE_OPEN.exec(text);
18103
- const opening = open9?.[1];
18104
- if (opening !== void 0 && !ignoredFences.has(index) && !(opening[0] === "`" && (open9?.[2] ?? "").includes("`"))) {
18395
+ const open10 = FENCE_OPEN.exec(text);
18396
+ const opening = open10?.[1];
18397
+ if (opening !== void 0 && !ignoredFences.has(index) && !(opening[0] === "`" && (open10?.[2] ?? "").includes("`"))) {
18105
18398
  fence = { line: index, char: opening[0] ?? "`", length: opening.length };
18106
18399
  continue;
18107
18400
  }
@@ -19317,8 +19610,8 @@ function buildContinuationMessage(input) {
19317
19610
  }
19318
19611
  return sanitizeForPaste(lines.join("\n"));
19319
19612
  }
19320
- function transcriptReferenceFor(path76, style) {
19321
- return style === "at-quoted" ? fileReference(path76, "at-quoted") : `"${path76}"`;
19613
+ function transcriptReferenceFor(path78, style) {
19614
+ return style === "at-quoted" ? fileReference(path78, "at-quoted") : `"${path78}"`;
19322
19615
  }
19323
19616
  function planTranscript(input) {
19324
19617
  const turns = splitTurns(input.events);
@@ -20259,7 +20552,8 @@ function defaultAppSettings() {
20259
20552
  vault: { enabled: false, dir: null, toolResultMaxChars: VAULT_TOOL_RESULT_DEFAULT_CHARS },
20260
20553
  remote: { enabled: false, port: REMOTE_DEFAULT_PORT },
20261
20554
  console: { shell: "auto" },
20262
- agents: { disabled: [] }
20555
+ agents: { disabled: [] },
20556
+ scheduledMessages: { enabled: false }
20263
20557
  };
20264
20558
  }
20265
20559
  function isAbsoluteDir(dir, platform) {
@@ -20281,6 +20575,8 @@ function parseAppSettings(value, platform, warn = () => void 0) {
20281
20575
  const agentsRecord = asRecord(record["agents"]);
20282
20576
  const disabled = agentsRecord === null ? null : asAgentIdList(agentsRecord["disabled"]);
20283
20577
  if (disabled !== null) settings2.agents.disabled = disabled;
20578
+ const scheduledRecord = asRecord(record["scheduledMessages"]);
20579
+ settings2.scheduledMessages.enabled = (scheduledRecord === null ? null : asBoolean(scheduledRecord["enabled"])) ?? false;
20284
20580
  const remote = asRecord(record["remote"]);
20285
20581
  if (remote !== null) {
20286
20582
  settings2.remote.enabled = asBoolean(remote["enabled"]) ?? false;
@@ -20304,7 +20600,8 @@ function frozen(settings2) {
20304
20600
  vault: Object.freeze({ ...settings2.vault }),
20305
20601
  remote: Object.freeze({ ...settings2.remote }),
20306
20602
  console: Object.freeze({ ...settings2.console }),
20307
- agents: Object.freeze({ disabled: Object.freeze([...settings2.agents.disabled]) })
20603
+ agents: Object.freeze({ disabled: Object.freeze([...settings2.agents.disabled]) }),
20604
+ scheduledMessages: Object.freeze({ ...settings2.scheduledMessages })
20308
20605
  });
20309
20606
  }
20310
20607
  var SettingsStore = class {
@@ -20360,6 +20657,23 @@ var SettingsStore = class {
20360
20657
  if (settings2 === null) return;
20361
20658
  this.current = frozen({ ...this.current, remote: settings2.remote });
20362
20659
  }
20660
+ /**
20661
+ * Relee **solo** "Mensajes programados" (hito 51), por lo mismo que el acceso
20662
+ * remoto: apagarlo desde la otra instancia tiene que valer en esta, que es la
20663
+ * que podria estar por mandar algo. true si cambio.
20664
+ */
20665
+ async reloadScheduledMessages() {
20666
+ let parsed;
20667
+ try {
20668
+ parsed = JSON.parse(await readFile21(this.filePath, "utf8"));
20669
+ } catch {
20670
+ return false;
20671
+ }
20672
+ const settings2 = parseAppSettings(parsed, this.platform);
20673
+ if (settings2 === null || settings2.scheduledMessages.enabled === this.current.scheduledMessages.enabled) return false;
20674
+ this.current = frozen({ ...this.current, scheduledMessages: settings2.scheduledMessages });
20675
+ return true;
20676
+ }
20363
20677
  /** Una marca que cambia cuando el archivo cambia (fecha y tamano), o '' si no esta. */
20364
20678
  async stamp() {
20365
20679
  try {
@@ -20381,12 +20695,14 @@ var SettingsStore = class {
20381
20695
  update(patch) {
20382
20696
  const run2 = async () => {
20383
20697
  if (patch.remote === void 0) await this.reloadRemote();
20698
+ if (patch.scheduledMessages === void 0) await this.reloadScheduledMessages();
20384
20699
  const next = {
20385
20700
  version: SETTINGS_VERSION,
20386
20701
  vault: { ...this.current.vault },
20387
20702
  remote: { ...this.current.remote },
20388
20703
  console: { ...this.current.console },
20389
- agents: { disabled: [...this.current.agents.disabled] }
20704
+ agents: { disabled: [...this.current.agents.disabled] },
20705
+ scheduledMessages: { ...this.current.scheduledMessages }
20390
20706
  };
20391
20707
  const vault = patch.vault ?? {};
20392
20708
  if (vault.enabled !== void 0) next.vault.enabled = vault.enabled;
@@ -20401,6 +20717,7 @@ var SettingsStore = class {
20401
20717
  }
20402
20718
  if (patch.console?.shell !== void 0) next.console.shell = patch.console.shell;
20403
20719
  if (patch.agents?.disabled !== void 0) next.agents.disabled = [...new Set(patch.agents.disabled)];
20720
+ if (patch.scheduledMessages?.enabled !== void 0) next.scheduledMessages.enabled = patch.scheduledMessages.enabled;
20404
20721
  const remote = patch.remote ?? {};
20405
20722
  if (remote.enabled !== void 0) next.remote.enabled = remote.enabled;
20406
20723
  if (remote.port !== void 0) {
@@ -20960,10 +21277,353 @@ var DraftStore = class {
20960
21277
  }
20961
21278
  };
20962
21279
 
21280
+ // packages/server/src/scheduled-messages/store.ts
21281
+ import { mkdir as mkdir14, open as open8, readFile as readFile24, readdir as readdir14, rm as rm5, stat as stat34 } from "node:fs/promises";
21282
+ import path56 from "node:path";
21283
+ var STATE_VERSION4 = 1;
21284
+ var ID_PATTERN2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
21285
+ function isScheduledMessageId(value) {
21286
+ return ID_PATTERN2.test(value);
21287
+ }
21288
+ function sameOwner(a, b) {
21289
+ return a.agent === b.agent && a.sessionId === b.sessionId;
21290
+ }
21291
+ function parseEntry2(value) {
21292
+ const record = asRecord(value);
21293
+ if (record === null) return null;
21294
+ const id = asNonEmptyString(record["id"]);
21295
+ const agent = asLiteral(record["agent"], AGENT_IDS);
21296
+ const sessionId = asNonEmptyString(record["sessionId"]);
21297
+ const text = asString(record["text"]);
21298
+ const sendAt = asFiniteNumber(record["sendAt"]);
21299
+ const createdAt = asFiniteNumber(record["createdAt"]);
21300
+ if (id === null || !isScheduledMessageId(id) || agent === null || sessionId === null) return null;
21301
+ if (text === null || text.length === 0 || text.length > MAX_SCHEDULED_TEXT_CHARS) return null;
21302
+ if (sendAt === null || createdAt === null) return null;
21303
+ const message = { id, agent, sessionId, text, sendAt, createdAt };
21304
+ if (record["failure"] !== void 0) {
21305
+ const failure = parseServerText(record["failure"]);
21306
+ message.failure = failure ?? { key: "scheduledUncertain" };
21307
+ }
21308
+ return message;
21309
+ }
21310
+ function parseScheduledState(raw) {
21311
+ let parsed;
21312
+ try {
21313
+ parsed = JSON.parse(raw);
21314
+ } catch {
21315
+ return [];
21316
+ }
21317
+ const record = asRecord(parsed);
21318
+ if (record === null || record["version"] !== STATE_VERSION4) return [];
21319
+ const seen = /* @__PURE__ */ new Set();
21320
+ const messages = [];
21321
+ for (const message of asArrayFiltered(record["messages"], parseEntry2) ?? []) {
21322
+ if (seen.has(message.id)) continue;
21323
+ seen.add(message.id);
21324
+ messages.push(message);
21325
+ }
21326
+ return messages;
21327
+ }
21328
+ function copyOf2(message) {
21329
+ return message.failure === void 0 ? { ...message } : { ...message, failure: { ...message.failure } };
21330
+ }
21331
+ var ScheduledMessageStore = class {
21332
+ messages = [];
21333
+ /** Fecha y tamano del archivo la ultima vez que se leyo: si cambian, lo cambio otra instancia. */
21334
+ stamp = "";
21335
+ /** Los cambios, en fila: cada uno relee lo que dejo el anterior. */
21336
+ queue = Promise.resolve();
21337
+ statePath;
21338
+ claimsDir;
21339
+ pid;
21340
+ isAlive;
21341
+ constructor(options = {}) {
21342
+ this.statePath = options.statePath ?? scheduledMessagesPath();
21343
+ this.claimsDir = options.claimsDir ?? scheduledClaimsDir();
21344
+ this.pid = options.pid ?? process.pid;
21345
+ this.isAlive = options.isAlive ?? processAlive;
21346
+ }
21347
+ async load() {
21348
+ await this.readDisk();
21349
+ }
21350
+ /** Una copia de cada uno. */
21351
+ list() {
21352
+ return this.messages.map(copyOf2);
21353
+ }
21354
+ get(id) {
21355
+ const found = this.messages.find((message) => message.id === id);
21356
+ return found === void 0 ? null : copyOf2(found);
21357
+ }
21358
+ get size() {
21359
+ return this.messages.length;
21360
+ }
21361
+ /** Relee el archivo si cambio desde la ultima vez (lo cambio otra instancia). true si cambio. */
21362
+ async refresh() {
21363
+ if (await this.currentStamp() === this.stamp) return false;
21364
+ const before = JSON.stringify(this.messages);
21365
+ await this.readDisk();
21366
+ return JSON.stringify(this.messages) !== before;
21367
+ }
21368
+ add(message) {
21369
+ return this.mutate((messages) => {
21370
+ messages.push(copyOf2(message));
21371
+ return void 0;
21372
+ });
21373
+ }
21374
+ /** Saca uno. Devuelve el que estaba, o null. */
21375
+ remove(id) {
21376
+ return this.mutate((messages) => {
21377
+ const index = messages.findIndex((message) => message.id === id);
21378
+ if (index === -1) return null;
21379
+ const [removed] = messages.splice(index, 1);
21380
+ return removed === void 0 ? null : copyOf2(removed);
21381
+ });
21382
+ }
21383
+ /** Cambia la hora o la falla de uno. null si ya no esta. */
21384
+ update(id, change) {
21385
+ return this.mutate((messages) => {
21386
+ const message = messages.find((entry) => entry.id === id);
21387
+ if (message === void 0) return null;
21388
+ if (change.sendAt !== void 0) message.sendAt = change.sendAt;
21389
+ if (change.failure === null) delete message.failure;
21390
+ else if (change.failure !== void 0) message.failure = { ...change.failure };
21391
+ return copyOf2(message);
21392
+ });
21393
+ }
21394
+ /** Saca los de una conversacion: se cerro su pestana. Devuelve los que saco. */
21395
+ removeOwner(owner) {
21396
+ return this.mutate((messages) => {
21397
+ const removed = messages.filter((message) => sameOwner(message, owner));
21398
+ if (removed.length === 0) return removed;
21399
+ const kept = messages.filter((message) => !sameOwner(message, owner));
21400
+ messages.splice(0, messages.length, ...kept);
21401
+ return removed.map(copyOf2);
21402
+ });
21403
+ }
21404
+ /**
21405
+ * La pestana cambio de conversacion (`/clear`): sus mensajes van con ella,
21406
+ * como el borrador (§6.29). Solo pasa entre CLIs iguales: el id es de una.
21407
+ */
21408
+ move(from, to) {
21409
+ if (sameOwner(from, to) || from.agent !== to.agent) return Promise.resolve(0);
21410
+ return this.mutate((messages) => {
21411
+ let moved = 0;
21412
+ for (const message of messages) {
21413
+ if (!sameOwner(message, from)) continue;
21414
+ message.sessionId = to.sessionId;
21415
+ moved += 1;
21416
+ }
21417
+ return moved;
21418
+ });
21419
+ }
21420
+ /**
21421
+ * Deja solo los de estas conversaciones: las pestanas del archivo del
21422
+ * arranque. Lo demas es de una pestana que se cerro con una build anterior,
21423
+ * que no los borra, y no lo va a mostrar nadie.
21424
+ */
21425
+ retainOnly(owners) {
21426
+ const keep = [...owners];
21427
+ return this.mutate((messages) => {
21428
+ const kept = messages.filter((message) => keep.some((owner) => sameOwner(message, owner)));
21429
+ const dropped = messages.length - kept.length;
21430
+ if (dropped > 0) messages.splice(0, messages.length, ...kept);
21431
+ return dropped;
21432
+ });
21433
+ }
21434
+ /** Espera a que se escriba lo pendiente, para el apagado. */
21435
+ async flush() {
21436
+ await this.queue.catch(() => void 0);
21437
+ }
21438
+ // ---- quien lo esta mandando ----
21439
+ /**
21440
+ * Reclama un mensaje para este proceso, en `reserved`. Un reclamo de un
21441
+ * proceso muerto en `reserved` se borra y se vuelve a intentar: ese proceso no
21442
+ * escribio nada.
21443
+ */
21444
+ async claim(id) {
21445
+ if (!isScheduledMessageId(id)) return "other";
21446
+ await mkdir14(this.claimsDir, { recursive: true });
21447
+ for (let attempt = 0; attempt < 2; attempt += 1) {
21448
+ try {
21449
+ const handle = await open8(this.claimPath(id), "wx");
21450
+ try {
21451
+ await handle.writeFile(this.claimBody("reserved"));
21452
+ } finally {
21453
+ await handle.close();
21454
+ }
21455
+ return "mine";
21456
+ } catch (error) {
21457
+ if (error.code !== "EEXIST") throw error;
21458
+ }
21459
+ const existing = await this.claimOf(id);
21460
+ if (existing === null) continue;
21461
+ if (existing.mine) return "mine";
21462
+ if (existing.alive) return "other";
21463
+ if (existing.phase === "writing") return "orphan-writing";
21464
+ await rm5(this.claimPath(id), { force: true });
21465
+ }
21466
+ return "other";
21467
+ }
21468
+ /** Pasa un reclamo propio a otro paso. */
21469
+ async setPhase(id, phase) {
21470
+ const existing = await this.claimOf(id);
21471
+ if (existing === null || !existing.mine) return;
21472
+ await writeFileAtomic(this.claimPath(id), this.claimBody(phase));
21473
+ }
21474
+ /** Suelta un reclamo propio. El de otra instancia no se toca. */
21475
+ async release(id) {
21476
+ const existing = await this.claimOf(id);
21477
+ if (existing === null || !existing.mine) return;
21478
+ await rm5(this.claimPath(id), { force: true });
21479
+ }
21480
+ /** Borra el reclamo de un proceso muerto, ya declarado. El de uno vivo no se toca. */
21481
+ async discardOrphan(id) {
21482
+ const existing = await this.claimOf(id);
21483
+ if (existing === null || existing.mine || existing.alive) return;
21484
+ await rm5(this.claimPath(id), { force: true });
21485
+ }
21486
+ /** El reclamo de un mensaje, o null si no hay (o no se entiende: se trata como ausente). */
21487
+ async claimOf(id) {
21488
+ if (!isScheduledMessageId(id)) return null;
21489
+ let raw;
21490
+ try {
21491
+ raw = await readFile24(this.claimPath(id), "utf8");
21492
+ } catch {
21493
+ return null;
21494
+ }
21495
+ let parsed;
21496
+ try {
21497
+ parsed = JSON.parse(raw);
21498
+ } catch {
21499
+ return { pid: -1, phase: "reserved", mine: false, alive: true };
21500
+ }
21501
+ const record = asRecord(parsed);
21502
+ const pid = record === null ? null : asFiniteNumber(record["pid"]);
21503
+ const phase = record === null ? null : asLiteral(record["phase"], ["reserved", "writing"]);
21504
+ if (pid === null || phase === null) return { pid: -1, phase: "reserved", mine: false, alive: true };
21505
+ const mine = pid === this.pid;
21506
+ return { pid, phase, mine, alive: mine || this.isAlive(pid) };
21507
+ }
21508
+ /** Los ids que tienen reclamo en disco. Para limpiar los de mensajes que ya no estan. */
21509
+ async claimedIds() {
21510
+ let names;
21511
+ try {
21512
+ names = await readdir14(this.claimsDir);
21513
+ } catch {
21514
+ return [];
21515
+ }
21516
+ return names.filter((name) => name.endsWith(".json")).map((name) => name.slice(0, -".json".length)).filter(isScheduledMessageId);
21517
+ }
21518
+ /** Suelta los reclamos propios en `reserved`: la instancia se apaga sin haber escrito nada. */
21519
+ async releaseReserved() {
21520
+ for (const id of await this.claimedIds()) {
21521
+ const existing = await this.claimOf(id);
21522
+ if (existing !== null && existing.mine && existing.phase === "reserved") await rm5(this.claimPath(id), { force: true });
21523
+ }
21524
+ }
21525
+ // ---- disco ----
21526
+ claimPath(id) {
21527
+ return path56.join(this.claimsDir, `${id}.json`);
21528
+ }
21529
+ claimBody(phase) {
21530
+ return `${JSON.stringify({ pid: this.pid, phase, at: Date.now() })}
21531
+ `;
21532
+ }
21533
+ async currentStamp() {
21534
+ try {
21535
+ const info = await stat34(this.statePath);
21536
+ return `${info.mtimeMs}:${info.size}`;
21537
+ } catch {
21538
+ return "";
21539
+ }
21540
+ }
21541
+ async readDisk() {
21542
+ const stamp2 = await this.currentStamp();
21543
+ let messages = [];
21544
+ try {
21545
+ messages = parseScheduledState(await readFile24(this.statePath, "utf8"));
21546
+ } catch {
21547
+ }
21548
+ this.messages = messages;
21549
+ this.stamp = stamp2;
21550
+ return messages;
21551
+ }
21552
+ /** Relee, aplica el cambio y escribe. Lo que devuelve `change` es lo que devuelve el cambio. */
21553
+ mutate(change) {
21554
+ const run2 = async () => {
21555
+ const messages = await this.readDisk();
21556
+ const before = JSON.stringify(messages);
21557
+ const result2 = change(messages);
21558
+ if (JSON.stringify(messages) === before) return result2;
21559
+ await mkdir14(path56.dirname(this.statePath), { recursive: true });
21560
+ await writeFileAtomic(this.statePath, `${JSON.stringify({ version: STATE_VERSION4, messages }, null, 2)}
21561
+ `);
21562
+ this.messages = messages;
21563
+ this.stamp = await this.currentStamp();
21564
+ return result2;
21565
+ };
21566
+ const result = this.queue.then(run2, run2);
21567
+ this.queue = result.catch(() => void 0);
21568
+ return result;
21569
+ }
21570
+ };
21571
+
21572
+ // packages/server/src/scheduled-messages/instances.ts
21573
+ import { mkdir as mkdir15, readdir as readdir15, rm as rm6, writeFile as writeFile9 } from "node:fs/promises";
21574
+ import path57 from "node:path";
21575
+ var FILE_PATTERN = /^(\d+)\.json$/;
21576
+ var RunningInstances = class {
21577
+ constructor(dir = runningInstancesDir(), pid = process.pid, isAlive = processAlive) {
21578
+ this.dir = dir;
21579
+ this.pid = pid;
21580
+ this.isAlive = isAlive;
21581
+ }
21582
+ dir;
21583
+ pid;
21584
+ isAlive;
21585
+ fileOf(pid) {
21586
+ return path57.join(this.dir, `${pid}.json`);
21587
+ }
21588
+ /** Anota esta instancia. Nunca lanza: sin el archivo, la otra no espera, como antes del hito. */
21589
+ async register() {
21590
+ try {
21591
+ await mkdir15(this.dir, { recursive: true });
21592
+ await writeFile9(this.fileOf(this.pid), `${JSON.stringify({ pid: this.pid, startedAt: Date.now() })}
21593
+ `);
21594
+ } catch (error) {
21595
+ console.warn("[scheduled] couldn't note this instance:", error);
21596
+ }
21597
+ }
21598
+ /** La saca, al apagarse. Nunca lanza. */
21599
+ async unregister() {
21600
+ await rm6(this.fileOf(this.pid), { force: true }).catch(() => void 0);
21601
+ }
21602
+ /** true si otra instancia esta corriendo. Borra los archivos de las que ya no estan. */
21603
+ async othersRunning() {
21604
+ let names;
21605
+ try {
21606
+ names = await readdir15(this.dir);
21607
+ } catch {
21608
+ return false;
21609
+ }
21610
+ let found = false;
21611
+ for (const name of names) {
21612
+ const match = FILE_PATTERN.exec(name);
21613
+ if (match === null) continue;
21614
+ const pid = Number(match[1]);
21615
+ if (pid === this.pid) continue;
21616
+ if (this.isAlive(pid)) found = true;
21617
+ else await rm6(this.fileOf(pid), { force: true }).catch(() => void 0);
21618
+ }
21619
+ return found;
21620
+ }
21621
+ };
21622
+
20963
21623
  // packages/server/src/shell-locator.ts
20964
21624
  import { access as access2 } from "node:fs/promises";
20965
21625
  import { constants as fsConstants2 } from "node:fs";
20966
- import path56 from "node:path";
21626
+ import path58 from "node:path";
20967
21627
  async function isExecutable(candidate) {
20968
21628
  try {
20969
21629
  await access2(candidate, process.platform === "win32" ? fsConstants2.R_OK : fsConstants2.X_OK);
@@ -20981,21 +21641,21 @@ var SYSTEM_PROBE = {
20981
21641
  function windowsPowerShellPath(env) {
20982
21642
  const systemRoot = env["SystemRoot"] ?? env["windir"];
20983
21643
  if (systemRoot === void 0 || systemRoot.length === 0) return null;
20984
- return path56.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
21644
+ return path58.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
20985
21645
  }
20986
21646
  function gitBashCandidates(gitPath, env) {
20987
21647
  const candidates = [];
20988
21648
  if (gitPath !== null) {
20989
- let dir = path56.win32.dirname(gitPath);
21649
+ let dir = path58.win32.dirname(gitPath);
20990
21650
  for (let level = 0; level < 3; level++) {
20991
- candidates.push(path56.win32.join(dir, "bin", "bash.exe"));
20992
- const parent = path56.win32.dirname(dir);
21651
+ candidates.push(path58.win32.join(dir, "bin", "bash.exe"));
21652
+ const parent = path58.win32.dirname(dir);
20993
21653
  if (parent === dir) break;
20994
21654
  dir = parent;
20995
21655
  }
20996
21656
  }
20997
- for (const base of [env["ProgramFiles"], env["LOCALAPPDATA"] === void 0 ? void 0 : path56.win32.join(env["LOCALAPPDATA"], "Programs")]) {
20998
- if (base !== void 0 && base.length > 0) candidates.push(path56.win32.join(base, "Git", "bin", "bash.exe"));
21657
+ for (const base of [env["ProgramFiles"], env["LOCALAPPDATA"] === void 0 ? void 0 : path58.win32.join(env["LOCALAPPDATA"], "Programs")]) {
21658
+ if (base !== void 0 && base.length > 0) candidates.push(path58.win32.join(base, "Git", "bin", "bash.exe"));
20999
21659
  }
21000
21660
  const systemRoot = (env["SystemRoot"] ?? env["windir"] ?? "").toLowerCase();
21001
21661
  return [...new Set(candidates)].filter((candidate) => systemRoot.length === 0 || !candidate.toLowerCase().startsWith(systemRoot));
@@ -21026,7 +21686,7 @@ async function locatePosixShells(probe) {
21026
21686
  const found = [];
21027
21687
  const preferred = probe.env["SHELL"];
21028
21688
  if (preferred !== void 0 && preferred.length > 0 && await probe.isExecutable(preferred)) {
21029
- const name = path56.posix.basename(preferred);
21689
+ const name = path58.posix.basename(preferred);
21030
21690
  found.push({ id: "login", label: name, location: { file: preferred, args: [], name } });
21031
21691
  }
21032
21692
  for (const [id, file] of [["bash", "/bin/bash"], ["sh", "/bin/sh"], ["zsh", "/bin/zsh"]]) {
@@ -21075,9 +21735,18 @@ var AppSettingsService = class extends EventEmitter5 {
21075
21735
  consoleShells: this.options.shells.map(({ id, label }) => ({ id, label })),
21076
21736
  shellName: this.currentShell()?.name ?? null,
21077
21737
  disabledAgents: [...settings2.agents.disabled],
21078
- disabledAtStart: [...this.disabledAtStart]
21738
+ disabledAtStart: [...this.disabledAtStart],
21739
+ scheduledMessages: settings2.scheduledMessages.enabled
21079
21740
  };
21080
21741
  }
21742
+ /** "Mensajes programados" encendido, segun lo ultimo leido (hito 51). */
21743
+ scheduledMessagesEnabled() {
21744
+ return this.options.settings.get().scheduledMessages.enabled;
21745
+ }
21746
+ /** Lo relee del disco; si la otra instancia lo cambio, avisa como cualquier cambio. */
21747
+ async refreshScheduledMessages() {
21748
+ if (await this.options.settings.reloadScheduledMessages()) this.emit("status", this.status());
21749
+ }
21081
21750
  /** Guarda el cambio, avisa al registro si cambio la consola, y emite `status`. */
21082
21751
  async update(change) {
21083
21752
  if (change.consoleShell !== void 0 && !this.offered(change.consoleShell)) {
@@ -21086,7 +21755,8 @@ var AppSettingsService = class extends EventEmitter5 {
21086
21755
  const before = this.currentShell();
21087
21756
  await this.options.settings.update({
21088
21757
  ...change.consoleShell !== void 0 ? { console: { shell: change.consoleShell } } : {},
21089
- ...change.disabledAgents !== void 0 ? { agents: { disabled: change.disabledAgents } } : {}
21758
+ ...change.disabledAgents !== void 0 ? { agents: { disabled: change.disabledAgents } } : {},
21759
+ ...change.scheduledMessages !== void 0 ? { scheduledMessages: { enabled: change.scheduledMessages } } : {}
21090
21760
  });
21091
21761
  const after = this.currentShell();
21092
21762
  if (after !== before) this.options.onShellChange(after);
@@ -21098,13 +21768,13 @@ var AppSettingsService = class extends EventEmitter5 {
21098
21768
  };
21099
21769
 
21100
21770
  // packages/server/src/session-watcher.ts
21101
- import { stat as stat34 } from "node:fs/promises";
21771
+ import { stat as stat35 } from "node:fs/promises";
21102
21772
  import chokidar3 from "chokidar";
21103
21773
  var INDEX_DEBOUNCE_MS = 750;
21104
21774
  var ROOT_PROBE_MS = 5e3;
21105
21775
  async function isDirectory3(target) {
21106
21776
  try {
21107
- return (await stat34(target)).isDirectory();
21777
+ return (await stat35(target)).isDirectory();
21108
21778
  } catch {
21109
21779
  return false;
21110
21780
  }
@@ -21207,7 +21877,7 @@ function watchSessions(index, hub, agents, options = {}) {
21207
21877
  // packages/server/src/terminal-registry.ts
21208
21878
  import { randomUUID as randomUUID5 } from "node:crypto";
21209
21879
  import { EventEmitter as EventEmitter6 } from "node:events";
21210
- import { stat as stat35 } from "node:fs/promises";
21880
+ import { stat as stat36 } from "node:fs/promises";
21211
21881
 
21212
21882
  // packages/server/src/launch-hook-slot.ts
21213
21883
  var LaunchHookSlot = class {
@@ -21508,9 +22178,9 @@ async function resolveLaunchPlan(launch) {
21508
22178
  }
21509
22179
 
21510
22180
  // packages/server/src/workspace-store.ts
21511
- import { mkdir as mkdir14, readFile as readFile24, writeFile as writeFile9 } from "node:fs/promises";
21512
- import path57 from "node:path";
21513
- var STATE_VERSION4 = 1;
22181
+ import { mkdir as mkdir16, readFile as readFile25, writeFile as writeFile10 } from "node:fs/promises";
22182
+ import path59 from "node:path";
22183
+ var STATE_VERSION5 = 1;
21514
22184
  var WRITE_DEBOUNCE_MS4 = 400;
21515
22185
  var LEGACY_TAB_AGENT = "claude-code";
21516
22186
  function placeByPosition(base, placed) {
@@ -21574,7 +22244,7 @@ function parseState4(raw) {
21574
22244
  try {
21575
22245
  const parsed = JSON.parse(raw);
21576
22246
  if (!isRecord(parsed)) return null;
21577
- if (parsed["version"] !== STATE_VERSION4) return null;
22247
+ if (parsed["version"] !== STATE_VERSION5) return null;
21578
22248
  const rawTabs = parsed["tabs"];
21579
22249
  if (!Array.isArray(rawTabs)) return null;
21580
22250
  const base = [];
@@ -21612,7 +22282,7 @@ function serializeState(state) {
21612
22282
  otherTabs.push({ ...entry.tab, position });
21613
22283
  }
21614
22284
  }
21615
- return otherTabs.length === 0 ? { version: STATE_VERSION4, tabs } : { version: STATE_VERSION4, tabs, otherTabs };
22285
+ return otherTabs.length === 0 ? { version: STATE_VERSION5, tabs } : { version: STATE_VERSION5, tabs, otherTabs };
21616
22286
  }
21617
22287
  var EMPTY_STATE = () => ({ tabs: [], foreignTabs: [] });
21618
22288
  function persistableTabs(descriptors) {
@@ -21636,7 +22306,7 @@ var WorkspaceStore = class {
21636
22306
  pending = null;
21637
22307
  async load() {
21638
22308
  try {
21639
- const raw = await readFile24(workspaceStatePath(), "utf8");
22309
+ const raw = await readFile25(workspaceStatePath(), "utf8");
21640
22310
  return parseState4(raw) ?? EMPTY_STATE();
21641
22311
  } catch {
21642
22312
  return EMPTY_STATE();
@@ -21666,10 +22336,10 @@ var WorkspaceStore = class {
21666
22336
  }
21667
22337
  async writeNow(state) {
21668
22338
  try {
21669
- await mkdir14(appConfigDir(), { recursive: true });
22339
+ await mkdir16(appConfigDir(), { recursive: true });
21670
22340
  const target = workspaceStatePath();
21671
- const temporary = path57.join(path57.dirname(target), `workspace.${process.pid}.tmp`);
21672
- await writeFile9(temporary, JSON.stringify(serializeState(state), null, 2), "utf8");
22341
+ const temporary = path59.join(path59.dirname(target), `workspace.${process.pid}.tmp`);
22342
+ await writeFile10(temporary, JSON.stringify(serializeState(state), null, 2), "utf8");
21673
22343
  const { rename: rename8 } = await import("node:fs/promises");
21674
22344
  await rename8(temporary, target);
21675
22345
  } catch (error) {
@@ -21937,7 +22607,7 @@ var TerminalRegistry = class extends EventEmitter6 {
21937
22607
  async assertDirectory(cwd) {
21938
22608
  let info;
21939
22609
  try {
21940
- info = await stat35(cwd);
22610
+ info = await stat36(cwd);
21941
22611
  } catch {
21942
22612
  throw new TerminalOpenError("invalid-cwd", serverText("cwdMissing", { cwd }));
21943
22613
  }
@@ -22395,26 +23065,26 @@ import { WebSocketServer } from "ws";
22395
23065
 
22396
23066
  // packages/server/src/directory-picker.ts
22397
23067
  import { randomUUID as randomUUID6 } from "node:crypto";
22398
- import { lstat, mkdir as mkdir15, readdir as readdir14, stat as stat36 } from "node:fs/promises";
23068
+ import { lstat, mkdir as mkdir17, readdir as readdir16, stat as stat37 } from "node:fs/promises";
22399
23069
  import { homedir as homedir14 } from "node:os";
22400
- import path58 from "node:path";
23070
+ import path60 from "node:path";
22401
23071
  var MAX_ENTRIES = 500;
22402
23072
  var MAX_PICKERS = 8;
22403
23073
  var SKIPPED = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "bin", "obj", "$RECYCLE.BIN"]);
22404
23074
  var DirectoryPickerError = class extends ServerTextError {
22405
23075
  };
22406
23076
  function isInsideProtected(target, dirs) {
22407
- const resolved = path58.resolve(target);
23077
+ const resolved = path60.resolve(target);
22408
23078
  return dirs.some((dir) => {
22409
- const protectedDir = path58.resolve(dir);
22410
- return resolved === protectedDir || resolved.startsWith(protectedDir + path58.sep);
23079
+ const protectedDir = path60.resolve(dir);
23080
+ return resolved === protectedDir || resolved.startsWith(protectedDir + path60.sep);
22411
23081
  });
22412
23082
  }
22413
23083
  async function isListedDirectory(parent, entry, readLink = lstat) {
22414
23084
  if (entry.isDirectory()) return true;
22415
23085
  if (!entry.isSymbolicLink()) return false;
22416
23086
  try {
22417
- const info = await readLink(path58.join(parent, entry.name));
23087
+ const info = await readLink(path60.join(parent, entry.name));
22418
23088
  return info.isDirectory() && !info.isSymbolicLink();
22419
23089
  } catch {
22420
23090
  return false;
@@ -22429,7 +23099,7 @@ async function listRoots() {
22429
23099
  letters.map(async (letter) => {
22430
23100
  const drive = `${letter}:\\`;
22431
23101
  try {
22432
- await stat36(drive);
23102
+ await stat37(drive);
22433
23103
  return drive;
22434
23104
  } catch {
22435
23105
  return null;
@@ -22481,14 +23151,14 @@ var DirectoryPickers = class {
22481
23151
  async enter(pickerId, name) {
22482
23152
  const picker = this.require(pickerId);
22483
23153
  if (name === "..") {
22484
- const parent = path58.dirname(picker.current);
23154
+ const parent = path60.dirname(picker.current);
22485
23155
  if (parent !== picker.current) picker.current = parent;
22486
23156
  return this.listing(picker);
22487
23157
  }
22488
23158
  if (!picker.entries.has(name)) {
22489
23159
  throw new DirectoryPickerError(serverText("pickerNotListed"));
22490
23160
  }
22491
- const target = path58.join(picker.current, name);
23161
+ const target = path60.join(picker.current, name);
22492
23162
  if (isInsideProtected(target, this.protectedDirs)) {
22493
23163
  throw new DirectoryPickerError(serverText("pickerCliFolder"));
22494
23164
  }
@@ -22528,12 +23198,12 @@ var DirectoryPickers = class {
22528
23198
  throw new DirectoryPickerError(serverText("pickerNameChars"));
22529
23199
  }
22530
23200
  }
22531
- const target = path58.join(picker.current, clean);
23201
+ const target = path60.join(picker.current, clean);
22532
23202
  if (isInsideProtected(target, this.protectedDirs)) {
22533
23203
  throw new DirectoryPickerError(serverText("pickerNoProjectsHere"));
22534
23204
  }
22535
23205
  try {
22536
- await mkdir15(target);
23206
+ await mkdir17(target);
22537
23207
  } catch (error) {
22538
23208
  const code = error.code;
22539
23209
  throw new DirectoryPickerError(
@@ -22564,9 +23234,9 @@ var DirectoryPickers = class {
22564
23234
  let names = [];
22565
23235
  let truncated = false;
22566
23236
  try {
22567
- const found = await readdir14(picker.current, { withFileTypes: true });
23237
+ const found = await readdir16(picker.current, { withFileTypes: true });
22568
23238
  const listed = await Promise.all(found.map((entry) => isListedDirectory(picker.current, entry)));
22569
- const dirs = found.filter((_, index) => listed[index] === true).map((entry) => entry.name).filter((name) => !SKIPPED.has(name)).filter((name) => !isInsideProtected(path58.join(picker.current, name), this.protectedDirs)).sort((a, b) => a.localeCompare(b));
23239
+ const dirs = found.filter((_, index) => listed[index] === true).map((entry) => entry.name).filter((name) => !SKIPPED.has(name)).filter((name) => !isInsideProtected(path60.join(picker.current, name), this.protectedDirs)).sort((a, b) => a.localeCompare(b));
22570
23240
  truncated = dirs.length > MAX_ENTRIES;
22571
23241
  names = dirs.slice(0, MAX_ENTRIES);
22572
23242
  } catch {
@@ -22577,7 +23247,7 @@ var DirectoryPickers = class {
22577
23247
  path: picker.current,
22578
23248
  entries: names,
22579
23249
  roots,
22580
- canGoUp: path58.dirname(picker.current) !== picker.current,
23250
+ canGoUp: path60.dirname(picker.current) !== picker.current,
22581
23251
  truncated
22582
23252
  };
22583
23253
  }
@@ -22637,7 +23307,7 @@ var PlanLimitsBoard = class {
22637
23307
 
22638
23308
  // packages/server/src/cli-folders.ts
22639
23309
  import { realpath as realpath4 } from "node:fs/promises";
22640
- import path59 from "node:path";
23310
+ import path61 from "node:path";
22641
23311
  function isSameOrInside(child, parent, platform) {
22642
23312
  const childKey = normalizeCwdKey(child, platform);
22643
23313
  const parentKey = normalizeCwdKey(parent, platform);
@@ -22649,7 +23319,7 @@ function isSameOrInside(child, parent, platform) {
22649
23319
  async function cliFolders(dirs, platform = process.platform) {
22650
23320
  const known = [];
22651
23321
  for (const dir of dirs) {
22652
- known.push(path59.resolve(dir));
23322
+ known.push(path61.resolve(dir));
22653
23323
  try {
22654
23324
  known.push(await realpath4(dir));
22655
23325
  } catch {
@@ -22672,8 +23342,8 @@ async function cliFolders(dirs, platform = process.platform) {
22672
23342
 
22673
23343
  // packages/server/src/file-browser.ts
22674
23344
  import { createHash as createHash4 } from "node:crypto";
22675
- import { readFile as readFile25, readdir as readdir15, realpath as realpath5, stat as stat37 } from "node:fs/promises";
22676
- import path60 from "node:path";
23345
+ import { readFile as readFile26, readdir as readdir17, realpath as realpath5, stat as stat38 } from "node:fs/promises";
23346
+ import path62 from "node:path";
22677
23347
  var MAX_ENTRIES2 = 1e3;
22678
23348
  var SEARCH_MAX_RESULTS = 200;
22679
23349
  var SEARCH_MAX_DIRECTORIES = 4e3;
@@ -22736,7 +23406,7 @@ function languageFor(fileName) {
22736
23406
  const lower = fileName.toLowerCase();
22737
23407
  const byName = LANGUAGE_BY_NAME[lower];
22738
23408
  if (byName !== void 0) return byName;
22739
- const extension = path60.extname(lower);
23409
+ const extension = path62.extname(lower);
22740
23410
  return LANGUAGE_BY_EXTENSION[extension] ?? null;
22741
23411
  }
22742
23412
  function looksBinary2(buffer) {
@@ -22773,7 +23443,7 @@ async function listDirectory(cwd, relativePath, folders, options = {}) {
22773
23443
  let sizeBytes = 0;
22774
23444
  let modifiedAt = 0;
22775
23445
  try {
22776
- const info = await stat37(path60.join(absolute, candidate.name));
23446
+ const info = await stat38(path62.join(absolute, candidate.name));
22777
23447
  sizeBytes = candidate.isDirectory ? 0 : info.size;
22778
23448
  modifiedAt = info.mtimeMs;
22779
23449
  } catch {
@@ -22799,7 +23469,7 @@ async function scanLevel(absolute, repoRoot2, folders) {
22799
23469
  return level;
22800
23470
  }
22801
23471
  async function readLevel(absolute, folders) {
22802
- const dirents = await readdir15(absolute, { withFileTypes: true });
23472
+ const dirents = await readdir17(absolute, { withFileTypes: true });
22803
23473
  const realLevel = await realpath5(absolute);
22804
23474
  let skipped = 0;
22805
23475
  const candidates = [];
@@ -22807,8 +23477,8 @@ async function readLevel(absolute, folders) {
22807
23477
  const always = ALWAYS_SKIP.has(dirent.name.toLowerCase());
22808
23478
  if (dirent.isSymbolicLink()) {
22809
23479
  try {
22810
- const linked = path60.join(absolute, dirent.name);
22811
- const target = await stat37(linked);
23480
+ const linked = path62.join(absolute, dirent.name);
23481
+ const target = await stat38(linked);
22812
23482
  if (folders.holds(await realpath5(linked))) continue;
22813
23483
  candidates.push({
22814
23484
  name: dirent.name,
@@ -22824,7 +23494,7 @@ async function readLevel(absolute, folders) {
22824
23494
  skipped += 1;
22825
23495
  continue;
22826
23496
  }
22827
- if (folders.holds(path60.join(realLevel, dirent.name))) continue;
23497
+ if (folders.holds(path62.join(realLevel, dirent.name))) continue;
22828
23498
  candidates.push({
22829
23499
  name: dirent.name,
22830
23500
  isDirectory: dirent.isDirectory(),
@@ -22839,7 +23509,7 @@ async function markIgnored(repoRoot2, levels) {
22839
23509
  for (const level of levels) {
22840
23510
  for (const entry of level.entries) {
22841
23511
  if (entry.hidden !== null) continue;
22842
- keys.set(entry, toPosixPath(path60.relative(repoRoot2, path60.join(level.absolute, entry.name))));
23512
+ keys.set(entry, toPosixPath(path62.relative(repoRoot2, path62.join(level.absolute, entry.name))));
22843
23513
  }
22844
23514
  }
22845
23515
  if (keys.size === 0) return;
@@ -22901,7 +23571,7 @@ async function searchFiles(cwd, query, folders, options = {}) {
22901
23571
  let sizeBytes = 0;
22902
23572
  let modifiedAt = 0;
22903
23573
  try {
22904
- const info = await stat37(path60.join(level.absolute, candidate.name));
23574
+ const info = await stat38(path62.join(level.absolute, candidate.name));
22905
23575
  sizeBytes = candidate.isDirectory ? 0 : info.size;
22906
23576
  modifiedAt = info.mtimeMs;
22907
23577
  } catch {
@@ -22939,7 +23609,7 @@ async function existingPaths(cwd, relativePaths, folders) {
22939
23609
  for (const relativePath of relativePaths) {
22940
23610
  try {
22941
23611
  const absolute = await resolveForFiles(cwd, relativePath, folders);
22942
- const info = await stat37(absolute);
23612
+ const info = await stat38(absolute);
22943
23613
  if (info.isFile()) files.push(relativePath);
22944
23614
  else if (info.isDirectory()) directories.push(relativePath);
22945
23615
  } catch {
@@ -22955,12 +23625,12 @@ function isEditableText(content, text) {
22955
23625
  }
22956
23626
  async function readPreview(cwd, relativePath, folders) {
22957
23627
  const absolute = await resolveForFiles(cwd, relativePath, folders);
22958
- const info = await stat37(absolute);
23628
+ const info = await stat38(absolute);
22959
23629
  if (info.isDirectory()) {
22960
23630
  throw new Error("It's a directory, not a file.");
22961
23631
  }
22962
- const content = await readFile25(absolute);
22963
- const fileName = path60.basename(absolute);
23632
+ const content = await readFile26(absolute);
23633
+ const fileName = path62.basename(absolute);
22964
23634
  if (looksBinary2(content)) {
22965
23635
  return {
22966
23636
  path: relativePath,
@@ -22986,7 +23656,7 @@ async function readPreview(cwd, relativePath, folders) {
22986
23656
 
22987
23657
  // packages/server/src/file-write.ts
22988
23658
  import { constants } from "node:fs";
22989
- import { access as access3, readFile as readFile26, realpath as realpath6, stat as stat38 } from "node:fs/promises";
23659
+ import { access as access3, readFile as readFile27, realpath as realpath6, stat as stat39 } from "node:fs/promises";
22990
23660
  var FileWriteError = class extends ServerTextError {
22991
23661
  };
22992
23662
  function withLineEndings(text, original) {
@@ -23011,9 +23681,9 @@ async function saveProjectFile(cwd, relativePath, text, baseHash, folders) {
23011
23681
  }
23012
23682
  if (await folders.contains(absolute)) throw new InvalidPathError(serverText("cliFolderClosed"));
23013
23683
  const target = await realpath6(absolute);
23014
- const info = await stat38(target);
23684
+ const info = await stat39(target);
23015
23685
  if (!info.isFile()) throw new FileWriteError(serverText("fileSaveFailed"));
23016
- const current = await readFile26(target);
23686
+ const current = await readFile27(target);
23017
23687
  if (contentHash(current) !== baseHash) {
23018
23688
  return { outcome: "changed", preview: await readPreview(cwd, relativePath, folders).catch(() => null) };
23019
23689
  }
@@ -23323,12 +23993,115 @@ function prefillReasonFor(outcome) {
23323
23993
  }
23324
23994
  }
23325
23995
 
23326
- // packages/server/src/vault/read-session.ts
23327
- var MAX_POLLS = 5;
23328
- var MAX_TOTAL_POLLS = 20;
23329
- var UnsupportedHistoryError = class extends Error {
23330
- constructor(label) {
23331
- super(`${label}: the source doesn't declare that its follower respects the local copy's limits (wholeRead)`);
23996
+ // packages/server/src/submit-message.ts
23997
+ function prepareSubmit(deps, terminalId) {
23998
+ const submitting = deps.adapterOf(terminalId);
23999
+ const isBlind = () => submitting !== null && deps.conversations.isBlindToApprovals(terminalId, submitting);
24000
+ const guardsPending = submitting?.capabilities.permissionCycle?.approvesPendingOnCycle === true;
24001
+ const blocksWhileWaiting = submitting?.capabilities.waitingBlocksSubmit === true;
24002
+ if (submitting !== null) {
24003
+ const launchedAt = deps.launchedAtOf(terminalId);
24004
+ const blind = isBlind();
24005
+ const refusal = submitRefusal({
24006
+ label: submitting.label,
24007
+ approvesPendingOnCycle: guardsPending,
24008
+ waitingFor: deps.conversations.getWaitingFor(terminalId),
24009
+ waitingBlocksSubmit: blocksWhileWaiting,
24010
+ activity: deps.activityOf(terminalId),
24011
+ blind,
24012
+ openToolCall: blind && launchedAt !== null && deps.conversations.hasOpenToolCall(terminalId, launchedAt)
24013
+ });
24014
+ if (refusal !== null) return { refusal };
24015
+ }
24016
+ const approvalGuard = submitting !== null && (guardsPending || blocksWhileWaiting || isBlind()) ? async () => {
24017
+ const current = deps.launchedAtOf(terminalId);
24018
+ if (current === null) return true;
24019
+ if (blocksWhileWaiting && deps.activityOf(terminalId) === "waiting") return false;
24020
+ if (isBlind() && await deps.conversations.checkOpenToolCall(terminalId, current)) return false;
24021
+ return !guardsPending || await deps.conversations.checkWaitingFor(terminalId) === null;
24022
+ } : void 0;
24023
+ const input = deps.inputOf(terminalId);
24024
+ const startupDialog = input.startupDialog;
24025
+ const firstMessage = startupDialog !== void 0 && (deps.sessionIdOf(terminalId) ?? "") === "";
24026
+ let startupChecked = false;
24027
+ let stoppedByDialog = false;
24028
+ const pieceGuard = firstMessage || approvalGuard !== void 0 ? async () => {
24029
+ if (firstMessage && startupDialog !== void 0 && !startupChecked) {
24030
+ startupChecked = true;
24031
+ const screen = deps.watchScreen(terminalId);
24032
+ if (screen !== null) {
24033
+ try {
24034
+ stoppedByDialog = screenShowsDialog(await screen.lines(), startupDialog);
24035
+ } finally {
24036
+ screen.dispose();
24037
+ }
24038
+ if (stoppedByDialog) return false;
24039
+ }
24040
+ }
24041
+ return approvalGuard === void 0 ? true : approvalGuard();
24042
+ } : void 0;
24043
+ return {
24044
+ terminalId,
24045
+ input,
24046
+ label: submitting?.label ?? null,
24047
+ blocksWhileWaiting,
24048
+ pieceGuard,
24049
+ stoppedByDialog: () => stoppedByDialog,
24050
+ promptQueueSource: submitting?.promptQueue?.source ?? null,
24051
+ hasQueuedSubmits: submitting?.queuedSubmits !== void 0
24052
+ };
24053
+ }
24054
+ async function runSubmit(deps, plan, request) {
24055
+ const { terminalId, input } = plan;
24056
+ let result = { kind: "dropped" };
24057
+ await deps.enqueue(
24058
+ terminalId,
24059
+ async (lane) => {
24060
+ result = { kind: "failed" };
24061
+ const content = await request.prepare(input);
24062
+ if (content === null) {
24063
+ result = { kind: "prepare-failed" };
24064
+ return;
24065
+ }
24066
+ const { fullText, imagePaths } = content;
24067
+ const pieces = buildSubmissionWrites(fullText, imagePaths, input, { send: request.send });
24068
+ if (pieces === null) {
24069
+ result = { kind: "nothing" };
24070
+ return;
24071
+ }
24072
+ deps.noteSubmitted(terminalId, fullText);
24073
+ const startAfterMs = submitStartDelayMs(deps.launchedAtOf(terminalId), deps.now(), input.readyAfterLaunchMs ?? 0);
24074
+ const outcome = await lane.writePieces(pieces, input.pieceGapMs, request.write, plan.pieceGuard, { startAfterMs });
24075
+ request.onWritten?.(outcome);
24076
+ if (outcome === "written" && request.send && plan.promptQueueSource === "app" && deps.activityOf(terminalId) === "busy" && !request.text.trimStart().startsWith("/")) {
24077
+ deps.conversations.noteQueued(terminalId, { text: request.text, images: request.imageCount, at: deps.now() });
24078
+ }
24079
+ if (outcome === "written" && request.send && plan.hasQueuedSubmits) {
24080
+ deps.queuedSubmits.note(terminalId, submissionPayload(fullText, imagePaths, input.imageReference) ?? fullText, deps.now(), fullText);
24081
+ }
24082
+ if (outcome === "written") result = { kind: "written" };
24083
+ else if (outcome === "refused") result = { kind: "refused" };
24084
+ else if (outcome === "interrupted") result = { kind: "interrupted" };
24085
+ else {
24086
+ result = {
24087
+ kind: "blocked",
24088
+ byDialog: plan.stoppedByDialog(),
24089
+ waitingNow: plan.blocksWhileWaiting && deps.activityOf(terminalId) === "waiting"
24090
+ };
24091
+ }
24092
+ },
24093
+ // Descartado por una interrupcion antes de empezar: queda `dropped`.
24094
+ { onDropped: () => void 0 }
24095
+ );
24096
+ return result;
24097
+ }
24098
+
24099
+ // packages/server/src/vault/read-session.ts
24100
+ var MAX_POLLS = 5;
24101
+ var MAX_TOTAL_POLLS = 20;
24102
+ var UnsupportedHistoryError = class extends Error {
24103
+ constructor(label) {
24104
+ super(`${label}: the source doesn't declare that its follower respects the local copy's limits (wholeRead)`);
23332
24105
  this.name = "UnsupportedHistoryError";
23333
24106
  }
23334
24107
  };
@@ -23685,14 +24458,14 @@ var TerminalWriteQueue = class {
23685
24458
  };
23686
24459
 
23687
24460
  // packages/server/src/vault/service.ts
23688
- import { mkdir as mkdir18, stat as stat42 } from "node:fs/promises";
24461
+ import { mkdir as mkdir20, stat as stat43 } from "node:fs/promises";
23689
24462
  import { homedir as homedir15 } from "node:os";
23690
- import path64 from "node:path";
24463
+ import path66 from "node:path";
23691
24464
 
23692
24465
  // packages/server/src/vault/move.ts
23693
24466
  import { constants as constants2 } from "node:fs";
23694
- import { copyFile as copyFile2, lstat as lstat2, mkdir as mkdir16, readdir as readdir16, rm as rm5, stat as stat39, utimes, writeFile as writeFile10 } from "node:fs/promises";
23695
- import path61 from "node:path";
24467
+ import { copyFile as copyFile2, lstat as lstat2, mkdir as mkdir18, readdir as readdir18, rm as rm7, stat as stat40, utimes, writeFile as writeFile11 } from "node:fs/promises";
24468
+ import path63 from "node:path";
23696
24469
  var MOVED_ENTRIES = ["vault.json", "sessions", "memory"];
23697
24470
  var OWN_TEMP_FILE = /\.\d+\.[0-9a-f]{12}\.tmp$/;
23698
24471
  async function copyTree(source, target, result) {
@@ -23703,14 +24476,14 @@ async function copyTree(source, target, result) {
23703
24476
  return;
23704
24477
  }
23705
24478
  if (info.isDirectory()) {
23706
- await mkdir16(target, { recursive: true });
23707
- for (const name of await readdir16(source)) {
23708
- await copyTree(path61.join(source, name), path61.join(target, name), result);
24479
+ await mkdir18(target, { recursive: true });
24480
+ for (const name of await readdir18(source)) {
24481
+ await copyTree(path63.join(source, name), path63.join(target, name), result);
23709
24482
  }
23710
24483
  return;
23711
24484
  }
23712
24485
  if (!info.isFile() || OWN_TEMP_FILE.test(source)) return;
23713
- await mkdir16(path61.dirname(target), { recursive: true });
24486
+ await mkdir18(path63.dirname(target), { recursive: true });
23714
24487
  try {
23715
24488
  await copyFile2(source, target, constants2.COPYFILE_EXCL);
23716
24489
  } catch (error) {
@@ -23725,9 +24498,9 @@ async function copyTree(source, target, result) {
23725
24498
  }
23726
24499
  async function moveVault(from, to) {
23727
24500
  const result = { copied: 0, skipped: 0 };
23728
- await mkdir16(to, { recursive: true });
24501
+ await mkdir18(to, { recursive: true });
23729
24502
  for (const entry of MOVED_ENTRIES) {
23730
- await copyTree(path61.join(from, entry), path61.join(to, entry), result);
24503
+ await copyTree(path63.join(from, entry), path63.join(to, entry), result);
23731
24504
  }
23732
24505
  return result;
23733
24506
  }
@@ -23759,16 +24532,16 @@ function checkVaultTarget(current, target, options) {
23759
24532
  return { kind: "ok" };
23760
24533
  }
23761
24534
  async function canWriteInto(dir) {
23762
- const probe = temporaryPathFor(path61.join(dir, ".escritura"));
24535
+ const probe = temporaryPathFor(path63.join(dir, ".escritura"));
23763
24536
  try {
23764
- await mkdir16(dir, { recursive: true });
23765
- if (!(await stat39(dir)).isDirectory()) return false;
23766
- await writeFile10(probe, "");
24537
+ await mkdir18(dir, { recursive: true });
24538
+ if (!(await stat40(dir)).isDirectory()) return false;
24539
+ await writeFile11(probe, "");
23767
24540
  return true;
23768
24541
  } catch {
23769
24542
  return false;
23770
24543
  } finally {
23771
- await rm5(probe, { force: true }).catch(() => void 0);
24544
+ await rm7(probe, { force: true }).catch(() => void 0);
23772
24545
  }
23773
24546
  }
23774
24547
 
@@ -23865,12 +24638,12 @@ function serializeSession(input) {
23865
24638
  }
23866
24639
 
23867
24640
  // packages/server/src/vault/writer.ts
23868
- import { readdir as readdir18, rm as rm6, stat as stat41 } from "node:fs/promises";
23869
- import path63 from "node:path";
24641
+ import { readdir as readdir20, rm as rm8, stat as stat42 } from "node:fs/promises";
24642
+ import path65 from "node:path";
23870
24643
 
23871
24644
  // packages/server/src/vault/memory-copy.ts
23872
- import { mkdir as mkdir17, readdir as readdir17, readFile as readFile27, stat as stat40, utimes as utimes2 } from "node:fs/promises";
23873
- import path62 from "node:path";
24645
+ import { mkdir as mkdir19, readdir as readdir19, readFile as readFile28, stat as stat41, utimes as utimes2 } from "node:fs/promises";
24646
+ import path64 from "node:path";
23874
24647
  var MEMORY_FILE_MAX_BYTES = 1024 * 1024;
23875
24648
  var MEMORY_MAX_FILES = 500;
23876
24649
  var MEMORY_SOURCE_FILE = "source.json";
@@ -23885,7 +24658,7 @@ async function listMemoryNotes(cwd) {
23885
24658
  }
23886
24659
  let entries;
23887
24660
  try {
23888
- entries = await readdir17(folder, { withFileTypes: true });
24661
+ entries = await readdir19(folder, { withFileTypes: true });
23889
24662
  } catch {
23890
24663
  return [];
23891
24664
  }
@@ -23895,7 +24668,7 @@ async function listMemoryNotes(cwd) {
23895
24668
  let absolute;
23896
24669
  try {
23897
24670
  absolute = await resolveInside(cwd, `${MEMORY_DIR}/${name}`, { mustExist: true });
23898
- const info = await stat40(absolute);
24671
+ const info = await stat41(absolute);
23899
24672
  if (!info.isFile() || info.size > MEMORY_FILE_MAX_BYTES) continue;
23900
24673
  notes.push({ name, absolute, size: info.size, mtimeMs: info.mtimeMs });
23901
24674
  } catch {
@@ -23909,7 +24682,7 @@ async function measureProjectMemory(cwd) {
23909
24682
  }
23910
24683
  async function isUpToDate(target, note) {
23911
24684
  try {
23912
- const info = await stat40(target);
24685
+ const info = await stat41(target);
23913
24686
  return info.isFile() && info.size === note.size && Math.abs(info.mtimeMs - note.mtimeMs) < SAME_MTIME_MS;
23914
24687
  } catch {
23915
24688
  return false;
@@ -23917,7 +24690,7 @@ async function isUpToDate(target, note) {
23917
24690
  }
23918
24691
  async function exists3(file) {
23919
24692
  try {
23920
- await stat40(file);
24693
+ await stat41(file);
23921
24694
  return true;
23922
24695
  } catch {
23923
24696
  return false;
@@ -23933,14 +24706,14 @@ async function copyProjectMemory(dir, cwd, platform, deps = DEFAULT_WRITE_DEPS)
23933
24706
  if (prepared) return;
23934
24707
  prepared = true;
23935
24708
  await ensureVaultMarker(dir, deps);
23936
- await mkdir17(target, { recursive: true });
24709
+ await mkdir19(target, { recursive: true });
23937
24710
  };
23938
24711
  for (const note of notes) {
23939
- const destination = path62.join(target, note.name);
24712
+ const destination = path64.join(target, note.name);
23940
24713
  if (await isUpToDate(destination, note)) continue;
23941
24714
  let content;
23942
24715
  try {
23943
- content = await readFile27(note.absolute);
24716
+ content = await readFile28(note.absolute);
23944
24717
  } catch {
23945
24718
  continue;
23946
24719
  }
@@ -23952,7 +24725,7 @@ async function copyProjectMemory(dir, cwd, platform, deps = DEFAULT_WRITE_DEPS)
23952
24725
  result.files += 1;
23953
24726
  result.bytes += content.length;
23954
24727
  }
23955
- const source = path62.join(target, MEMORY_SOURCE_FILE);
24728
+ const source = path64.join(target, MEMORY_SOURCE_FILE);
23956
24729
  if (result.files > 0 || !await exists3(source)) {
23957
24730
  await prepare();
23958
24731
  await writeFileAtomic(source, `${JSON.stringify({ cwd, copiedAt: deps.now() })}
@@ -24446,7 +25219,7 @@ var VaultWriter = class {
24446
25219
  }
24447
25220
  async assetOnDisk(dir, agent, sessionId, asset, bytes) {
24448
25221
  try {
24449
- const info = await stat41(assetFile(dir, agent, sessionId, asset));
25222
+ const info = await stat42(assetFile(dir, agent, sessionId, asset));
24450
25223
  return info.isFile() && info.size === bytes;
24451
25224
  } catch {
24452
25225
  return false;
@@ -24546,30 +25319,30 @@ var VaultWriter = class {
24546
25319
  const folders = [dir];
24547
25320
  const subfolders = async (parent) => {
24548
25321
  try {
24549
- return (await readdir18(parent, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => path63.join(parent, entry.name));
25322
+ return (await readdir20(parent, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => path65.join(parent, entry.name));
24550
25323
  } catch {
24551
25324
  return [];
24552
25325
  }
24553
25326
  };
24554
25327
  for (const agentDir of await subfolders(sessionsRoot2(dir))) {
24555
- if (!SESSION_AGENT_IDS.includes(path63.basename(agentDir))) continue;
25328
+ if (!SESSION_AGENT_IDS.includes(path65.basename(agentDir))) continue;
24556
25329
  folders.push(agentDir);
24557
25330
  folders.push(...(await subfolders(agentDir)).filter((folder) => folder.endsWith(".assets")));
24558
25331
  }
24559
- folders.push(...await subfolders(path63.join(dir, "memory")));
25332
+ folders.push(...await subfolders(path65.join(dir, "memory")));
24560
25333
  for (const folder of folders) {
24561
25334
  let names;
24562
25335
  try {
24563
- names = await readdir18(folder);
25336
+ names = await readdir20(folder);
24564
25337
  } catch {
24565
25338
  continue;
24566
25339
  }
24567
25340
  for (const name of names) {
24568
25341
  if (!OWN_TEMP_FILE2.test(name)) continue;
24569
- const file = path63.join(folder, name);
25342
+ const file = path65.join(folder, name);
24570
25343
  try {
24571
- const info = await stat41(file);
24572
- if (info.isFile() && now - info.mtimeMs > TEMP_MAX_AGE_MS) await rm6(file, { force: true });
25344
+ const info = await stat42(file);
25345
+ if (info.isFile() && now - info.mtimeMs > TEMP_MAX_AGE_MS) await rm8(file, { force: true });
24573
25346
  } catch {
24574
25347
  }
24575
25348
  }
@@ -24756,14 +25529,14 @@ var VaultService = class {
24756
25529
  async setDir(target) {
24757
25530
  const { platform, agents, settings: settings2, catalog } = this.options;
24758
25531
  const current = this.dir();
24759
- const protectedDirs = [...agents.protectedDirs(), path64.join(this.options.homeDir ?? homedir15(), ".gemini")];
25532
+ const protectedDirs = [...agents.protectedDirs(), path66.join(this.options.homeDir ?? homedir15(), ".gemini")];
24760
25533
  const check = checkVaultTarget(current, target, { protectedDirs, platform });
24761
25534
  if (check.kind === "refused") throw new VaultError(check.text);
24762
25535
  if (check.kind === "same") return;
24763
25536
  try {
24764
25537
  await this.writer.exclusive("moving", async () => {
24765
25538
  if (!await canWriteInto(target)) throw new VaultError(serverText("vaultCannotWrite"));
24766
- const hadCopy = await stat42(vaultMarkerFile(current)).then(
25539
+ const hadCopy = await stat43(vaultMarkerFile(current)).then(
24767
25540
  () => true,
24768
25541
  () => false
24769
25542
  );
@@ -24797,7 +25570,7 @@ var VaultService = class {
24797
25570
  /** Abre la carpeta de la copia con el explorador del sistema. */
24798
25571
  async reveal() {
24799
25572
  const dir = this.activeDir();
24800
- const isFolder = await stat42(dir).then(
25573
+ const isFolder = await stat43(dir).then(
24801
25574
  (info) => info.isDirectory(),
24802
25575
  () => false
24803
25576
  );
@@ -24821,7 +25594,7 @@ var VaultService = class {
24821
25594
  fromCopy: true
24822
25595
  });
24823
25596
  const file = sessionExportFile(dir, agent, sessionId);
24824
- await mkdir18(path64.dirname(file), { recursive: true });
25597
+ await mkdir20(path66.dirname(file), { recursive: true });
24825
25598
  await writeFileAtomic(file, text, this.writeDeps);
24826
25599
  this.openWithSystem(file);
24827
25600
  return file;
@@ -24908,7 +25681,7 @@ var VaultService = class {
24908
25681
  const project = this.options.index.getProjects().find((candidate) => candidate.key === projectKey);
24909
25682
  if (project === void 0) throw new VaultError(serverText("vaultProjectGone"));
24910
25683
  const folder = projectExportDirFor(this.activeDir(), project, this.options.platform);
24911
- await mkdir18(folder, { recursive: true });
25684
+ await mkdir20(folder, { recursive: true });
24912
25685
  const entries = [];
24913
25686
  const names = /* @__PURE__ */ new Set();
24914
25687
  let skipped = 0;
@@ -24925,7 +25698,7 @@ var VaultService = class {
24925
25698
  continue;
24926
25699
  }
24927
25700
  const fileName = this.exportFileName(summary, names);
24928
- await writeFileAtomic(path64.join(folder, fileName), text, this.writeDeps);
25701
+ await writeFileAtomic(path66.join(folder, fileName), text, this.writeDeps);
24929
25702
  entries.push({
24930
25703
  updatedAt: summary.updatedAt,
24931
25704
  agentLabel: this.labelOf(summary.agent),
@@ -24936,9 +25709,9 @@ var VaultService = class {
24936
25709
  });
24937
25710
  await nextTurn3();
24938
25711
  }
24939
- const name = project.cwd.length > 0 ? path64.basename(project.cwd) || project.cwd : project.fallbackName;
25712
+ const name = project.cwd.length > 0 ? path66.basename(project.cwd) || project.cwd : project.fallbackName;
24940
25713
  const index = renderProjectIndex({ name, cwd: project.cwd, exportedAt: this.now(), entries, skipped });
24941
- await writeFileAtomic(path64.join(folder, "index.md"), index, this.writeDeps);
25714
+ await writeFileAtomic(path66.join(folder, "index.md"), index, this.writeDeps);
24942
25715
  this.openWithSystem(folder);
24943
25716
  return { sessions: entries.length, skipped, folder };
24944
25717
  }
@@ -25076,14 +25849,14 @@ var VaultService = class {
25076
25849
  // packages/server/src/worktrees/service.ts
25077
25850
  import { randomBytes as randomBytes6, randomUUID as randomUUID7 } from "node:crypto";
25078
25851
  import { EventEmitter as EventEmitter7 } from "node:events";
25079
- import { lstat as lstat5, mkdir as mkdir22, rename as rename7, rm as rm10, rmdir, stat as stat45 } from "node:fs/promises";
25080
- import path72 from "node:path";
25852
+ import { lstat as lstat5, mkdir as mkdir24, rename as rename7, rm as rm12, rmdir, stat as stat46 } from "node:fs/promises";
25853
+ import path74 from "node:path";
25081
25854
 
25082
25855
  // packages/server/src/worktrees/git.ts
25083
25856
  import { execFile as execFile6 } from "node:child_process";
25084
- import { mkdtemp, rm as rm7, stat as stat43 } from "node:fs/promises";
25857
+ import { mkdtemp, rm as rm9, stat as stat44 } from "node:fs/promises";
25085
25858
  import { tmpdir as tmpdir5 } from "node:os";
25086
- import path65 from "node:path";
25859
+ import path67 from "node:path";
25087
25860
  var READ_TIMEOUT_MS = 15e3;
25088
25861
  var WRITE_TIMEOUT_MS = 12e4;
25089
25862
  var MAX_BUFFER = 32 * 1024 * 1024;
@@ -25129,10 +25902,10 @@ async function repoLayout(cwd) {
25129
25902
  if (result.code !== 0) return null;
25130
25903
  const [toplevel, gitDir, commonDir] = result.stdout.split(/\r?\n/).map((line) => line.trim());
25131
25904
  if (!toplevel || !gitDir || !commonDir) return null;
25132
- const absoluteCommon = path65.resolve(cwd, commonDir);
25133
- const linked = path65.resolve(gitDir).toLowerCase() !== absoluteCommon.toLowerCase();
25134
- const mainRoot = path65.basename(absoluteCommon).toLowerCase() === ".git" ? path65.dirname(absoluteCommon) : null;
25135
- return { toplevel: path65.resolve(toplevel), mainRoot, linked };
25905
+ const absoluteCommon = path67.resolve(cwd, commonDir);
25906
+ const linked = path67.resolve(gitDir).toLowerCase() !== absoluteCommon.toLowerCase();
25907
+ const mainRoot = path67.basename(absoluteCommon).toLowerCase() === ".git" ? path67.dirname(absoluteCommon) : null;
25908
+ return { toplevel: path67.resolve(toplevel), mainRoot, linked };
25136
25909
  }
25137
25910
  async function currentBranch(cwd) {
25138
25911
  const result = await read(cwd, ["symbolic-ref", "--quiet", "HEAD"]);
@@ -25171,18 +25944,18 @@ async function uncommittedPaths(cwd) {
25171
25944
  async function hasLoadedSubmodule(cwd) {
25172
25945
  const [modules, staged] = await Promise.all([read(cwd, ["rev-parse", "--git-path", "modules"]), read(cwd, ["ls-files", "--stage", "-z"])]);
25173
25946
  if (modules.code !== 0 || staged.code !== 0) return null;
25174
- if (await isDirectory4(path65.resolve(cwd, modules.stdout.trim()))) return true;
25947
+ if (await isDirectory4(path67.resolve(cwd, modules.stdout.trim()))) return true;
25175
25948
  const submodules = staged.stdout.split("\0").filter((line) => line.startsWith("160000 ")).map((line) => line.slice(line.indexOf(" ") + 1));
25176
25949
  for (const submodule of submodules) {
25177
- if (await exists4(path65.join(cwd, submodule, ".git"))) return true;
25950
+ if (await exists4(path67.join(cwd, submodule, ".git"))) return true;
25178
25951
  }
25179
25952
  return false;
25180
25953
  }
25181
25954
  async function isDirectory4(candidate) {
25182
- return (await stat43(candidate).catch(() => null))?.isDirectory() === true;
25955
+ return (await stat44(candidate).catch(() => null))?.isDirectory() === true;
25183
25956
  }
25184
25957
  async function exists4(candidate) {
25185
- return await stat43(candidate).catch(() => null) !== null;
25958
+ return await stat44(candidate).catch(() => null) !== null;
25186
25959
  }
25187
25960
  async function isIntegrated(cwd, branch, base) {
25188
25961
  const result = await read(cwd, ["merge-base", "--is-ancestor", `refs/heads/${branch}`, `refs/heads/${base}`]);
@@ -25330,7 +26103,7 @@ async function branchFileDiff(cwd, base, branch, file, oldPath) {
25330
26103
  async function objectsDir(mainRoot) {
25331
26104
  const result = await read(mainRoot, ["rev-parse", "--git-path", "objects"]);
25332
26105
  const answer = result.stdout.trim();
25333
- return result.code === 0 && answer.length > 0 ? path65.resolve(mainRoot, answer) : null;
26106
+ return result.code === 0 && answer.length > 0 ? path67.resolve(mainRoot, answer) : null;
25334
26107
  }
25335
26108
  function alternatesValue(folder, platform = process.platform) {
25336
26109
  const separator = platform === "win32" ? ";" : ":";
@@ -25340,7 +26113,7 @@ function alternatesValue(folder, platform = process.platform) {
25340
26113
  async function mergeTreeCheck(mainRoot, base, branch) {
25341
26114
  const objects = await objectsDir(mainRoot);
25342
26115
  if (objects === null) return null;
25343
- const scratch = await mkdtemp(path65.join(tmpdir5(), "aw-merge-tree-")).catch(() => null);
26116
+ const scratch = await mkdtemp(path67.join(tmpdir5(), "aw-merge-tree-")).catch(() => null);
25344
26117
  if (scratch === null) return null;
25345
26118
  try {
25346
26119
  const result = await read(
@@ -25351,7 +26124,7 @@ async function mergeTreeCheck(mainRoot, base, branch) {
25351
26124
  );
25352
26125
  return parseMergeTree(result);
25353
26126
  } finally {
25354
- await rm7(scratch, { recursive: true, force: true, maxRetries: 3 }).catch(() => void 0);
26127
+ await rm9(scratch, { recursive: true, force: true, maxRetries: 3 }).catch(() => void 0);
25355
26128
  }
25356
26129
  }
25357
26130
  function parseMergeTree(result) {
@@ -25392,7 +26165,7 @@ function fastForward(baseFolder, target) {
25392
26165
 
25393
26166
  // packages/server/src/worktrees/holders.ts
25394
26167
  import { spawn as spawn4 } from "node:child_process";
25395
- import path66 from "node:path";
26168
+ import path68 from "node:path";
25396
26169
  var SCAN_MAX_ENTRIES = 2e5;
25397
26170
  var SCAN_MAX_MS = 4e3;
25398
26171
  var HOLDERS_TIMEOUT_MS = 15e3;
@@ -25545,7 +26318,7 @@ public static class AwFolderHolders {
25545
26318
  }
25546
26319
  `;
25547
26320
  function longPath(folder) {
25548
- const absolute = path66.win32.resolve(folder);
26321
+ const absolute = path68.win32.resolve(folder);
25549
26322
  if (absolute.startsWith("\\\\?\\")) return absolute;
25550
26323
  return absolute.startsWith("\\\\") ? `\\\\?\\UNC\\${absolute.slice(2)}` : `\\\\?\\${absolute}`;
25551
26324
  }
@@ -25580,7 +26353,7 @@ function parseHoldersOutput(output, selfPid) {
25580
26353
  }
25581
26354
  function powershellPath() {
25582
26355
  const systemRoot = process.env["SystemRoot"] ?? process.env["windir"] ?? "C:\\Windows";
25583
- return path66.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
26356
+ return path68.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
25584
26357
  }
25585
26358
  function runPowerShell(script) {
25586
26359
  return new Promise((resolve) => {
@@ -25629,8 +26402,8 @@ async function folderHolders(folder) {
25629
26402
 
25630
26403
  // packages/server/src/worktrees/include.ts
25631
26404
  import { createHash as createHash6 } from "node:crypto";
25632
- import { lstat as lstat3, mkdir as mkdir19, readdir as readdir19, readFile as readFile28, writeFile as writeFile11 } from "node:fs/promises";
25633
- import path67 from "node:path";
26405
+ import { lstat as lstat3, mkdir as mkdir21, readdir as readdir21, readFile as readFile29, writeFile as writeFile12 } from "node:fs/promises";
26406
+ import path69 from "node:path";
25634
26407
 
25635
26408
  // packages/server/src/worktrees/merge-notes.ts
25636
26409
  var MERGE_MAX_EDITS = 2e3;
@@ -25806,7 +26579,7 @@ var CLI_CONFIG_DIRS = /* @__PURE__ */ new Set([".claude", ".codex", ".gemini", "
25806
26579
  var CLI_CONFIG_FILES = /* @__PURE__ */ new Set([".mcp.json", "opencode.json", "opencode.jsonc"]);
25807
26580
  var INCLUDE_FILE = ".worktreeinclude";
25808
26581
  var INCLUDE_FILE_MAX_BYTES = 64 * 1024;
25809
- var toPosix2 = (value) => value.split(path67.sep).join("/");
26582
+ var toPosix2 = (value) => value.split(path69.sep).join("/");
25810
26583
  function hashOf(content) {
25811
26584
  return createHash6("sha256").update(content).digest("hex");
25812
26585
  }
@@ -25857,7 +26630,7 @@ async function walkEntry(mainRoot, relative, out, budget) {
25857
26630
  if (!info.isDirectory()) return;
25858
26631
  let names;
25859
26632
  try {
25860
- names = (await readdir19(absolute)).sort();
26633
+ names = (await readdir21(absolute)).sort();
25861
26634
  } catch {
25862
26635
  return;
25863
26636
  }
@@ -25911,10 +26684,10 @@ async function copyToWorktree(mainRoot, folder, files, bases) {
25911
26684
  const source = await resolveInside(mainRoot, file.path, { mustExist: true });
25912
26685
  const info = await lstat3(source);
25913
26686
  if (!info.isFile()) continue;
25914
- content = await readFile28(source);
26687
+ content = await readFile29(source);
25915
26688
  const target = await resolveInside(folder, file.path);
25916
- await mkdir19(path67.dirname(target), { recursive: true });
25917
- await writeFile11(target, content, { flag: "wx" });
26689
+ await mkdir21(path69.dirname(target), { recursive: true });
26690
+ await writeFile12(target, content, { flag: "wx" });
25918
26691
  copied.push({ path: file.path, hash: hashOf(content) });
25919
26692
  } catch (error) {
25920
26693
  console.warn(`[worktrees] ${file.path} wasn't copied to the branch folder:`, error);
@@ -25931,7 +26704,7 @@ async function readInside(root, relative) {
25931
26704
  const absolute = await resolveInside(root, relative, { mustExist: true });
25932
26705
  const info = await lstat3(absolute);
25933
26706
  if (!info.isFile()) return null;
25934
- return await readFile28(absolute);
26707
+ return await readFile29(absolute);
25935
26708
  } catch {
25936
26709
  return null;
25937
26710
  }
@@ -26000,7 +26773,7 @@ async function filesUnder(folder, relative, out) {
26000
26773
  return;
26001
26774
  }
26002
26775
  if (!info.isDirectory()) return;
26003
- for (const name of (await readdir19(absolute).catch(() => [])).sort()) {
26776
+ for (const name of (await readdir21(absolute).catch(() => [])).sort()) {
26004
26777
  await filesUnder(folder, `${relative}/${name}`, out);
26005
26778
  }
26006
26779
  }
@@ -26053,7 +26826,7 @@ async function returnNotes(mainRoot, folder, files, bases, options = {}) {
26053
26826
  result.skipped.push(file);
26054
26827
  continue;
26055
26828
  }
26056
- await mkdir19(path67.dirname(target), { recursive: true });
26829
+ await mkdir21(path69.dirname(target), { recursive: true });
26057
26830
  await writeAtomically(target, outcome.content, {
26058
26831
  beforeRename: async () => {
26059
26832
  const now = await readInside(mainRoot, file);
@@ -26075,15 +26848,15 @@ async function returnNotes(mainRoot, folder, files, bases, options = {}) {
26075
26848
  }
26076
26849
 
26077
26850
  // packages/server/src/worktrees/names.ts
26078
- import path68 from "node:path";
26079
- function worktreeFolderFor(mainRoot, branch, pathApi = path68) {
26851
+ import path70 from "node:path";
26852
+ function worktreeFolderFor(mainRoot, branch, pathApi = path70) {
26080
26853
  const parent = pathApi.dirname(mainRoot);
26081
26854
  return pathApi.join(parent, worktreesDirName(pathApi.basename(mainRoot)), worktreeFolderName(branch));
26082
26855
  }
26083
- function worktreesParentFor(mainRoot, pathApi = path68) {
26856
+ function worktreesParentFor(mainRoot, pathApi = path70) {
26084
26857
  return pathApi.join(pathApi.dirname(mainRoot), worktreesDirName(pathApi.basename(mainRoot)));
26085
26858
  }
26086
- function mainRootFromShape(folder, pathApi = path68) {
26859
+ function mainRootFromShape(folder, pathApi = path70) {
26087
26860
  const parent = pathApi.dirname(folder);
26088
26861
  const parentName = pathApi.basename(parent);
26089
26862
  if (parentName.length > ".worktrees".length && parentName.toLowerCase().endsWith(".worktrees")) {
@@ -26098,8 +26871,8 @@ function mainRootFromShape(folder, pathApi = path68) {
26098
26871
 
26099
26872
  // packages/server/src/worktrees/note-bases.ts
26100
26873
  import { createHash as createHash7 } from "node:crypto";
26101
- import { mkdir as mkdir20, readdir as readdir20, readFile as readFile29, rm as rm8, stat as stat44, utimes as utimes3 } from "node:fs/promises";
26102
- import path69 from "node:path";
26874
+ import { mkdir as mkdir22, readdir as readdir22, readFile as readFile30, rm as rm10, stat as stat45, utimes as utimes3 } from "node:fs/promises";
26875
+ import path71 from "node:path";
26103
26876
  var TEXT_NOTE_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".mdx", ".markdown", ".txt", ".rst", ".adoc"]);
26104
26877
  var NOTE_BASE_MAX_BYTES = 2 * 1024 * 1024;
26105
26878
  var PRUNE_GRACE_MS = 10 * 6e4;
@@ -26108,7 +26881,7 @@ function hashOf2(content) {
26108
26881
  return createHash7("sha256").update(content).digest("hex");
26109
26882
  }
26110
26883
  function keepsNoteBase(relative, content) {
26111
- return TEXT_NOTE_EXTENSIONS.has(path69.posix.extname(relative).toLowerCase()) && content.length <= NOTE_BASE_MAX_BYTES && !content.subarray(0, 8e3).includes(0);
26884
+ return TEXT_NOTE_EXTENSIONS.has(path71.posix.extname(relative).toLowerCase()) && content.length <= NOTE_BASE_MAX_BYTES && !content.subarray(0, 8e3).includes(0);
26112
26885
  }
26113
26886
  var NoteBases = class {
26114
26887
  constructor(dir = worktreeNotesDir(), options = {}) {
@@ -26128,21 +26901,21 @@ var NoteBases = class {
26128
26901
  async save(relative, content) {
26129
26902
  if (!keepsNoteBase(relative, content)) return;
26130
26903
  const hash = hashOf2(content);
26131
- const file = path69.join(this.dir, hash);
26132
- const current = await readFile29(file).catch(() => null);
26904
+ const file = path71.join(this.dir, hash);
26905
+ const current = await readFile30(file).catch(() => null);
26133
26906
  if (current !== null && hashOf2(current) === hash) {
26134
26907
  const now = new Date(this.now());
26135
26908
  await utimes3(file, now, now).catch(() => void 0);
26136
26909
  return;
26137
26910
  }
26138
- await mkdir20(this.dir, { recursive: true });
26911
+ await mkdir22(this.dir, { recursive: true });
26139
26912
  await writeAtomically(file, content);
26140
26913
  }
26141
26914
  /** La copia de origen con esa huella, o null si no hay (o lo que hay no es eso). */
26142
26915
  async load(hash) {
26143
26916
  if (!HASH.test(hash)) return null;
26144
26917
  try {
26145
- const content = await readFile29(path69.join(this.dir, hash));
26918
+ const content = await readFile30(path71.join(this.dir, hash));
26146
26919
  return hashOf2(content) === hash ? content : null;
26147
26920
  } catch {
26148
26921
  return null;
@@ -26152,18 +26925,18 @@ var NoteBases = class {
26152
26925
  async prune(keep) {
26153
26926
  let names;
26154
26927
  try {
26155
- names = await readdir20(this.dir);
26928
+ names = await readdir22(this.dir);
26156
26929
  } catch {
26157
26930
  return;
26158
26931
  }
26159
26932
  const now = this.now();
26160
26933
  for (const name of names) {
26161
26934
  if (!HASH.test(name) || keep.has(name)) continue;
26162
- const file = path69.join(this.dir, name);
26935
+ const file = path71.join(this.dir, name);
26163
26936
  try {
26164
- const info = await stat44(file);
26937
+ const info = await stat45(file);
26165
26938
  if (!info.isFile() || now - info.mtimeMs < this.graceMs) continue;
26166
- await rm8(file, { force: true });
26939
+ await rm10(file, { force: true });
26167
26940
  } catch {
26168
26941
  }
26169
26942
  }
@@ -26248,9 +27021,9 @@ function readVerdict(reply) {
26248
27021
  }
26249
27022
 
26250
27023
  // packages/server/src/worktrees/store.ts
26251
- import { mkdir as mkdir21, readFile as readFile30 } from "node:fs/promises";
26252
- import path70 from "node:path";
26253
- var STATE_VERSION5 = 1;
27024
+ import { mkdir as mkdir23, readFile as readFile31 } from "node:fs/promises";
27025
+ import path72 from "node:path";
27026
+ var STATE_VERSION6 = 1;
26254
27027
  var MAX_REMOVED = 300;
26255
27028
  var MAX_REVIEW_SESSIONS = 20;
26256
27029
  function parseNoteRequest(value) {
@@ -26330,7 +27103,7 @@ function parseWorktreeRecord(value) {
26330
27103
  function parseWorktreesState(raw) {
26331
27104
  try {
26332
27105
  const parsed = asRecord(JSON.parse(raw));
26333
- if (parsed === null || parsed["version"] !== STATE_VERSION5) return [];
27106
+ if (parsed === null || parsed["version"] !== STATE_VERSION6) return [];
26334
27107
  return asArrayFiltered(parsed["worktrees"], parseWorktreeRecord) ?? [];
26335
27108
  } catch {
26336
27109
  return [];
@@ -26345,7 +27118,7 @@ var WorktreeStore = class {
26345
27118
  writing = Promise.resolve();
26346
27119
  async load() {
26347
27120
  try {
26348
- this.records = parseWorktreesState(await readFile30(this.file, "utf8"));
27121
+ this.records = parseWorktreesState(await readFile31(this.file, "utf8"));
26349
27122
  } catch {
26350
27123
  this.records = [];
26351
27124
  }
@@ -26385,9 +27158,9 @@ var WorktreeStore = class {
26385
27158
  );
26386
27159
  this.records = this.records.filter((record) => !forget.has(record.id));
26387
27160
  }
26388
- const content = Buffer.from(JSON.stringify({ version: STATE_VERSION5, worktrees: this.records }, null, 2), "utf8");
27161
+ const content = Buffer.from(JSON.stringify({ version: STATE_VERSION6, worktrees: this.records }, null, 2), "utf8");
26389
27162
  this.writing = this.writing.catch(() => void 0).then(async () => {
26390
- await mkdir21(path70.dirname(this.file), { recursive: true });
27163
+ await mkdir23(path72.dirname(this.file), { recursive: true });
26391
27164
  await writeAtomically(this.file, content);
26392
27165
  });
26393
27166
  return this.writing;
@@ -26396,8 +27169,8 @@ var WorktreeStore = class {
26396
27169
 
26397
27170
  // packages/server/src/worktrees/untracked.ts
26398
27171
  import { createHash as createHash8 } from "node:crypto";
26399
- import { lstat as lstat4, readdir as readdir21, realpath as realpath7, rm as rm9 } from "node:fs/promises";
26400
- import path71 from "node:path";
27172
+ import { lstat as lstat4, readdir as readdir23, realpath as realpath7, rm as rm11 } from "node:fs/promises";
27173
+ import path73 from "node:path";
26401
27174
  var sameName = (a, b) => a.toLowerCase() === b.toLowerCase();
26402
27175
  function cleanEntry(entry) {
26403
27176
  const clean = entry.replace(/\/+$/, "");
@@ -26414,9 +27187,9 @@ async function scanUntracked(folder, entries, maxFiles = WORKTREE_UNTRACKED_MAX_
26414
27187
  let target;
26415
27188
  let real;
26416
27189
  try {
26417
- const parent = await resolveInside(folder, path71.posix.dirname(clean), { mustExist: true });
26418
- target = path71.join(parent, path71.posix.basename(clean));
26419
- real = path71.join(await realpath7(parent), path71.posix.basename(clean));
27190
+ const parent = await resolveInside(folder, path73.posix.dirname(clean), { mustExist: true });
27191
+ target = path73.join(parent, path73.posix.basename(clean));
27192
+ real = path73.join(await realpath7(parent), path73.posix.basename(clean));
26420
27193
  } catch {
26421
27194
  return null;
26422
27195
  }
@@ -26452,7 +27225,7 @@ async function visit(absolute, real, relative, walk2) {
26452
27225
  `);
26453
27226
  let names;
26454
27227
  try {
26455
- names = (await readdir21(absolute)).sort();
27228
+ names = (await readdir23(absolute)).sort();
26456
27229
  } catch {
26457
27230
  walk2.hash.update(`${relative}\0!
26458
27231
  `);
@@ -26463,14 +27236,14 @@ async function visit(absolute, real, relative, walk2) {
26463
27236
  return;
26464
27237
  }
26465
27238
  for (const name of names) {
26466
- await visit(path71.join(absolute, name), path71.join(real, name), `${relative}/${name}`, walk2);
27239
+ await visit(path73.join(absolute, name), path73.join(real, name), `${relative}/${name}`, walk2);
26467
27240
  if (walk2.blocked !== null) return;
26468
27241
  }
26469
27242
  }
26470
27243
  async function deleteUntracked(targets) {
26471
27244
  for (const target of targets) {
26472
27245
  try {
26473
- await rm9(target, { recursive: true, maxRetries: 3 });
27246
+ await rm11(target, { recursive: true, maxRetries: 3 });
26474
27247
  } catch (error) {
26475
27248
  if (error.code !== "ENOENT") throw error;
26476
27249
  }
@@ -26496,19 +27269,19 @@ var REPLY_EVENTS = 300;
26496
27269
  var REPLY_CACHE_MAX = 50;
26497
27270
  var REVIEW_ACTIVITY_DELAY_MS = 1e3;
26498
27271
  async function leftWithoutGit(folder) {
26499
- return await exists5(folder) && await lstat5(path72.join(folder, ".git")).catch(() => null) === null;
27272
+ return await exists5(folder) && await lstat5(path74.join(folder, ".git")).catch(() => null) === null;
26500
27273
  }
26501
27274
  async function exists5(candidate) {
26502
27275
  try {
26503
- await stat45(candidate);
27276
+ await stat46(candidate);
26504
27277
  return true;
26505
27278
  } catch {
26506
27279
  return false;
26507
27280
  }
26508
27281
  }
26509
27282
  function isInside4(outer, inner) {
26510
- const relative = path72.relative(path72.resolve(outer), path72.resolve(inner));
26511
- return relative.length === 0 || !relative.startsWith("..") && !path72.isAbsolute(relative);
27283
+ const relative = path74.relative(path74.resolve(outer), path74.resolve(inner));
27284
+ return relative.length === 0 || !relative.startsWith("..") && !path74.isAbsolute(relative);
26512
27285
  }
26513
27286
  function displayCommand(args) {
26514
27287
  return ["git", ...args].map((arg) => /[\s"]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg).join(" ");
@@ -26583,7 +27356,7 @@ var WorktreeService = class extends EventEmitter7 {
26583
27356
  return {
26584
27357
  mainRoot,
26585
27358
  parentDir: worktreesParentFor(mainRoot),
26586
- separator: path72.sep,
27359
+ separator: path74.sep,
26587
27360
  currentBranch: current,
26588
27361
  branches: branches.map((name) => ({ name, checkedOut: checkedOut.has(name) })),
26589
27362
  mainChanges: changes?.length ?? 0,
@@ -26607,7 +27380,7 @@ var WorktreeService = class extends EventEmitter7 {
26607
27380
  const folder = worktreeFolderFor(mainRoot, branch);
26608
27381
  if (await exists5(folder)) throw new WorktreeError(serverText("worktreeFolderExists", { folder }));
26609
27382
  const copy = await planCopy(mainRoot);
26610
- await mkdir22(path72.dirname(folder), { recursive: true });
27383
+ await mkdir24(path74.dirname(folder), { recursive: true });
26611
27384
  const added = await addWorktree(mainRoot, folder, branch, mode === "new" ? base : null);
26612
27385
  if (added.code !== 0) throw new WorktreeError(serverText("worktreeGitFailed", { detail: gitFailureText(added) }));
26613
27386
  const { copied, copiedDirs } = await this.copyInto(mainRoot, folder, copy, request.copyNotes);
@@ -26658,7 +27431,7 @@ var WorktreeService = class extends EventEmitter7 {
26658
27431
  const mainRoot = layout.mainRoot;
26659
27432
  const tips = await branchTips(mainRoot);
26660
27433
  const tipsStamp = [...tips].map(([branch, commit]) => `${branch}\0${commit}`).join("\n");
26661
- const cacheKey2 = path72.resolve(mainRoot).toLowerCase();
27434
+ const cacheKey2 = path74.resolve(mainRoot).toLowerCase();
26662
27435
  const cached = this.rowsCache.get(cacheKey2);
26663
27436
  const fresh = cached !== void 0 && cached.tips === tipsStamp && this.now() - cached.at < ROWS_TTL_MS;
26664
27437
  const generation = this.rowsGeneration;
@@ -26719,7 +27492,7 @@ var WorktreeService = class extends EventEmitter7 {
26719
27492
  const baseTip = tips.get(base);
26720
27493
  const branchTip = tips.get(branch);
26721
27494
  if (baseTip === void 0 || branchTip === void 0) return Promise.resolve(null);
26722
- const key = [path72.resolve(mainRoot).toLowerCase(), base, branch, baseTip, branchTip].join("\0");
27495
+ const key = [path74.resolve(mainRoot).toLowerCase(), base, branch, baseTip, branchTip].join("\0");
26723
27496
  const cached = this.pairs.get(key);
26724
27497
  if (cached !== void 0) {
26725
27498
  this.pairs.delete(key);
@@ -26769,7 +27542,7 @@ var WorktreeService = class extends EventEmitter7 {
26769
27542
  }
26770
27543
  /** Las pestañas y consolas abiertas dentro de la carpeta de la rama. */
26771
27544
  tabsIn(folder) {
26772
- return this.deps.registry.list().filter((tab) => isInside4(folder, tab.cwd)).map((tab) => ({ terminalId: tab.terminalId, kind: tab.kind, label: tab.label || path72.basename(tab.cwd) }));
27545
+ return this.deps.registry.list().filter((tab) => isInside4(folder, tab.cwd)).map((tab) => ({ terminalId: tab.terminalId, kind: tab.kind, label: tab.label || path74.basename(tab.cwd) }));
26773
27546
  }
26774
27547
  /**
26775
27548
  * Las pestañas de una carpeta de trabajo, sin las de otra que viva adentro
@@ -26802,7 +27575,7 @@ var WorktreeService = class extends EventEmitter7 {
26802
27575
  });
26803
27576
  const copied = [];
26804
27577
  for (const file of record.copied) {
26805
- if (entryOf(file.path) && await exists5(path72.join(record.path, file.path))) copied.push(file.path);
27578
+ if (entryOf(file.path) && await exists5(path74.join(record.path, file.path))) copied.push(file.path);
26806
27579
  }
26807
27580
  if (copied.length > 0) {
26808
27581
  const ignored = await ignoredAmong(record.path, copied);
@@ -26950,11 +27723,11 @@ var WorktreeService = class extends EventEmitter7 {
26950
27723
  * contenga, ni una carpeta que git todavía tenga registrada.
26951
27724
  */
26952
27725
  async removeLeftovers(record) {
26953
- const folder = path72.resolve(record.path);
27726
+ const folder = path74.resolve(record.path);
26954
27727
  if (!await exists5(folder)) return this.removeEmptyParent(folder);
26955
- if (isInside4(folder, record.mainRoot) || path72.dirname(folder) === folder) return;
27728
+ if (isInside4(folder, record.mainRoot) || path74.dirname(folder) === folder) return;
26956
27729
  if ((await listWorktrees(record.mainRoot)).some((entry) => samePathShape(entry.path, folder))) return;
26957
- await rm10(folder, { recursive: true, maxRetries: 3 });
27730
+ await rm12(folder, { recursive: true, maxRetries: 3 });
26958
27731
  await this.removeEmptyParent(folder);
26959
27732
  }
26960
27733
  /**
@@ -26977,13 +27750,13 @@ var WorktreeService = class extends EventEmitter7 {
26977
27750
  for (const record of this.deps.store.list()) {
26978
27751
  if (record.removedAt !== null || !samePathShape(record.mainRoot, mainRoot)) continue;
26979
27752
  if (listed.some((entry) => samePathShape(entry.path, record.path)) || !await leftWithoutGit(record.path)) continue;
26980
- released.push({ path: path72.resolve(record.path), branch: record.branch, isMain: false, bare: false, locked: false, prunable: true });
27753
+ released.push({ path: path74.resolve(record.path), branch: record.branch, isMain: false, bare: false, locked: false, prunable: true });
26981
27754
  }
26982
27755
  return released;
26983
27756
  }
26984
27757
  /** La carpeta `<proyecto>.worktrees`, sólo si quedó vacía. */
26985
27758
  async removeEmptyParent(folder) {
26986
- await rmdir(path72.dirname(folder)).catch(() => void 0);
27759
+ await rmdir(path74.dirname(folder)).catch(() => void 0);
26987
27760
  }
26988
27761
  /**
26989
27762
  * Vuelve a abrir la rama de una sesión cuya carpeta se quitó: la saca otra
@@ -27007,7 +27780,7 @@ var WorktreeService = class extends EventEmitter7 {
27007
27780
  throw new WorktreeError(serverText("worktreeBranchInUse", { branch: record.branch }));
27008
27781
  }
27009
27782
  const copy = await planCopy(record.mainRoot);
27010
- await mkdir22(path72.dirname(record.path), { recursive: true });
27783
+ await mkdir24(path74.dirname(record.path), { recursive: true });
27011
27784
  const added = await addWorktree(record.mainRoot, record.path, record.branch, recreateAt);
27012
27785
  if (added.code !== 0) throw new WorktreeError(serverText("worktreeGitFailed", { detail: gitFailureText(added) }));
27013
27786
  const { copied, copiedDirs } = await this.copyInto(record.mainRoot, record.path, copy, record.copyNotes);
@@ -27094,11 +27867,11 @@ var WorktreeService = class extends EventEmitter7 {
27094
27867
  return { branch: plan.branch };
27095
27868
  }
27096
27869
  removedRecordAbove(cwd) {
27097
- let current = path72.resolve(cwd);
27870
+ let current = path74.resolve(cwd);
27098
27871
  for (let depth = 0; depth < 24; depth += 1) {
27099
27872
  const record = this.deps.store.byPath(current);
27100
27873
  if (record !== null) return record.removedAt === null ? null : record;
27101
- const parent = path72.dirname(current);
27874
+ const parent = path74.dirname(current);
27102
27875
  if (parent === current) return null;
27103
27876
  current = parent;
27104
27877
  }
@@ -27207,8 +27980,8 @@ var WorktreeService = class extends EventEmitter7 {
27207
27980
  * reanudada; y si tampoco hay, una nueva en la carpeta de la rama.
27208
27981
  */
27209
27982
  async authorTarget(record) {
27210
- const open9 = this.authorTab(record);
27211
- if (open9 !== null) return { terminal: open9, opened: false };
27983
+ const open10 = this.authorTab(record);
27984
+ if (open10 !== null) return { terminal: open10, opened: false };
27212
27985
  const session = this.authorSession(record);
27213
27986
  if (session !== null && isAgentId(session.agent) && session.cwd.length > 0 && await exists5(session.cwd)) {
27214
27987
  const terminal = await this.deps.registry.open({ cwd: session.cwd, agent: session.agent, resumeSessionId: session.sessionId });
@@ -27547,6 +28320,549 @@ var WorktreeService = class extends EventEmitter7 {
27547
28320
  }
27548
28321
  };
27549
28322
 
28323
+ // packages/server/src/scheduled-messages/scheduler.ts
28324
+ import { randomUUID as randomUUID8 } from "node:crypto";
28325
+ var SCHEDULER_TICK_MS = 3e4;
28326
+ var SCHEDULER_NUDGE_MS = 300;
28327
+ var SCHEDULED_DELIVERY_TIMEOUT_MS = 6e4;
28328
+ var SCHEDULED_COOLDOWN_MS = 1e4;
28329
+ var SCHEDULED_OTHER_INSTANCE_GRACE_MS = 6e4;
28330
+ function scheduleCreateRefusal(input) {
28331
+ if (!input.enabled) return serverText("scheduleOff");
28332
+ if (input.owner === null || input.agentLabel === null) return serverText("scheduleNoConversation");
28333
+ if (!input.capable) return serverText("scheduleUnsupported", { label: input.agentLabel });
28334
+ if (input.text.trim().length === 0) return serverText("scheduleEmpty");
28335
+ if (input.text.length > MAX_SCHEDULED_TEXT_CHARS) return serverText("scheduleTooLong", { max: MAX_SCHEDULED_TEXT_CHARS });
28336
+ const problem = scheduleTimeProblem(input.sendAt, input.now);
28337
+ if (problem === "past") return serverText("schedulePast");
28338
+ if (problem === "too-far") return serverText("scheduleTooFar", { days: Math.round(MAX_SCHEDULE_AHEAD_MS / 864e5) });
28339
+ if (input.count >= MAX_SCHEDULED_MESSAGES) return serverText("scheduleTooMany", { max: MAX_SCHEDULED_MESSAGES });
28340
+ return null;
28341
+ }
28342
+ function scheduledFailureFor(outcome) {
28343
+ switch (outcome.kind) {
28344
+ case "unconfirmed":
28345
+ return serverText("scheduledUnconfirmed");
28346
+ case "sent":
28347
+ case "nothing":
28348
+ return null;
28349
+ case "gone":
28350
+ case "no-agent":
28351
+ return serverText("scheduledNoTab");
28352
+ case "no-session":
28353
+ case "no-ready-signal":
28354
+ case "no-images":
28355
+ case "not-ready":
28356
+ case "image-failed":
28357
+ return serverText("scheduledCliNotReady");
28358
+ case "interrupted":
28359
+ case "refused":
28360
+ return serverText("scheduledUncertainWrite");
28361
+ }
28362
+ }
28363
+ var defaultTimer2 = (callback, ms) => {
28364
+ const timer = setTimeout(callback, ms);
28365
+ timer.unref();
28366
+ return () => clearTimeout(timer);
28367
+ };
28368
+ var MessageScheduler = class {
28369
+ constructor(deps) {
28370
+ this.deps = deps;
28371
+ this.setTimer = deps.setTimer ?? defaultTimer2;
28372
+ }
28373
+ deps;
28374
+ /** Lo que esta pasando con cada mensaje en este proceso: esperando, mandandose. No se guarda. */
28375
+ runtime = /* @__PURE__ */ new Map();
28376
+ /** La primera vez que este proceso vio vencido cada mensaje: de ahi cuenta la hora de margen. */
28377
+ firstDue = /* @__PURE__ */ new Map();
28378
+ /** Los que se estan mandando ahora, desde este proceso. */
28379
+ inFlight = /* @__PURE__ */ new Set();
28380
+ /** La pestana de cada uno que se esta mandando: de a uno por pestana. */
28381
+ inFlightTabs = /* @__PURE__ */ new Map();
28382
+ /** Hasta cuando no se le manda otro a cada pestana (`SCHEDULED_COOLDOWN_MS`). */
28383
+ cooldownUntil = /* @__PURE__ */ new Map();
28384
+ /** Hito 52. Hasta cuando espera cada mensaje a la otra instancia (`SCHEDULED_OTHER_INSTANCE_GRACE_MS`). */
28385
+ graceUntil = /* @__PURE__ */ new Map();
28386
+ ticking = false;
28387
+ again = false;
28388
+ stopped = true;
28389
+ cancelTimer = null;
28390
+ timerAt = Number.POSITIVE_INFINITY;
28391
+ lastPublished = "";
28392
+ setTimer;
28393
+ /** Arranca: despues de restaurar las pestanas, o no sabria cual es la de cada mensaje. */
28394
+ start() {
28395
+ if (!this.stopped) return;
28396
+ this.stopped = false;
28397
+ void this.tick();
28398
+ }
28399
+ /** Para el reloj y suelta lo reservado: una instancia que se apaga no se queda con nada. */
28400
+ async stop() {
28401
+ this.stopped = true;
28402
+ this.cancelTimer?.();
28403
+ this.cancelTimer = null;
28404
+ this.timerAt = Number.POSITIVE_INFINITY;
28405
+ await this.deps.store.releaseReserved();
28406
+ }
28407
+ /** Algo cambio (el agente quedo libre, se toco un ajuste): una vuelta pronto. */
28408
+ nudge() {
28409
+ if (this.stopped) return;
28410
+ this.schedule(SCHEDULER_NUDGE_MS);
28411
+ }
28412
+ /** La lista tal como va a las ventanas, con la pestana de hoy de cada conversacion. */
28413
+ snapshot() {
28414
+ const tabs = this.deps.tabs();
28415
+ const now = this.deps.now();
28416
+ const enabled2 = this.deps.enabled();
28417
+ return this.deps.store.list().sort((a, b) => a.sendAt - b.sendAt || a.createdAt - b.createdAt).map((message) => {
28418
+ const tab = tabs.find((entry) => entry.agent === message.agent && entry.sessionId === message.sessionId);
28419
+ const running = this.runtime.get(message.id);
28420
+ let state;
28421
+ let note = null;
28422
+ if (message.failure !== void 0) {
28423
+ state = "failed";
28424
+ note = message.failure;
28425
+ } else if (this.inFlight.has(message.id)) {
28426
+ state = "sending";
28427
+ } else if (!enabled2) {
28428
+ state = "paused";
28429
+ } else if (running !== void 0) {
28430
+ state = running.state;
28431
+ note = running.note;
28432
+ } else {
28433
+ state = now < message.sendAt ? "scheduled" : "waiting";
28434
+ }
28435
+ return {
28436
+ id: message.id,
28437
+ agent: message.agent,
28438
+ sessionId: message.sessionId,
28439
+ terminalId: tab?.terminalId ?? null,
28440
+ text: message.text,
28441
+ sendAt: message.sendAt,
28442
+ createdAt: message.createdAt,
28443
+ state,
28444
+ note
28445
+ };
28446
+ });
28447
+ }
28448
+ /** Avisa la lista si cambio desde la ultima vez. Tambien cuando cambian las pestanas: cambia la de cada uno. */
28449
+ publish() {
28450
+ const list = this.snapshot();
28451
+ const key = JSON.stringify(list);
28452
+ if (key === this.lastPublished) return;
28453
+ this.lastPublished = key;
28454
+ this.deps.onChange(list);
28455
+ }
28456
+ async create(owner, agent, text, sendAt) {
28457
+ const message = {
28458
+ id: randomUUID8(),
28459
+ agent,
28460
+ sessionId: owner.sessionId,
28461
+ text,
28462
+ sendAt,
28463
+ createdAt: this.deps.now()
28464
+ };
28465
+ await this.deps.store.add(message);
28466
+ this.publish();
28467
+ this.nudge();
28468
+ return message;
28469
+ }
28470
+ /** Lo saca, si no se esta mandando. Su texto vuelve al cuadro: lo hace quien llama. */
28471
+ async cancel(id) {
28472
+ await this.deps.store.refresh();
28473
+ if (this.deps.store.get(id) === null) return { kind: "gone" };
28474
+ if (this.inFlight.has(id)) return { kind: "sending" };
28475
+ const claim = await this.deps.store.claimOf(id);
28476
+ if (claim !== null && !claim.mine && claim.alive && claim.phase === "writing") return { kind: "sending" };
28477
+ const removed = await this.deps.store.remove(id);
28478
+ if (removed === null) return { kind: "gone" };
28479
+ await this.deps.store.release(id);
28480
+ this.forget(id);
28481
+ this.publish();
28482
+ return { kind: "removed", message: removed };
28483
+ }
28484
+ /** Lo pone a la hora de ahora y le borra la falla: se manda con las reglas de siempre. */
28485
+ async sendNow(id) {
28486
+ if (!this.deps.enabled()) return "off";
28487
+ await this.deps.store.refresh();
28488
+ if (this.deps.store.get(id) === null) return "gone";
28489
+ if (this.inFlight.has(id)) return "sending";
28490
+ const claim = await this.deps.store.claimOf(id);
28491
+ if (claim !== null && !claim.mine && claim.alive && claim.phase === "writing") return "sending";
28492
+ const updated = await this.deps.store.update(id, { sendAt: this.deps.now(), failure: null });
28493
+ if (updated === null) return "gone";
28494
+ this.forget(id);
28495
+ this.publish();
28496
+ this.nudge();
28497
+ return "ok";
28498
+ }
28499
+ /** Se cerro la pestana: lo de su conversacion se cancela (la web lo pregunto antes). */
28500
+ async dropOwner(owner) {
28501
+ const removed = await this.deps.store.removeOwner(owner);
28502
+ for (const message of removed) {
28503
+ if (!this.inFlight.has(message.id)) await this.deps.store.release(message.id);
28504
+ this.forget(message.id);
28505
+ }
28506
+ if (removed.length > 0) this.publish();
28507
+ }
28508
+ /** La pestana cambio de conversacion: lo programado va con ella, como el borrador. */
28509
+ async moveOwner(from, to) {
28510
+ if (await this.deps.store.move(from, to) > 0) {
28511
+ this.publish();
28512
+ this.nudge();
28513
+ }
28514
+ }
28515
+ /** Una vuelta. Nunca dos a la vez: la que llega mientras corre otra se hace despues. */
28516
+ async tick() {
28517
+ if (this.stopped) return;
28518
+ if (this.ticking) {
28519
+ this.again = true;
28520
+ return;
28521
+ }
28522
+ this.ticking = true;
28523
+ try {
28524
+ await this.pass();
28525
+ } catch (error) {
28526
+ this.deps.log?.(`[scheduled] a pass failed: ${error instanceof Error ? error.message : String(error)}`);
28527
+ } finally {
28528
+ this.ticking = false;
28529
+ this.publish();
28530
+ if (this.again) {
28531
+ this.again = false;
28532
+ this.schedule(0);
28533
+ } else {
28534
+ this.scheduleNext();
28535
+ }
28536
+ }
28537
+ }
28538
+ // ---- por dentro ----
28539
+ forget(id) {
28540
+ this.runtime.delete(id);
28541
+ this.firstDue.delete(id);
28542
+ this.graceUntil.delete(id);
28543
+ }
28544
+ set(id, state, note) {
28545
+ this.runtime.set(id, { state, note });
28546
+ }
28547
+ /** true si a esa pestana ya se le esta mandando uno, o se le mando hace muy poco. */
28548
+ tabCooling(terminalId, now) {
28549
+ if ((this.cooldownUntil.get(terminalId) ?? 0) > now) return true;
28550
+ for (const tab of this.inFlightTabs.values()) {
28551
+ if (tab === terminalId) return true;
28552
+ }
28553
+ return false;
28554
+ }
28555
+ async fail(id, note) {
28556
+ await this.deps.store.update(id, { failure: note });
28557
+ await this.deps.store.release(id);
28558
+ this.set(id, "failed", note);
28559
+ this.firstDue.delete(id);
28560
+ }
28561
+ /** Una vuelta dentro de `ms`, salvo que ya haya una antes. */
28562
+ schedule(ms) {
28563
+ if (this.stopped) return;
28564
+ const at = this.deps.now() + ms;
28565
+ if (this.cancelTimer !== null && this.timerAt <= at) return;
28566
+ this.cancelTimer?.();
28567
+ this.timerAt = at;
28568
+ this.cancelTimer = this.setTimer(() => {
28569
+ this.cancelTimer = null;
28570
+ this.timerAt = Number.POSITIVE_INFINITY;
28571
+ void this.tick();
28572
+ }, ms);
28573
+ }
28574
+ /** La proxima vuelta: a la hora del proximo mensaje, o a los 30 s, lo que llegue antes. */
28575
+ scheduleNext() {
28576
+ const now = this.deps.now();
28577
+ let wait = SCHEDULER_TICK_MS;
28578
+ if (this.deps.enabled()) {
28579
+ for (const message of this.deps.store.list()) {
28580
+ if (message.failure !== void 0 || message.sendAt <= now) continue;
28581
+ wait = Math.min(wait, message.sendAt - now + 20);
28582
+ }
28583
+ for (const until of this.cooldownUntil.values()) {
28584
+ if (until > now) wait = Math.min(wait, until - now + 20);
28585
+ }
28586
+ for (const until of this.graceUntil.values()) {
28587
+ if (until > now) wait = Math.min(wait, until - now + 20);
28588
+ }
28589
+ }
28590
+ this.schedule(Math.max(0, wait));
28591
+ }
28592
+ async pass() {
28593
+ const { store } = this.deps;
28594
+ await store.refresh();
28595
+ await this.deps.refreshEnabled?.();
28596
+ const messages = store.list().sort((a, b) => a.sendAt - b.sendAt || a.createdAt - b.createdAt);
28597
+ const present = new Set(messages.map((message) => message.id));
28598
+ for (const id of [...this.runtime.keys()]) {
28599
+ if (!present.has(id)) this.forget(id);
28600
+ }
28601
+ for (const id of await store.claimedIds()) {
28602
+ if (present.has(id) || this.inFlight.has(id)) continue;
28603
+ await store.release(id);
28604
+ await store.discardOrphan(id);
28605
+ }
28606
+ const tabs = this.deps.tabs();
28607
+ for (const message of messages) {
28608
+ if (this.stopped) return;
28609
+ if (this.inFlight.has(message.id)) continue;
28610
+ await this.consider(message, tabs);
28611
+ }
28612
+ }
28613
+ async consider(message, tabs) {
28614
+ const { store } = this.deps;
28615
+ const { id } = message;
28616
+ const now = this.deps.now();
28617
+ if (message.failure !== void 0) {
28618
+ this.set(id, "failed", message.failure);
28619
+ return;
28620
+ }
28621
+ if (!this.deps.enabled()) {
28622
+ this.firstDue.delete(id);
28623
+ await store.release(id);
28624
+ this.set(id, "paused", null);
28625
+ return;
28626
+ }
28627
+ if (now < message.sendAt) {
28628
+ this.firstDue.delete(id);
28629
+ this.set(id, "scheduled", null);
28630
+ return;
28631
+ }
28632
+ const firstDue = this.firstDue.get(id) ?? now;
28633
+ this.firstDue.set(id, firstDue);
28634
+ const claim = await store.claimOf(id);
28635
+ if (claim !== null && !claim.mine) {
28636
+ if (claim.alive) {
28637
+ this.set(id, "waiting", serverText("scheduledWaitOtherInstance"));
28638
+ return;
28639
+ }
28640
+ if (claim.phase === "writing") {
28641
+ await this.fail(id, serverText("scheduledUncertain"));
28642
+ await store.discardOrphan(id);
28643
+ return;
28644
+ }
28645
+ }
28646
+ const mine = claim?.mine === true;
28647
+ if (!mine && firstDue - message.sendAt > SCHEDULE_LATE_LIMIT_MS) {
28648
+ await this.fail(id, serverText("scheduledMissed"));
28649
+ return;
28650
+ }
28651
+ const overdue = now - message.sendAt > SCHEDULE_LATE_LIMIT_MS;
28652
+ const tab = tabs.find((entry) => entry.agent === message.agent && entry.sessionId === message.sessionId) ?? null;
28653
+ if (tab === null) {
28654
+ if (mine) await store.release(id);
28655
+ if (overdue) await this.fail(id, serverText("scheduledNoTab"));
28656
+ else this.set(id, "waiting", serverText("scheduledWaitNoTab"));
28657
+ return;
28658
+ }
28659
+ const agent = this.deps.agentOf(tab.agent);
28660
+ if (agent === null || !agent.scheduledSend) {
28661
+ await this.fail(id, serverText("scheduleUnsupported", { label: agent?.label ?? tab.agent }));
28662
+ return;
28663
+ }
28664
+ if (tab.alive) {
28665
+ if (await store.claim(id) !== "mine") {
28666
+ this.set(id, "waiting", serverText("scheduledWaitOtherInstance"));
28667
+ return;
28668
+ }
28669
+ const free = await this.freeNow(tab);
28670
+ if (free === "idle" && !this.tabCooling(tab.terminalId, now)) {
28671
+ this.startDelivery(message, tab, false);
28672
+ return;
28673
+ }
28674
+ this.set(id, "waiting", free === "waiting" ? serverText("scheduledWaitAnswer") : serverText("scheduledWaitBusy"));
28675
+ return;
28676
+ }
28677
+ if (this.tabCooling(tab.terminalId, now)) {
28678
+ this.set(id, "waiting", serverText("scheduledWaitBusy"));
28679
+ return;
28680
+ }
28681
+ if (!mine && this.deps.tellsOpenElsewhere?.(tab.agent) === false && await this.deps.othersRunning?.() === true) {
28682
+ const graceEnd = firstDue + SCHEDULED_OTHER_INSTANCE_GRACE_MS;
28683
+ if (now < graceEnd) {
28684
+ this.graceUntil.set(id, graceEnd);
28685
+ this.set(id, "waiting", serverText("scheduledWaitGrace"));
28686
+ return;
28687
+ }
28688
+ }
28689
+ this.graceUntil.delete(id);
28690
+ if (await this.deps.isOpenElsewhere(tab.agent, tab.sessionId)) {
28691
+ if (mine) await store.release(id);
28692
+ if (overdue) await this.fail(id, serverText("scheduledElsewhereTimeout"));
28693
+ else this.set(id, "waiting", serverText("scheduledWaitElsewhere"));
28694
+ return;
28695
+ }
28696
+ const claimed = await store.claim(id);
28697
+ if (claimed === "orphan-writing") {
28698
+ await this.fail(id, serverText("scheduledUncertain"));
28699
+ await store.discardOrphan(id);
28700
+ return;
28701
+ }
28702
+ if (claimed !== "mine") {
28703
+ this.set(id, "waiting", serverText("scheduledWaitOtherInstance"));
28704
+ return;
28705
+ }
28706
+ this.startDelivery(message, tab, true);
28707
+ }
28708
+ /** Si la CLI de la pestana esta libre: `freeNow`, o la actividad del registro. */
28709
+ async freeNow(tab) {
28710
+ if (this.deps.freeNow !== void 0) return this.deps.freeNow(tab);
28711
+ const activity = this.deps.activityOf(tab.terminalId);
28712
+ return activity === "idle" || activity === "busy" || activity === "waiting" ? activity : "unknown";
28713
+ }
28714
+ startDelivery(message, tab, wake) {
28715
+ const { store } = this.deps;
28716
+ const { id } = message;
28717
+ this.inFlight.add(id);
28718
+ this.inFlightTabs.set(id, tab.terminalId);
28719
+ this.runtime.delete(id);
28720
+ this.publish();
28721
+ void (async () => {
28722
+ let failure = null;
28723
+ let delivered = false;
28724
+ let cancelled = false;
28725
+ let stillBusy = false;
28726
+ try {
28727
+ await store.refresh();
28728
+ if (store.get(id) === null) {
28729
+ cancelled = true;
28730
+ } else {
28731
+ await store.setPhase(id, "writing");
28732
+ if (wake && !await this.deps.wake(tab.terminalId)) failure = serverText("scheduledNoTab");
28733
+ if (failure === null) {
28734
+ const outcome = await this.deps.deliver(tab.terminalId, message.text);
28735
+ if (outcome.kind === "busy") {
28736
+ stillBusy = true;
28737
+ } else {
28738
+ failure = scheduledFailureFor(outcome);
28739
+ delivered = failure === null;
28740
+ }
28741
+ }
28742
+ }
28743
+ } catch (error) {
28744
+ this.deps.log?.(`[scheduled] couldn't open the CLI for ${id.slice(0, 8)}: ${error instanceof Error ? error.message : String(error)}`);
28745
+ failure = serverText("scheduledCliOpenFailed");
28746
+ }
28747
+ try {
28748
+ if (delivered) await store.remove(id);
28749
+ else if (!cancelled && failure !== null) await store.update(id, { failure });
28750
+ } catch (error) {
28751
+ this.deps.log?.(`[scheduled] couldn't save what happened to ${id.slice(0, 8)}: ${error instanceof Error ? error.message : String(error)}`);
28752
+ } finally {
28753
+ await store.release(id).catch(() => void 0);
28754
+ this.inFlight.delete(id);
28755
+ this.inFlightTabs.delete(id);
28756
+ if (!cancelled) this.cooldownUntil.set(tab.terminalId, this.deps.now() + SCHEDULED_COOLDOWN_MS);
28757
+ this.forget(id);
28758
+ if (stillBusy) this.set(id, "waiting", serverText("scheduledWaitBusy"));
28759
+ this.publish();
28760
+ this.nudge();
28761
+ }
28762
+ })();
28763
+ }
28764
+ };
28765
+
28766
+ // packages/server/src/scheduled-messages/delivery.ts
28767
+ var SCHEDULED_POLL_MS = 250;
28768
+ function normalized(text) {
28769
+ return text.replace(/\s+/g, " ").trim();
28770
+ }
28771
+ function isUserMessageWith(event2, text) {
28772
+ if (event2.role !== "user") return false;
28773
+ const said = normalized(event2.parts.map((part) => part.kind === "text" ? part.text : "").join(" "));
28774
+ const sent = normalized(text);
28775
+ if (said.length === 0 || sent.length === 0) return false;
28776
+ const head = sent.slice(0, 200);
28777
+ return said === sent || said.includes(head) || said.length >= 40 && sent.startsWith(said.slice(0, 200));
28778
+ }
28779
+ async function freenessOf(hasStatus, activity, follower, launchedAt) {
28780
+ if (hasStatus) {
28781
+ if (activity === "idle") return "idle";
28782
+ if (activity === "busy") return "busy";
28783
+ if (activity === "waiting") return "waiting";
28784
+ return "unknown";
28785
+ }
28786
+ if (follower === null || launchedAt === null) return "unknown";
28787
+ await follower.poll().catch(() => null);
28788
+ if (follower.hasOpenTurn?.(launchedAt) === true || follower.hasOpenToolCall(launchedAt)) return "busy";
28789
+ return "idle";
28790
+ }
28791
+ async function deliverScheduled(deps, request) {
28792
+ const { terminalId, text, support } = request;
28793
+ const pollMs = request.pollMs ?? SCHEDULED_POLL_MS;
28794
+ const first = deps.tab(terminalId);
28795
+ if (first === null) return { kind: "gone" };
28796
+ if (!first.alive || first.sessionId.length === 0) return { kind: "not-ready" };
28797
+ const target = { cwd: first.cwd, sessionId: first.sessionId };
28798
+ const follower = deps.follow(target);
28799
+ await follower.start();
28800
+ await follower.poll().catch(() => null);
28801
+ const screen = support.kind === "screen" ? deps.screen(terminalId) : null;
28802
+ if (support.kind === "screen" && screen === null) return { kind: "not-ready" };
28803
+ try {
28804
+ const deadline = deps.now() + request.readyTimeoutMs;
28805
+ let previous = null;
28806
+ let last = "unknown";
28807
+ for (; ; ) {
28808
+ const tab = deps.tab(terminalId);
28809
+ if (tab === null) return { kind: "gone" };
28810
+ if (!tab.alive || tab.sessionId !== target.sessionId) return { kind: "not-ready" };
28811
+ last = await freenessOf(request.hasStatus, deps.activityOf(terminalId), follower, tab.launchedAt);
28812
+ if (last === "idle") {
28813
+ if (screen === null) break;
28814
+ const lines = await screen.lines();
28815
+ const key = lines.join("\n");
28816
+ if (support.kind === "screen" && support.readyScreen(lines)) {
28817
+ if (key === previous) break;
28818
+ previous = key;
28819
+ } else {
28820
+ previous = null;
28821
+ }
28822
+ } else {
28823
+ previous = null;
28824
+ }
28825
+ if (deps.now() >= deadline) return last === "busy" || last === "waiting" ? { kind: "busy" } : { kind: "not-ready" };
28826
+ await deps.wait(pollMs);
28827
+ }
28828
+ } finally {
28829
+ screen?.dispose();
28830
+ }
28831
+ const writtenAt = deps.now();
28832
+ if (support.kind === "api") {
28833
+ const outcome = await support.deliver(target, text).catch(() => "failed");
28834
+ if (outcome === "not-sent") return { kind: "not-ready" };
28835
+ } else {
28836
+ const result = await deps.submit(terminalId, text);
28837
+ switch (result.kind) {
28838
+ case "written":
28839
+ break;
28840
+ case "refusal":
28841
+ case "prepare-failed":
28842
+ case "dropped":
28843
+ return { kind: "not-ready" };
28844
+ case "nothing":
28845
+ return { kind: "nothing" };
28846
+ case "refused":
28847
+ return { kind: "refused" };
28848
+ case "blocked":
28849
+ case "interrupted":
28850
+ case "failed":
28851
+ return { kind: "interrupted" };
28852
+ }
28853
+ }
28854
+ const confirmDeadline = deps.now() + support.confirmTimeoutMs;
28855
+ for (; ; ) {
28856
+ const read2 = await follower.poll().catch(() => null);
28857
+ if (read2 !== null && read2.added.some((event2) => (event2.at === 0 || event2.at >= writtenAt - 5e3) && isUserMessageWith(event2, text))) {
28858
+ return { kind: "sent" };
28859
+ }
28860
+ if (request.hasStatus && deps.activityOf(terminalId) === "busy") return { kind: "sent" };
28861
+ if (deps.now() >= confirmDeadline) return { kind: "unconfirmed" };
28862
+ await deps.wait(pollMs);
28863
+ }
28864
+ }
28865
+
27550
28866
  // packages/server/src/terminal-socket.ts
27551
28867
  var NOTE_SEND_TIMEOUT_MS = 15e3;
27552
28868
  var REWIND_EXTRA_STEPS = 30;
@@ -27593,7 +28909,8 @@ function attachTerminalSocket(options) {
27593
28909
  defaultCwd,
27594
28910
  vault,
27595
28911
  remote,
27596
- worktrees
28912
+ worktrees,
28913
+ scheduled
27597
28914
  } = options;
27598
28915
  const pasteStore = options.pasteStore ?? new PasteStore();
27599
28916
  const writeQueue = new TerminalWriteQueue();
@@ -27658,12 +28975,12 @@ function attachTerminalSocket(options) {
27658
28975
  agents: agents.list(),
27659
28976
  defaultAgent: agents.defaultAgent()
27660
28977
  });
27661
- const writeToTerminal = (socket, terminalId, data) => {
28978
+ const writeToTerminal = (socket, terminalId, data, refusedAs) => {
27662
28979
  if (registry.write(terminalId, data)) return true;
27663
28980
  if (registry.get(terminalId) === null) {
27664
- sendError(socket, "unknown-terminal", serverText("terminalGone"));
28981
+ sendError(socket, refusedAs ?? "unknown-terminal", serverText("terminalGone"));
27665
28982
  } else {
27666
- sendError(socket, "terminal-asleep", serverText("terminalAsleep"));
28983
+ sendError(socket, refusedAs ?? "terminal-asleep", serverText("terminalAsleep"));
27667
28984
  }
27668
28985
  return false;
27669
28986
  };
@@ -27676,6 +28993,13 @@ function attachTerminalSocket(options) {
27676
28993
  for (const { from, to } of moved) drafts.move(from, to);
27677
28994
  const entries = draftEntriesOf(appeared, drafts).filter((entry) => !isEmptyComposerDraft(entry.draft));
27678
28995
  if (entries.length > 0) broadcast({ type: "composer.drafts", drafts: entries });
28996
+ for (const { from, to } of moved) {
28997
+ void scheduler.moveOwner(from, to).catch((error) => {
28998
+ console.warn("[scheduled] couldn't move the messages of a tab:", error);
28999
+ });
29000
+ }
29001
+ scheduler.publish();
29002
+ scheduler.nudge();
27679
29003
  };
27680
29004
  const onRegistryExit = (terminalId, exitCode, signal) => broadcast({ type: "terminal.exit", terminalId, exitCode, signal });
27681
29005
  const onRegistryActivity = (terminalId, activity, offlineReason) => {
@@ -27684,6 +29008,7 @@ function attachTerminalSocket(options) {
27684
29008
  offlineReason === null ? { type: "terminal.activity", terminalId, activity } : { type: "terminal.activity", terminalId, activity, offlineReason }
27685
29009
  );
27686
29010
  if (registry.get(terminalId)?.review === true) worktrees.reviewActivity();
29011
+ scheduler.nudge();
27687
29012
  };
27688
29013
  const onIndexStatus = (status) => broadcast({ type: "index.status", status });
27689
29014
  const onIndexProjects = (projects, replace) => broadcast({ type: "index.projects", projects, replace });
@@ -27719,7 +29044,7 @@ function attachTerminalSocket(options) {
27719
29044
  const onConversationState = (terminalId, state) => broadcast({ type: "conversation.state", terminalId, state });
27720
29045
  const onConversationMode = (terminalId, mode) => broadcast({ type: "conversation.mode", terminalId, mode });
27721
29046
  const onConversationWaiting = (terminalId, waitingFor) => broadcast({ type: "conversation.waiting", terminalId, waitingFor });
27722
- const onConversationToolCall = (terminalId, open9) => broadcast({ type: "conversation.toolCall", terminalId, open: open9 });
29047
+ const onConversationToolCall = (terminalId, open10) => broadcast({ type: "conversation.toolCall", terminalId, open: open10 });
27723
29048
  const onConversationSubagents = (terminalId, subagents) => broadcast({ type: "conversation.subagents", terminalId, subagents });
27724
29049
  const onConversationLiveQuestion = (terminalId, question) => broadcast({ type: "conversation.liveQuestion", terminalId, question });
27725
29050
  const onConversationQueue = (terminalId, queue) => broadcast({ type: "conversation.queue", terminalId, queue });
@@ -27733,6 +29058,7 @@ function attachTerminalSocket(options) {
27733
29058
  return await adapter.defaults(descriptor.cwd, sessionModel) ?? NO_DEFAULTS;
27734
29059
  };
27735
29060
  const pieceWriter = (socket, terminalId) => (piece) => writeToTerminal(socket, terminalId, piece);
29061
+ const answerWriter = (socket, terminalId) => (piece) => writeToTerminal(socket, terminalId, piece, "answer-failed");
27736
29062
  const interruptAgent = (socket, terminalId, options2) => {
27737
29063
  writeQueue.interrupt(terminalId);
27738
29064
  const [first, ...rest] = buildInterruptKeys(inputOf(terminalId).interruptPresses);
@@ -27807,6 +29133,12 @@ function attachTerminalSocket(options) {
27807
29133
  const draftOwner = draftOwnerOf(registry.get(terminalId));
27808
29134
  if (!registry.close(terminalId)) return false;
27809
29135
  if (draftOwner !== null) drafts.delete(draftOwner);
29136
+ turnProbes.delete(terminalId);
29137
+ if (draftOwner !== null) {
29138
+ void scheduler.dropOwner(draftOwner).catch((error) => {
29139
+ console.warn("[scheduled] couldn't cancel the messages of a closed tab:", error);
29140
+ });
29141
+ }
27810
29142
  conversations.drop(terminalId);
27811
29143
  repos.drop(terminalId);
27812
29144
  memory.drop(terminalId);
@@ -27828,6 +29160,111 @@ function attachTerminalSocket(options) {
27828
29160
  noteSubmitted: (terminalId, text) => registry.noteSubmitted(terminalId, text),
27829
29161
  enqueue: (terminalId, job, enqueueOptions) => writeQueue.enqueue(terminalId, job, enqueueOptions)
27830
29162
  };
29163
+ const submitDeps = {
29164
+ adapterOf,
29165
+ inputOf,
29166
+ launchedAtOf: (terminalId) => registry.launchedAtOf(terminalId),
29167
+ activityOf: (terminalId) => registry.activityOf(terminalId),
29168
+ sessionIdOf: (terminalId) => registry.get(terminalId)?.sessionId ?? null,
29169
+ watchScreen: (terminalId) => registry.watchScreen(terminalId),
29170
+ noteSubmitted: (terminalId, text) => registry.noteSubmitted(terminalId, text),
29171
+ conversations,
29172
+ queuedSubmits,
29173
+ enqueue: (terminalId, job, enqueueOptions) => writeQueue.enqueue(terminalId, job, enqueueOptions),
29174
+ now: Date.now
29175
+ };
29176
+ const turnProbes = /* @__PURE__ */ new Map();
29177
+ const probeFor = (terminalId, follow, sessionId) => {
29178
+ const known = turnProbes.get(terminalId);
29179
+ if (known !== void 0 && known.sessionId === sessionId) return known;
29180
+ const follower = follow();
29181
+ const probe = { sessionId, follower, started: follower.start() };
29182
+ turnProbes.set(terminalId, probe);
29183
+ return probe;
29184
+ };
29185
+ const scheduledDeliveryDeps = (follow) => ({
29186
+ tab: (terminalId) => {
29187
+ const tab = registry.get(terminalId);
29188
+ if (tab === null) return null;
29189
+ return { alive: tab.alive, sessionId: tab.sessionId, cwd: tab.cwd, launchedAt: registry.launchedAtOf(terminalId) };
29190
+ },
29191
+ activityOf: (terminalId) => registry.activityOf(terminalId),
29192
+ screen: (terminalId) => registry.watchScreen(terminalId),
29193
+ // El camino del cuadro, con sus guardas, solo texto.
29194
+ submit: async (terminalId, text) => {
29195
+ const plan = prepareSubmit(submitDeps, terminalId);
29196
+ if ("refusal" in plan) return { kind: "refusal" };
29197
+ return runSubmit(submitDeps, plan, {
29198
+ text,
29199
+ imageCount: 0,
29200
+ send: true,
29201
+ prepare: () => Promise.resolve({ fullText: text, imagePaths: [] }),
29202
+ write: (piece) => registry.write(terminalId, piece)
29203
+ });
29204
+ },
29205
+ follow,
29206
+ now: Date.now,
29207
+ wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
29208
+ });
29209
+ const scheduler = new MessageScheduler({
29210
+ store: scheduled,
29211
+ now: Date.now,
29212
+ enabled: () => appSettings.scheduledMessagesEnabled(),
29213
+ refreshEnabled: () => appSettings.refreshScheduledMessages(),
29214
+ tabs: () => registry.list().flatMap(
29215
+ (tab) => tab.kind === "agent" && tab.agent !== null && tab.sessionId.length > 0 ? [{ terminalId: tab.terminalId, agent: tab.agent, sessionId: tab.sessionId, alive: tab.alive }] : []
29216
+ ),
29217
+ activityOf: (terminalId) => registry.activityOf(terminalId),
29218
+ freeNow: async (tab) => {
29219
+ const adapter = agents.get(tab.agent)?.adapter ?? null;
29220
+ if (adapter === null) return "unknown";
29221
+ if (adapter.status !== null) {
29222
+ const activity = registry.activityOf(tab.terminalId);
29223
+ return activity === "idle" || activity === "busy" || activity === "waiting" ? activity : "unknown";
29224
+ }
29225
+ const tabNow = registry.get(tab.terminalId);
29226
+ if (tabNow === null) return "unknown";
29227
+ const probe = probeFor(tab.terminalId, () => adapter.history.follow({ cwd: tabNow.cwd, sessionId: tab.sessionId }), tab.sessionId);
29228
+ await probe.started;
29229
+ return freenessOf(false, null, probe.follower, registry.launchedAtOf(tab.terminalId));
29230
+ },
29231
+ othersRunning: () => options.instances?.othersRunning() ?? Promise.resolve(false),
29232
+ tellsOpenElsewhere: (agent) => agents.get(agent)?.adapter.status?.isOpen !== void 0,
29233
+ agentOf: (agent) => {
29234
+ const registered = agents.get(agent);
29235
+ if (registered === null) return null;
29236
+ return {
29237
+ label: registered.adapter.label,
29238
+ scheduledSend: registered.adapter.capabilities.scheduledSend && registered.location !== null
29239
+ };
29240
+ },
29241
+ isOpenElsewhere: async (agent, sessionId) => await agents.get(agent)?.adapter.status?.isOpen?.(sessionId) === true,
29242
+ wake: async (terminalId) => await registry.wake(terminalId) !== null,
29243
+ deliver: (terminalId, text) => {
29244
+ const adapter = adapterOf(terminalId);
29245
+ if (adapter?.scheduled !== void 0) {
29246
+ const follow = (target) => adapter.history.follow(target);
29247
+ return deliverScheduled(scheduledDeliveryDeps(follow), {
29248
+ terminalId,
29249
+ text,
29250
+ support: adapter.scheduled,
29251
+ hasStatus: adapter.status !== null,
29252
+ readyTimeoutMs: SCHEDULED_DELIVERY_TIMEOUT_MS
29253
+ });
29254
+ }
29255
+ return deliverWhenReady(deliverDeps, {
29256
+ terminalId,
29257
+ text,
29258
+ images: [],
29259
+ timeoutMs: SCHEDULED_DELIVERY_TIMEOUT_MS,
29260
+ write: (piece) => registry.write(terminalId, piece)
29261
+ });
29262
+ },
29263
+ onChange: (messages) => broadcast({ type: "schedule.list", messages }),
29264
+ log: (line) => console.warn(line)
29265
+ });
29266
+ const startScheduler = () => scheduler.start();
29267
+ void options.restored.then(startScheduler, startScheduler);
27831
29268
  const continueDeps = {
27832
29269
  registry,
27833
29270
  closeTab: (terminalId) => {
@@ -27896,7 +29333,11 @@ function attachTerminalSocket(options) {
27896
29333
  const stopRemoteStatus = remote.onChange(
27897
29334
  () => broadcastToHost({ type: "remote.status", status: remote.status() })
27898
29335
  );
27899
- const onAppSettingsStatus = (settings2) => broadcast({ type: "settings.status", settings: settings2 });
29336
+ const onAppSettingsStatus = (settings2) => {
29337
+ broadcast({ type: "settings.status", settings: settings2 });
29338
+ scheduler.publish();
29339
+ scheduler.nudge();
29340
+ };
27900
29341
  appSettings.on("status", onAppSettingsStatus);
27901
29342
  const stopRemoteRevoked = remote.onRevoked((deviceId) => {
27902
29343
  for (const [socket, id] of remoteSockets) {
@@ -28024,6 +29465,7 @@ function attachTerminalSocket(options) {
28024
29465
  send(socket, { type: "index.projects", projects: index.getProjects(), replace: true });
28025
29466
  send(socket, { type: "notes.list", notes: notes.list() });
28026
29467
  send(socket, { type: "composer.drafts", drafts: draftEntriesOf(registry.list(), drafts) });
29468
+ send(socket, { type: "schedule.list", messages: scheduler.snapshot() });
28027
29469
  send(socket, { type: "vault.status", status: vault.status() });
28028
29470
  if (deviceId === null) send(socket, { type: "remote.status", status: remote.status() });
28029
29471
  send(socket, { type: "settings.status", settings: appSettings.status() });
@@ -28105,58 +29547,19 @@ function attachTerminalSocket(options) {
28105
29547
  break;
28106
29548
  }
28107
29549
  const submitting = adapterOf(terminalId);
28108
- const isBlind = () => submitting !== null && conversations.isBlindToApprovals(terminalId, submitting);
28109
- const guardsPending = submitting?.capabilities.permissionCycle?.approvesPendingOnCycle === true;
28110
- const blocksWhileWaiting = submitting?.capabilities.waitingBlocksSubmit === true;
28111
- if (submitting !== null) {
28112
- const launchedAt = registry.launchedAtOf(terminalId);
28113
- const blind = isBlind();
28114
- const refusal2 = submitRefusal({
28115
- label: submitting.label,
28116
- approvesPendingOnCycle: guardsPending,
28117
- waitingFor: conversations.getWaitingFor(terminalId),
28118
- waitingBlocksSubmit: blocksWhileWaiting,
28119
- activity: registry.activityOf(terminalId),
28120
- blind,
28121
- openToolCall: blind && launchedAt !== null && conversations.hasOpenToolCall(terminalId, launchedAt)
28122
- });
28123
- if (refusal2 !== null) {
28124
- sendError(socket, "submit-failed", refusal2);
28125
- settle(false);
28126
- break;
28127
- }
29550
+ const plan = prepareSubmit(submitDeps, terminalId);
29551
+ if ("refusal" in plan) {
29552
+ sendError(socket, "submit-failed", plan.refusal);
29553
+ settle(false);
29554
+ break;
28128
29555
  }
28129
- const approvalGuard = submitting !== null && (guardsPending || blocksWhileWaiting || isBlind()) ? async () => {
28130
- const current = registry.launchedAtOf(terminalId);
28131
- if (current === null) return true;
28132
- if (blocksWhileWaiting && registry.activityOf(terminalId) === "waiting") return false;
28133
- if (isBlind() && await conversations.checkOpenToolCall(terminalId, current)) return false;
28134
- return !guardsPending || await conversations.checkWaitingFor(terminalId) === null;
28135
- } : void 0;
28136
- const input = inputOf(terminalId);
28137
- const startupDialog = input.startupDialog;
28138
- const firstMessage = startupDialog !== void 0 && (registry.get(terminalId)?.sessionId ?? "") === "";
28139
- let startupChecked = false;
28140
- let stoppedByDialog = false;
28141
- const pieceGuard = firstMessage || approvalGuard !== void 0 ? async () => {
28142
- if (firstMessage && startupDialog !== void 0 && !startupChecked) {
28143
- startupChecked = true;
28144
- const screen = registry.watchScreen(terminalId);
28145
- if (screen !== null) {
28146
- try {
28147
- stoppedByDialog = screenShowsDialog(await screen.lines(), startupDialog);
28148
- } finally {
28149
- screen.dispose();
28150
- }
28151
- if (stoppedByDialog) return false;
28152
- }
28153
- }
28154
- return approvalGuard === void 0 ? true : approvalGuard();
28155
- } : void 0;
28156
- const interrupted = () => sendError(socket, "submit-failed", serverText("submitInterrupted"));
28157
- void writeQueue.enqueue(
28158
- terminalId,
28159
- async (lane) => {
29556
+ void runSubmit(submitDeps, plan, {
29557
+ text: text2,
29558
+ imageCount: images.length,
29559
+ send: message.send !== false,
29560
+ write: pieceWriter(socket, terminalId),
29561
+ onWritten: (outcome) => settle(outcome === "written"),
29562
+ prepare: async (input) => {
28160
29563
  const imagePaths = [];
28161
29564
  try {
28162
29565
  for (const image of images) {
@@ -28171,7 +29574,7 @@ function attachTerminalSocket(options) {
28171
29574
  error instanceof PasteImageError ? error.text : serverText("pasteImageFailed"),
28172
29575
  detail
28173
29576
  );
28174
- return;
29577
+ return null;
28175
29578
  }
28176
29579
  const attachmentLines = [];
28177
29580
  try {
@@ -28189,55 +29592,25 @@ function attachTerminalSocket(options) {
28189
29592
  error instanceof PasteFileError ? error.text : serverText("attachmentSaveFailed"),
28190
29593
  detail
28191
29594
  );
28192
- return;
29595
+ return null;
28193
29596
  }
28194
- const fullText = textWithAttachments(text2, attachmentLines);
28195
- const pieces = buildSubmissionWrites(fullText, imagePaths, input, {
28196
- send: message.send !== false
28197
- });
28198
- if (pieces === null) return;
28199
- registry.noteSubmitted(terminalId, fullText);
28200
- const startAfterMs = submitStartDelayMs(
28201
- registry.launchedAtOf(terminalId),
28202
- Date.now(),
28203
- input.readyAfterLaunchMs ?? 0
28204
- );
28205
- const outcome = await lane.writePieces(
28206
- pieces,
28207
- input.pieceGapMs,
28208
- pieceWriter(socket, terminalId),
28209
- pieceGuard,
28210
- { startAfterMs }
29597
+ return { fullText: textWithAttachments(text2, attachmentLines), imagePaths };
29598
+ }
29599
+ }).then((result) => {
29600
+ if (result.kind === "dropped" || result.kind === "interrupted") {
29601
+ sendError(socket, "submit-failed", serverText("submitInterrupted"));
29602
+ }
29603
+ if (result.kind === "blocked" && result.byDialog) {
29604
+ sendError(socket, "submit-failed", serverText("submitStartupDialog", { label: submitting?.label ?? "" }));
29605
+ if (clearsDraft) send(socket, { type: "composer.prefill", terminalId, text: text2, reason: "not-ready" });
29606
+ } else if (result.kind === "blocked") {
29607
+ sendError(
29608
+ socket,
29609
+ "submit-failed",
29610
+ result.waitingNow ? submitting === null ? serverText("submitStoppedWaitingNoLabel") : serverText("submitStoppedWaiting", { label: submitting.label }) : submitting === null ? serverText("submitStoppedToolNoLabel") : serverText("submitStoppedTool", { label: submitting.label })
28211
29611
  );
28212
- settle(outcome === "written");
28213
- if (outcome === "written" && message.send !== false && submitting?.promptQueue?.source === "app" && registry.activityOf(terminalId) === "busy" && !text2.trimStart().startsWith("/")) {
28214
- conversations.noteQueued(terminalId, { text: text2, images: images.length, at: Date.now() });
28215
- }
28216
- if (outcome === "written" && message.send !== false && submitting?.queuedSubmits !== void 0) {
28217
- queuedSubmits.note(
28218
- terminalId,
28219
- submissionPayload(fullText, imagePaths, input.imageReference) ?? fullText,
28220
- Date.now(),
28221
- fullText
28222
- );
28223
- }
28224
- if (outcome === "interrupted") interrupted();
28225
- if (outcome === "blocked" && stoppedByDialog) {
28226
- sendError(socket, "submit-failed", serverText("submitStartupDialog", { label: submitting?.label ?? "" }));
28227
- if (clearsDraft) send(socket, { type: "composer.prefill", terminalId, text: text2, reason: "not-ready" });
28228
- return;
28229
- }
28230
- if (outcome === "blocked") {
28231
- const waitingNow = blocksWhileWaiting && registry.activityOf(terminalId) === "waiting";
28232
- sendError(
28233
- socket,
28234
- "submit-failed",
28235
- waitingNow ? submitting === null ? serverText("submitStoppedWaitingNoLabel") : serverText("submitStoppedWaiting", { label: submitting.label }) : submitting === null ? serverText("submitStoppedToolNoLabel") : serverText("submitStoppedTool", { label: submitting.label })
28236
- );
28237
- }
28238
- },
28239
- { onDropped: interrupted }
28240
- ).then(() => settle(false));
29612
+ }
29613
+ }).finally(() => settle(false));
28241
29614
  break;
28242
29615
  }
28243
29616
  /*
@@ -28311,7 +29684,7 @@ function attachTerminalSocket(options) {
28311
29684
  sendError(socket, "answer-failed", check === "gone" ? ANSWER_NOT_PENDING_TEXT : ANSWER_MENU_CHANGED_TEXT);
28312
29685
  return;
28313
29686
  }
28314
- const outcome = await lane.writePieces(keys, ANSWER_KEY_INTERVAL_MS, pieceWriter(socket, message.terminalId));
29687
+ const outcome = await lane.writePieces(keys, ANSWER_KEY_INTERVAL_MS, answerWriter(socket, message.terminalId));
28315
29688
  if (outcome === "interrupted") interruptedAnswer();
28316
29689
  },
28317
29690
  { onDropped: interruptedAnswer }
@@ -28343,7 +29716,7 @@ function attachTerminalSocket(options) {
28343
29716
  const outcome = await lane.writePieces(
28344
29717
  keys,
28345
29718
  ANSWER_KEY_INTERVAL_MS,
28346
- pieceWriter(socket, message.terminalId)
29719
+ answerWriter(socket, message.terminalId)
28347
29720
  );
28348
29721
  if (outcome === "interrupted") interruptedAnswer();
28349
29722
  },
@@ -28409,10 +29782,10 @@ function attachTerminalSocket(options) {
28409
29782
  nunca esconde nada.
28410
29783
  */
28411
29784
  case "session.archive": {
28412
- const open9 = new Set(
29785
+ const open10 = new Set(
28413
29786
  registry.list().map((terminal) => terminal.sessionId).filter((id) => id.length > 0)
28414
29787
  );
28415
- const targets = message.archived ? message.sessionIds.filter((id) => !open9.has(id)) : message.sessionIds;
29788
+ const targets = message.archived ? message.sessionIds.filter((id) => !open10.has(id)) : message.sessionIds;
28416
29789
  if (archived.set(targets, message.archived)) index.refresh();
28417
29790
  if (message.archived && targets.length < message.sessionIds.length) {
28418
29791
  sendError(
@@ -29667,6 +31040,88 @@ ${seen.join("\n-----\n")}`);
29667
31040
  );
29668
31041
  break;
29669
31042
  }
31043
+ /*
31044
+ Programar lo escrito en el cuadro (hito 51, §6.39). La pestana dice de
31045
+ que conversacion es; el servidor revisa todo antes de guardar. Si no lo
31046
+ acepta, el texto vuelve al cuadro, que ya se habia vaciado al mandarlo.
31047
+ Guardado, el borrador se vacia como con Enviar (§6.29).
31048
+ */
31049
+ case "schedule.create": {
31050
+ const { requestId, terminalId, text: text2, sendAt } = message;
31051
+ const giveBack = () => {
31052
+ if (text2.trim().length > 0) send(socket, { type: "composer.prefill", terminalId, text: text2, reason: "unscheduled" });
31053
+ };
31054
+ const descriptor = registry.get(terminalId);
31055
+ if (descriptor === null) {
31056
+ sendError(socket, "unknown-terminal", serverText("terminalGone"), void 0, requestId);
31057
+ break;
31058
+ }
31059
+ const owner = draftOwnerOf(descriptor);
31060
+ const scheduling = adapterOf(terminalId);
31061
+ const refusal2 = scheduleCreateRefusal({
31062
+ enabled: appSettings.scheduledMessagesEnabled(),
31063
+ owner,
31064
+ agentLabel: scheduling?.label ?? null,
31065
+ capable: scheduling?.capabilities.scheduledSend === true,
31066
+ text: text2,
31067
+ sendAt,
31068
+ now: Date.now(),
31069
+ count: scheduled.size
31070
+ });
31071
+ if (refusal2 !== null || owner === null || descriptor.agent === null) {
31072
+ sendError(socket, "schedule-failed", refusal2 ?? serverText("scheduleNoConversation"), void 0, requestId);
31073
+ giveBack();
31074
+ break;
31075
+ }
31076
+ const agent = descriptor.agent;
31077
+ void (async () => {
31078
+ try {
31079
+ const created = await scheduler.create(owner, agent, text2, sendAt);
31080
+ drafts.delete(owner);
31081
+ broadcast({ type: "composer.drafts", drafts: draftEntriesOf(registry.list(), drafts, owner) });
31082
+ send(socket, { type: "schedule.created", requestId, id: created.id });
31083
+ } catch (error) {
31084
+ const detail = error instanceof Error ? error.message : String(error);
31085
+ console.warn("[scheduled] couldn't save a scheduled message:", detail);
31086
+ sendError(socket, "schedule-failed", serverText("scheduleSaveFailed"), detail, requestId);
31087
+ giveBack();
31088
+ }
31089
+ })();
31090
+ break;
31091
+ }
31092
+ /* Sacarlo: su texto vuelve al cuadro de su pestana, en esta ventana. */
31093
+ case "schedule.cancel": {
31094
+ void (async () => {
31095
+ const outcome = await scheduler.cancel(message.id);
31096
+ if (outcome.kind === "gone") {
31097
+ sendError(socket, "schedule-failed", serverText("scheduleGone"));
31098
+ return;
31099
+ }
31100
+ if (outcome.kind === "sending") {
31101
+ sendError(socket, "schedule-failed", serverText("scheduleSending"));
31102
+ return;
31103
+ }
31104
+ const { agent, sessionId, text: text2 } = outcome.message;
31105
+ const tab = registry.list().find((entry) => entry.kind === "agent" && entry.agent === agent && entry.sessionId === sessionId);
31106
+ if (tab !== void 0) send(socket, { type: "composer.prefill", terminalId: tab.terminalId, text: text2, reason: "unscheduled" });
31107
+ })().catch((error) => {
31108
+ const detail = error instanceof Error ? error.message : String(error);
31109
+ sendError(socket, "schedule-failed", serverText("scheduleSaveFailed"), detail);
31110
+ });
31111
+ break;
31112
+ }
31113
+ /* Mandarlo ya, con las reglas de siempre: si el agente trabaja, espera. */
31114
+ case "schedule.sendNow": {
31115
+ void scheduler.sendNow(message.id).then((outcome) => {
31116
+ if (outcome === "off") sendError(socket, "schedule-failed", serverText("scheduleOff"));
31117
+ else if (outcome === "gone") sendError(socket, "schedule-failed", serverText("scheduleGone"));
31118
+ else if (outcome === "sending") sendError(socket, "schedule-failed", serverText("scheduleSending"));
31119
+ }).catch((error) => {
31120
+ const detail = error instanceof Error ? error.message : String(error);
31121
+ sendError(socket, "schedule-failed", serverText("scheduleSaveFailed"), detail);
31122
+ });
31123
+ break;
31124
+ }
29670
31125
  /*
29671
31126
  Ajustes (Hito 41, §6.32). Una consola que ya no esta, o un disco que no
29672
31127
  deja escribir, se dicen; lo demas lo valido el parser. A una ventana
@@ -29799,14 +31254,15 @@ ${seen.join("\n-----\n")}`);
29799
31254
  appSettings.off("status", onAppSettingsStatus);
29800
31255
  stopRemoteStatus();
29801
31256
  stopRemoteRevoked();
31257
+ void scheduler.stop();
29802
31258
  for (const client of wss.clients) client.terminate();
29803
31259
  wss.close();
29804
31260
  };
29805
31261
  }
29806
31262
 
29807
31263
  // packages/server/src/vault/catalog.ts
29808
- import { open as open8, readdir as readdir22, stat as stat46 } from "node:fs/promises";
29809
- import path73 from "node:path";
31264
+ import { open as open9, readdir as readdir24, stat as stat47 } from "node:fs/promises";
31265
+ import path75 from "node:path";
29810
31266
  import { createInterface as createInterface4 } from "node:readline";
29811
31267
  var HEADER_MAX_BYTES = 64 * 1024;
29812
31268
  var LOAD_CONCURRENCY = 16;
@@ -29818,7 +31274,7 @@ async function assetBytesOf(dir, agent, sessionId) {
29818
31274
  const folder = assetsDir(dir, agent, sessionId);
29819
31275
  let names;
29820
31276
  try {
29821
- names = await readdir22(folder);
31277
+ names = await readdir24(folder);
29822
31278
  } catch {
29823
31279
  return 0;
29824
31280
  }
@@ -29826,7 +31282,7 @@ async function assetBytesOf(dir, agent, sessionId) {
29826
31282
  for (const name of names) {
29827
31283
  if (!VAULT_ASSET_NAME_PATTERN.test(name)) continue;
29828
31284
  try {
29829
- const info = await stat46(path73.join(folder, name));
31285
+ const info = await stat47(path75.join(folder, name));
29830
31286
  if (info.isFile()) total += info.size;
29831
31287
  } catch {
29832
31288
  }
@@ -29837,7 +31293,7 @@ async function readEntry2(dir, agent, sessionId) {
29837
31293
  const file = sessionFile(dir, agent, sessionId);
29838
31294
  let sizeBytes;
29839
31295
  try {
29840
- const info = await stat46(file);
31296
+ const info = await stat47(file);
29841
31297
  if (!info.isFile()) return { kind: "absent" };
29842
31298
  sizeBytes = info.size;
29843
31299
  } catch {
@@ -29894,7 +31350,7 @@ var VaultCatalog = class {
29894
31350
  const keys = [];
29895
31351
  let agentFolders = [];
29896
31352
  try {
29897
- agentFolders = await readdir22(sessionsRoot2(dir));
31353
+ agentFolders = await readdir24(sessionsRoot2(dir));
29898
31354
  } catch {
29899
31355
  agentFolders = [];
29900
31356
  }
@@ -29902,7 +31358,7 @@ var VaultCatalog = class {
29902
31358
  if (!isSessionAgentId2(folder)) continue;
29903
31359
  let names;
29904
31360
  try {
29905
- names = await readdir22(agentSessionsDir(dir, folder));
31361
+ names = await readdir24(agentSessionsDir(dir, folder));
29906
31362
  } catch {
29907
31363
  continue;
29908
31364
  }
@@ -29991,7 +31447,7 @@ var VaultCatalog = class {
29991
31447
  async *bodyLines(agent, sessionId) {
29992
31448
  const dir = this.dir;
29993
31449
  if (dir === null || !this.entries.has(keyOf3(agent, sessionId))) return;
29994
- const handle = await open8(sessionFile(dir, agent, sessionId), "r").catch(() => null);
31450
+ const handle = await open9(sessionFile(dir, agent, sessionId), "r").catch(() => null);
29995
31451
  if (handle === null) return;
29996
31452
  const input = handle.createReadStream({ encoding: "utf8" });
29997
31453
  const lines = createInterface4({ input, crlfDelay: Number.POSITIVE_INFINITY });
@@ -30046,22 +31502,22 @@ var VaultCatalog = class {
30046
31502
  };
30047
31503
 
30048
31504
  // packages/server/src/worktrees/links.ts
30049
- import { stat as stat47 } from "node:fs/promises";
30050
- import path74 from "node:path";
31505
+ import { stat as stat48 } from "node:fs/promises";
31506
+ import path76 from "node:path";
30051
31507
  async function exists6(candidate) {
30052
31508
  try {
30053
- await stat47(candidate);
31509
+ await stat48(candidate);
30054
31510
  return true;
30055
31511
  } catch {
30056
31512
  return false;
30057
31513
  }
30058
31514
  }
30059
31515
  function recordAbove(registry, cwd) {
30060
- let current = path74.resolve(cwd);
31516
+ let current = path76.resolve(cwd);
30061
31517
  for (let depth = 0; depth < 24; depth += 1) {
30062
31518
  const record = registry.byPath(current);
30063
- if (record !== null) return { record, inner: path74.relative(current, path74.resolve(cwd)) };
30064
- const parent = path74.dirname(current);
31519
+ if (record !== null) return { record, inner: path76.relative(current, path76.resolve(cwd)) };
31520
+ const parent = path76.dirname(current);
30065
31521
  if (parent === current) return null;
30066
31522
  current = parent;
30067
31523
  }
@@ -30082,7 +31538,7 @@ var WorktreeLinks = class {
30082
31538
  return {
30083
31539
  mainCwd: mainCwdFor(cwd, checkout),
30084
31540
  worktree: {
30085
- branch: checkout.branch ?? record?.branch ?? path74.basename(checkout.root),
31541
+ branch: checkout.branch ?? record?.branch ?? path76.basename(checkout.root),
30086
31542
  exists: true,
30087
31543
  reopenable: false
30088
31544
  }
@@ -30092,13 +31548,13 @@ var WorktreeLinks = class {
30092
31548
  if (found !== null) {
30093
31549
  const { record, inner } = found;
30094
31550
  return {
30095
- mainCwd: inner.length === 0 ? record.mainRoot : path74.join(record.mainRoot, inner),
31551
+ mainCwd: inner.length === 0 ? record.mainRoot : path76.join(record.mainRoot, inner),
30096
31552
  worktree: { branch: record.branch, exists: false, reopenable: await this.reopenable(record) }
30097
31553
  };
30098
31554
  }
30099
- const mainRoot = mainRootFromShape(path74.resolve(cwd));
31555
+ const mainRoot = mainRootFromShape(path76.resolve(cwd));
30100
31556
  if (mainRoot === null) return null;
30101
- return { mainCwd: mainRoot, worktree: { branch: path74.basename(cwd), exists: false, reopenable: false } };
31557
+ return { mainCwd: mainRoot, worktree: { branch: path76.basename(cwd), exists: false, reopenable: false } };
30102
31558
  }
30103
31559
  /**
30104
31560
  * Se puede volver a abrir una rama quitada si la creó la app, su carpeta no
@@ -30128,15 +31584,15 @@ var WorktreeLinks = class {
30128
31584
  };
30129
31585
 
30130
31586
  // packages/server/src/index.ts
30131
- var serverDir = path75.dirname(fileURLToPath2(import.meta.url));
31587
+ var serverDir = path77.dirname(fileURLToPath2(import.meta.url));
30132
31588
  var isPackaged = true;
30133
- var webRoot = isPackaged ? serverDir : path75.resolve(serverDir, "../../web");
30134
- var webDist = isPackaged ? path75.join(serverDir, "web") : path75.join(webRoot, "dist");
30135
- var repoRoot = path75.resolve(serverDir, "../../..");
31589
+ var webRoot = isPackaged ? serverDir : path77.resolve(serverDir, "../../web");
31590
+ var webDist = isPackaged ? path77.join(serverDir, "web") : path77.join(webRoot, "dist");
31591
+ var repoRoot = path77.resolve(serverDir, "../../..");
30136
31592
  var isProduction = isPackaged || process.argv.includes("--prod");
30137
31593
  function resolveDefaultCwd() {
30138
31594
  const override = process.env["AGENT_WORKBENCH_CWD"];
30139
- if (override !== void 0 && override.length > 0) return path75.resolve(override);
31595
+ if (override !== void 0 && override.length > 0) return path77.resolve(override);
30140
31596
  if (isPackaged) return process.cwd();
30141
31597
  return repoRoot;
30142
31598
  }
@@ -30211,7 +31667,7 @@ async function mountDevUi(app, httpServer) {
30211
31667
  }
30212
31668
  var LOOKS_LIKE_FILE = /\.[a-zA-Z0-9]{1,8}$/;
30213
31669
  function mountBuiltUi(app) {
30214
- const indexHtml = path75.join(webDist, "index.html");
31670
+ const indexHtml = path77.join(webDist, "index.html");
30215
31671
  if (!existsSync4(indexHtml)) {
30216
31672
  throw new Error(
30217
31673
  `There's no compiled interface in ${webDist}.
@@ -30299,11 +31755,11 @@ async function main() {
30299
31755
  findSession: (agent, sessionId) => index.find(agent, sessionId),
30300
31756
  // Hito 49: las conversaciones de una rama (para dejarle un pedido a su autor) y la respuesta del revisor.
30301
31757
  sessionsUnder: (folder) => {
30302
- const root = path75.resolve(folder);
31758
+ const root = path77.resolve(folder);
30303
31759
  return index.getProjects().flatMap((project) => project.sessions).filter((session) => {
30304
31760
  if (session.archived || session.cwd.length === 0) return false;
30305
- const relative = path75.relative(root, path75.resolve(session.cwd));
30306
- return relative.length === 0 || !relative.startsWith("..") && !path75.isAbsolute(relative);
31761
+ const relative = path77.relative(root, path77.resolve(session.cwd));
31762
+ return relative.length === 0 || !relative.startsWith("..") && !path77.isAbsolute(relative);
30307
31763
  });
30308
31764
  },
30309
31765
  history: (agent) => agents.get(agent)?.adapter.history ?? null
@@ -30327,6 +31783,14 @@ async function main() {
30327
31783
  await notes.load();
30328
31784
  const drafts = new DraftStore();
30329
31785
  await drafts.load();
31786
+ const scheduled = new ScheduledMessageStore();
31787
+ await scheduled.load();
31788
+ const instances = new RunningInstances();
31789
+ await instances.register();
31790
+ let markRestored = () => void 0;
31791
+ const restored = new Promise((resolve) => {
31792
+ markRestored = resolve;
31793
+ });
30330
31794
  const conversations = new ConversationHub(registry, agents);
30331
31795
  const repos = new RepoHub(registry);
30332
31796
  const memory = new MemoryHub(registry);
@@ -30378,21 +31842,30 @@ async function main() {
30378
31842
  defaultCwd,
30379
31843
  vault,
30380
31844
  remote,
30381
- worktrees
31845
+ worktrees,
31846
+ scheduled,
31847
+ restored,
31848
+ instances
30382
31849
  });
30383
31850
  void pasteStore.purgeStale();
30384
31851
  void index.start();
30385
31852
  const stopWatching = watchSessions(index, conversations, agents);
30386
31853
  conversations.on("historyGrew", (agent, filePath) => void index.refreshPath(agent, filePath));
30387
31854
  if (process.env["AGENT_WORKBENCH_NO_RESTORE"] !== "1") {
30388
- void store.load().then((state) => {
30389
- drafts.retainOnly(workspaceDraftOwners(state));
31855
+ void store.load().then(async (state) => {
31856
+ const owners = workspaceDraftOwners(state);
31857
+ drafts.retainOnly(owners);
31858
+ await scheduled.retainOnly(owners).catch((error) => {
31859
+ console.warn("[scheduled] couldn't drop the messages of closed tabs:", error);
31860
+ });
30390
31861
  if (state.tabs.length === 0 && state.foreignTabs.length === 0) return;
30391
31862
  if (agents.anyAvailable()) {
30392
31863
  console.log(`Restoring ${state.tabs.length} tab(s) from the previous run...`);
30393
31864
  }
30394
31865
  return registry.restore(state);
30395
- });
31866
+ }).finally(markRestored);
31867
+ } else {
31868
+ markRestored();
30396
31869
  }
30397
31870
  const url = `http://${LOOPBACK_HOST}:${port}/?${TOKEN_QUERY_PARAM}=${token}`;
30398
31871
  const remoteStatus = remote.status();
@@ -30423,6 +31896,9 @@ async function main() {
30423
31896
  archived.flush(),
30424
31897
  notes.flush(),
30425
31898
  drafts.flush(),
31899
+ // Lo que esta instancia tenia reservado sin haber escrito nada lo puede tomar otra (hito 51).
31900
+ scheduled.flush().then(() => scheduled.releaseReserved()),
31901
+ instances.unregister(),
30426
31902
  remote.flush(),
30427
31903
  worktreeStore.flush(),
30428
31904
  agentsDisposed