agent-workbench 0.8.0 → 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 path73 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) {
@@ -144,20 +165,25 @@ var WORKTREE_COPY_MAX_FILES = 2e3;
144
165
  var WORKTREE_COPY_MAX_BYTES = 50 * 1024 * 1024;
145
166
  var WORKTREE_COPY_SAMPLE = 8;
146
167
  var WORKTREE_CREATE_MODES = ["new", "existing"];
168
+ var WORKTREE_HOLDERS_MAX = 8;
147
169
  var WORKTREE_PLAN_PATHS_MAX = 20;
170
+ var WORKTREE_UNTRACKED_MAX_FILES = 2e4;
148
171
  var WORKTREE_ROW_FILES_MAX = 8;
149
172
  var WORKTREE_CHANGES_COMMITS_MAX = 50;
150
173
  var WORKTREE_CHANGES_FILES_MAX = 2e3;
151
174
  function asCommitId(value) {
152
175
  return typeof value === "string" && /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(value) ? value : null;
153
176
  }
177
+ function asUntrackedStamp(value) {
178
+ return typeof value === "string" && /^[0-9a-f]{64}$/.test(value) ? value : null;
179
+ }
154
180
  function parseSessionWorktree(value) {
155
181
  const record = asRecord(value);
156
182
  if (record === null) return null;
157
183
  const branch = asNonEmptyString(record["branch"]);
158
- const exists6 = asBoolean(record["exists"]);
159
- if (branch === null || exists6 === null) return null;
160
- return { branch, exists: exists6, reopenable: record["reopenable"] === true };
184
+ const exists7 = asBoolean(record["exists"]);
185
+ if (branch === null || exists7 === null) return null;
186
+ return { branch, exists: exists7, reopenable: record["reopenable"] === true };
161
187
  }
162
188
  function parseWorktreeSource(value) {
163
189
  const record = asRecord(value);
@@ -172,6 +198,16 @@ function parseWorktreeSource(value) {
172
198
  }
173
199
  return null;
174
200
  }
201
+ var HOLDER_TEXT_MAX = 255;
202
+ function parseWorktreeHolder(value) {
203
+ const record = asRecord(value);
204
+ if (record === null) return null;
205
+ const pid = asPositiveInt(record["pid"]);
206
+ const name = asNonEmptyString(record["name"]);
207
+ if (pid === null || name === null || name.length > HOLDER_TEXT_MAX) return null;
208
+ const description = asString(record["description"])?.trim() ?? "";
209
+ return { pid, name, description: description.length > 0 && description.length <= HOLDER_TEXT_MAX ? description : null };
210
+ }
175
211
 
176
212
  // packages/shared/src/models.ts
177
213
  var SESSION_TITLE_SOURCES = [
@@ -226,6 +262,9 @@ function sameQuestionItem(a, b) {
226
262
  return left.length > 0 && right.length > 0 && (left.startsWith(right) || right.startsWith(left));
227
263
  });
228
264
  }
265
+ function askedBeforeLaunch(askedAt, launchedAt) {
266
+ return askedAt > 0 && launchedAt !== null && askedAt < launchedAt;
267
+ }
229
268
  var CONVERSATION_IMAGE_SOURCES = [
230
269
  "content",
231
270
  "attachment",
@@ -483,6 +522,17 @@ function parseComposerDraft(value) {
483
522
  return { text, pasted: pasted2 };
484
523
  }
485
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
+
486
536
  // packages/shared/src/memory.ts
487
537
  var MEMORY_DIR = ".agents/memory";
488
538
  var MEMORY_INDEX_FILE = "MEMORY.md";
@@ -842,13 +892,16 @@ function asAgentIdList(value) {
842
892
  function parseAppSettingsChange(record) {
843
893
  const rawShell = record["consoleShell"];
844
894
  const rawAgents = record["disabledAgents"];
895
+ const rawScheduled = record["scheduledMessages"];
845
896
  const consoleShell = rawShell === void 0 ? void 0 : asConsoleShellId(rawShell);
846
897
  const disabledAgents = rawAgents === void 0 ? void 0 : asAgentIdList(rawAgents);
847
- if (consoleShell === null || disabledAgents === null) return null;
848
- 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;
849
901
  return {
850
902
  ...consoleShell !== void 0 ? { consoleShell } : {},
851
- ...disabledAgents !== void 0 ? { disabledAgents } : {}
903
+ ...disabledAgents !== void 0 ? { disabledAgents } : {},
904
+ ...scheduledMessages !== void 0 ? { scheduledMessages } : {}
852
905
  };
853
906
  }
854
907
 
@@ -1285,6 +1338,21 @@ function parseClientMessage(raw) {
1285
1338
  const draft = parseComposerDraft(record["draft"]);
1286
1339
  return terminalId === null || draft === null ? null : { type: "composer.draft", terminalId, draft };
1287
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
+ }
1288
1356
  case "settings.update": {
1289
1357
  const change = parseAppSettingsChange(record);
1290
1358
  return change === null ? null : { type: "settings.update", ...change };
@@ -1326,7 +1394,10 @@ function parseClientMessage(raw) {
1326
1394
  const requestId = asNonEmptyString(record["requestId"]);
1327
1395
  const worktreeId = asNonEmptyString(record["worktreeId"]);
1328
1396
  const discardConflicts = asBoolean(record["discardConflicts"]);
1329
- return requestId === null || worktreeId === null || discardConflicts === null ? null : { type: "worktree.remove", requestId, worktreeId, discardConflicts };
1397
+ if (requestId === null || worktreeId === null || discardConflicts === null) return null;
1398
+ const message = { type: "worktree.remove", requestId, worktreeId, discardConflicts };
1399
+ const discardUntracked = asUntrackedStamp(record["discardUntracked"]);
1400
+ return discardUntracked === null ? message : { ...message, discardUntracked };
1330
1401
  }
1331
1402
  case "worktree.reopen": {
1332
1403
  const requestId = asNonEmptyString(record["requestId"]);
@@ -1364,8 +1435,8 @@ function parseClientMessage(raw) {
1364
1435
  case "worktree.changesDiff": {
1365
1436
  const requestId = asNonEmptyString(record["requestId"]);
1366
1437
  const worktreeId = asNonEmptyString(record["worktreeId"]);
1367
- const path74 = asNonEmptyString(record["path"]);
1368
- return requestId === null || worktreeId === null || path74 === null ? null : { type: "worktree.changesDiff", requestId, worktreeId, path: path74 };
1438
+ const path78 = asNonEmptyString(record["path"]);
1439
+ return requestId === null || worktreeId === null || path78 === null ? null : { type: "worktree.changesDiff", requestId, worktreeId, path: path78 };
1369
1440
  }
1370
1441
  case "worktree.requestReview": {
1371
1442
  const requestId = asNonEmptyString(record["requestId"]);
@@ -1503,6 +1574,15 @@ function notesImagesDir() {
1503
1574
  function composerDraftsPath() {
1504
1575
  return path.join(appConfigDir(), "composer-drafts.json");
1505
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
+ }
1506
1586
  function catalogTempDir() {
1507
1587
  return path.join(tmpdir(), "agent-workbench", "agy-catalog");
1508
1588
  }
@@ -2952,9 +3032,9 @@ function toTitle(raw) {
2952
3032
  return text;
2953
3033
  }
2954
3034
  function withoutPastedText(raw) {
2955
- const normalized = raw.replace(/\r\n?/g, "\n");
2956
- const typed = normalized.replace(pastedBlockPattern(), "$1");
2957
- 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(), " ");
2958
3038
  }
2959
3039
 
2960
3040
  // packages/server/src/agents/antigravity/session-follower.ts
@@ -3222,12 +3302,12 @@ function blockPattern(name) {
3222
3302
  return new RegExp(`<${name}>([\\s\\S]*?)</${name}>`, "g");
3223
3303
  }
3224
3304
  function unwrapUserInput(content) {
3225
- const open9 = content.indexOf(REQUEST_OPEN);
3226
- 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);
3227
3307
  let text;
3228
3308
  let rest;
3229
- if (open9 !== -1 && close !== -1) {
3230
- 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();
3231
3311
  rest = content.slice(close + REQUEST_CLOSE.length);
3232
3312
  } else {
3233
3313
  text = content.replace(blockPattern("ADDITIONAL_METADATA"), "").replace(blockPattern("USER_SETTINGS_CHANGE"), "").trim();
@@ -4050,6 +4130,15 @@ function createAntigravityQueueEdit() {
4050
4130
  return { run: (io, context) => runAntigravityQueueEdit(io, context) };
4051
4131
  }
4052
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
+
4053
4142
  // packages/server/src/agents/antigravity/settings.ts
4054
4143
  import { readFile as readFile4, stat as stat7 } from "node:fs/promises";
4055
4144
  var SETTINGS_MAX_BYTES = 256 * 1024;
@@ -4634,6 +4723,8 @@ var ANTIGRAVITY_BASE_CAPABILITIES = {
4634
4723
  forceSend: true,
4635
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).
4636
4725
  editQueue: true,
4726
+ // Hito 52: sin status line no se sabe si trabaja, y no se le programa nada.
4727
+ scheduledSend: false,
4637
4728
  /*
4638
4729
  Hito 49: `--mode plan`. Medido con la 1.4.0 al lanzar: arranca en "Plan
4639
4730
  mode: research & plan only" y el ciclo sigue desde ahi. Que hace con
@@ -4645,7 +4736,13 @@ var ANTIGRAVITY_STATUS_LINE_CAPABILITIES = {
4645
4736
  ...ANTIGRAVITY_BASE_CAPABILITIES,
4646
4737
  statusSource: true,
4647
4738
  readySignal: READY_SIGNAL_SAFE,
4648
- 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
4649
4746
  };
4650
4747
  var ANTIGRAVITY_INPUT = {
4651
4748
  imageReference: null,
@@ -4840,6 +4937,8 @@ function createAntigravityAdapter(options = {}) {
4840
4937
  es lo que la app le mando con el agente trabajando. "Enviar ya" es Alt+M.
4841
4938
  */
4842
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 },
4843
4942
  // Sacar la cola para editarla en el cuadro (§6.35.1): vuelve lo que la app le mando.
4844
4943
  queueEdit: createAntigravityQueueEdit(),
4845
4944
  /**
@@ -5069,6 +5168,17 @@ async function readAgentDefaults(cwd, sessionModel = null) {
5069
5168
  import { readdir as readdir7, readFile as readFile8 } from "node:fs/promises";
5070
5169
  import path14 from "node:path";
5071
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
+
5072
5182
  // packages/server/src/agents/claude-code/paths.ts
5073
5183
  import { homedir as homedir4, tmpdir as tmpdir2 } from "node:os";
5074
5184
  import path13 from "node:path";
@@ -5109,7 +5219,16 @@ function readStatus(value) {
5109
5219
  statusUpdatedAt: typeof updatedAt === "number" && Number.isFinite(updatedAt) ? updatedAt : null
5110
5220
  };
5111
5221
  }
5222
+ function fileCounts(record, isAlive) {
5223
+ const pid = record["pid"];
5224
+ return typeof pid !== "number" || isAlive(pid);
5225
+ }
5112
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;
5113
5232
  subscriptions = /* @__PURE__ */ new Set();
5114
5233
  timer = null;
5115
5234
  reading = false;
@@ -5158,6 +5277,34 @@ var CliStatusWatcher = class {
5158
5277
  await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
5159
5278
  }
5160
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
+ }
5161
5308
  dispose() {
5162
5309
  this.subscriptions.clear();
5163
5310
  this.stop();
@@ -5196,8 +5343,10 @@ var CliStatusWatcher = class {
5196
5343
  continue;
5197
5344
  }
5198
5345
  if (typeof parsed !== "object" || parsed === null) continue;
5199
- const sessionId = parsed["sessionId"];
5346
+ const record = parsed;
5347
+ const sessionId = record["sessionId"];
5200
5348
  if (typeof sessionId !== "string" || sessionId.length === 0) continue;
5349
+ if (!fileCounts(record, this.isAlive)) continue;
5201
5350
  const status = readStatus(parsed);
5202
5351
  if (status !== null) found.set(sessionId, status);
5203
5352
  }
@@ -5878,11 +6027,11 @@ function relativeInside(root, absolute) {
5878
6027
  }
5879
6028
  function planFileNameOf(value) {
5880
6029
  if (typeof value !== "string" || value.length === 0) return null;
5881
- const normalized = value.replace(/\\/g, "/");
6030
+ const normalized2 = value.replace(/\\/g, "/");
5882
6031
  const marker = "/.claude/plans/";
5883
- const at = normalized.toLowerCase().lastIndexOf(marker);
6032
+ const at = normalized2.toLowerCase().lastIndexOf(marker);
5884
6033
  if (at === -1) return null;
5885
- const name = normalized.slice(at + marker.length);
6034
+ const name = normalized2.slice(at + marker.length);
5886
6035
  if (name.length === 0 || name.includes("/")) return null;
5887
6036
  if (name === "." || name === "..") return null;
5888
6037
  return name;
@@ -5962,18 +6111,18 @@ var BranchTracker = class {
5962
6111
  }
5963
6112
  /** La linea y todas las de las que cuelga. */
5964
6113
  pathFrom(leaf) {
5965
- const path74 = /* @__PURE__ */ new Set();
6114
+ const path78 = /* @__PURE__ */ new Set();
5966
6115
  let current = leaf;
5967
- while (current !== null && !path74.has(current)) {
6116
+ while (current !== null && !path78.has(current)) {
5968
6117
  const node = this.nodes.get(current);
5969
6118
  if (node === void 0) break;
5970
- path74.add(current);
6119
+ path78.add(current);
5971
6120
  current = node.parent;
5972
6121
  }
5973
- return path74;
6122
+ return path78;
5974
6123
  }
5975
6124
  recompute() {
5976
- const path74 = this.pathFrom(this.leaf());
6125
+ const path78 = this.pathFrom(this.leaf());
5977
6126
  const children = /* @__PURE__ */ new Map();
5978
6127
  for (const [uuid, node] of this.nodes) {
5979
6128
  const key = node.parent !== null && this.nodes.has(node.parent) ? node.parent : null;
@@ -5982,9 +6131,9 @@ var BranchTracker = class {
5982
6131
  else list.push(uuid);
5983
6132
  }
5984
6133
  const roots = [];
5985
- for (const uuid of children.get(null) ?? []) if (!path74.has(uuid)) roots.push(uuid);
5986
- for (const uuid of path74) {
5987
- for (const child of children.get(uuid) ?? []) if (!path74.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);
5988
6137
  }
5989
6138
  const hidden = /* @__PURE__ */ new Set();
5990
6139
  for (const root of roots) {
@@ -8001,6 +8150,12 @@ var CLAUDE_CODE_CAPABILITIES = {
8001
8150
  forceSend: true,
8002
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).
8003
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,
8004
8159
  /*
8005
8160
  Hito 49: `--permission-mode plan`. Medido con la 2.1.304 y el pedido de
8006
8161
  revision: corrio git log, diff y status sin pedir permiso, no ofrecio salir
@@ -8034,6 +8189,8 @@ function createStatusSource(watcher) {
8034
8189
  )
8035
8190
  ),
8036
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),
8037
8194
  dispose: () => watcher.dispose()
8038
8195
  };
8039
8196
  }
@@ -8654,9 +8811,9 @@ var CodexSessionDiscovery = class {
8654
8811
  onInput: () => void 0,
8655
8812
  onSubmitted: (text) => {
8656
8813
  if (released || this.pending.get(terminalId) !== tab) return;
8657
- const normalized = normalizeMessageText(text);
8658
- if (normalized.length === 0) return;
8659
- tab.submitted.push(normalized);
8814
+ const normalized2 = normalizeMessageText(text);
8815
+ if (normalized2.length === 0) return;
8816
+ tab.submitted.push(normalized2);
8660
8817
  if (tab.submitted.length > MAX_SUBMITTED) tab.submitted.splice(0, tab.submitted.length - MAX_SUBMITTED);
8661
8818
  }
8662
8819
  };
@@ -9071,6 +9228,13 @@ var CodexRolloutSink = class {
9071
9228
  turns = [];
9072
9229
  /** Llamadas del turno en curso sin resultado todavia: `call_id` -> epoch ms. */
9073
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;
9074
9238
  warnedForeignMeta = false;
9075
9239
  getUsage() {
9076
9240
  return this.usage;
@@ -9096,7 +9260,13 @@ var CodexRolloutSink = class {
9096
9260
  }
9097
9261
  return false;
9098
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
+ }
9099
9268
  reset() {
9269
+ this.turnOpenSince = null;
9100
9270
  this.model = null;
9101
9271
  this.effort = null;
9102
9272
  this.window = null;
@@ -9178,6 +9348,7 @@ var CodexRolloutSink = class {
9178
9348
  if (TURN_START_TYPES.includes(kind)) {
9179
9349
  this.lastAssistantEventId = null;
9180
9350
  this.openCalls.clear();
9351
+ this.turnOpenSince = at;
9181
9352
  const window = readWindow(payload["model_context_window"]);
9182
9353
  if (window !== null) {
9183
9354
  this.window = window;
@@ -9199,6 +9370,7 @@ var CodexRolloutSink = class {
9199
9370
  }
9200
9371
  if (TURN_END_TYPES.includes(kind)) {
9201
9372
  this.openCalls.clear();
9373
+ this.turnOpenSince = null;
9202
9374
  const duration = payload["duration_ms"];
9203
9375
  const eventId = this.lastAssistantEventId;
9204
9376
  this.lastAssistantEventId = null;
@@ -9432,6 +9604,9 @@ var CodexSessionFollower = class {
9432
9604
  hasOpenToolCall(launchedAt) {
9433
9605
  return this.sink.hasOpenToolCall(launchedAt);
9434
9606
  }
9607
+ hasOpenTurn(launchedAt) {
9608
+ return this.sink.hasOpenTurn(launchedAt);
9609
+ }
9435
9610
  getState() {
9436
9611
  return this.jsonl.getState();
9437
9612
  }
@@ -9579,6 +9754,18 @@ function createCodexHistory(options = {}) {
9579
9754
  };
9580
9755
  }
9581
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
+
9582
9769
  // packages/server/src/agents/codex/index.ts
9583
9770
  var CODEX_COMMAND = "codex";
9584
9771
  var CODEX_INSTALL_URL = "https://learn.chatgpt.com/docs/codex/cli";
@@ -9614,6 +9801,11 @@ var CODEX_CAPABILITIES = {
9614
9801
  // No hace cola: un mensaje con el agente trabajando le llega al modelo en el acto (hito 45).
9615
9802
  forceSend: false,
9616
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,
9617
9809
  /*
9618
9810
  Hito 49: `--sandbox read-only`, al abrir y al reanudar. Medido con la 0.162.1
9619
9811
  y un proveedor falso: con la sandbox de Windows del usuario, escribir fallo
@@ -9700,6 +9892,13 @@ function createCodexAdapter() {
9700
9892
  },
9701
9893
  history: createCodexHistory({ planLimits }),
9702
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 },
9703
9902
  planLimits,
9704
9903
  defaults: () => Promise.resolve(null),
9705
9904
  /**
@@ -10361,7 +10560,22 @@ FROM part WHERE id = $id AND json_extract(data, '$.type') = 'file'`,
10361
10560
  * `question`; `$id` y `$s`.
10362
10561
  */
10363
10562
  partCallById: `SELECT message_id, json_extract(data, '$.callID') AS call_id
10364
- 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`
10365
10579
  });
10366
10580
  var OPENCODE_REQUIRED_COLUMNS = Object.freeze([
10367
10581
  { table: "session", columns: ["id", "parent_id", "directory", "title", "time_created", "time_updated", "revert"] },
@@ -11228,6 +11442,29 @@ var OpenCodeServeClient = class {
11228
11442
  if (!OPENCODE_SESSION_ID_PATTERN.test(sessionId)) throw new Error("Invalid OpenCode session id.");
11229
11443
  await this.request("POST", `/session/${sessionId}/abort`, directory, {});
11230
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
+ }
11231
11468
  /**
11232
11469
  * El flujo de `/global/event`. Reconecta con las esperas de
11233
11470
  * `SERVE_RECONNECT_DELAYS_MS` y llama `onReconnect` **cada vez que la conexion
@@ -12268,6 +12505,22 @@ var OpenCodeServeStatus = class {
12268
12505
  }
12269
12506
  };
12270
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
+
12271
12524
  // packages/server/src/agents/opencode/index.ts
12272
12525
  var OPENCODE_COMMAND = "opencode";
12273
12526
  var OPENCODE_INSTALL_URL = "https://opencode.ai/docs/";
@@ -12318,6 +12571,11 @@ var OPENCODE_CAPABILITIES = {
12318
12571
  forceSend: false,
12319
12572
  // El mensaje en cola ya esta escrito en su base (§11.13): no hay de donde sacarlo.
12320
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,
12321
12579
  /*
12322
12580
  Hito 49: ninguno. Su agente `plan` pide permiso antes de editar, pero la app
12323
12581
  abre el TUI con `opencode attach`, que segun su documentacion no acepta
@@ -12443,6 +12701,27 @@ function createOpenCodeAdapter(options = {}) {
12443
12701
  questions,
12444
12702
  // La cola la dice la base: un mensaje sin respuesta detras de uno que se esta contestando (hito 45, §11.13).
12445
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
+ },
12446
12725
  /**
12447
12726
  * La base que se lee, para el arranque. Solo si hay algo que decir, para que
12448
12727
  * quien no usa OpenCode no vea una linea nueva:
@@ -13068,8 +13347,8 @@ function textOf2(message) {
13068
13347
  }
13069
13348
  function sameFile(a, b) {
13070
13349
  const normalize3 = (value) => {
13071
- const normalized = path36.normalize(value);
13072
- return process.platform === "win32" ? normalized.toLowerCase() : normalized;
13350
+ const normalized2 = path36.normalize(value);
13351
+ return process.platform === "win32" ? normalized2.toLowerCase() : normalized2;
13073
13352
  };
13074
13353
  return normalize3(a) === normalize3(b);
13075
13354
  }
@@ -14068,6 +14347,23 @@ function createPiQueueEdit(timing = DEFAULT_TIMING3) {
14068
14347
  return { run: (io, context) => runPiQueueEdit(io, context, piDequeueKeys(), timing) };
14069
14348
  }
14070
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
+
14071
14367
  // packages/server/src/agents/pi/tab-watch.ts
14072
14368
  import { readdir as readdir11, stat as stat25 } from "node:fs/promises";
14073
14369
  import path40 from "node:path";
@@ -14190,9 +14486,9 @@ var PiTabWatch = class {
14190
14486
  checkTrust() {
14191
14487
  if (this.closed || this.sessionId.length === 0) return;
14192
14488
  const output = this.context.readOutput();
14193
- const open9 = trustDialogOpen(output);
14194
- this.live.setBlocked(this.sessionId, open9);
14195
- if (open9) return;
14489
+ const open10 = trustDialogOpen(output);
14490
+ this.live.setBlocked(this.sessionId, open10);
14491
+ if (open10) return;
14196
14492
  if (interfaceStarted(output) || this.now() - this.context.launchedAt > STARTUP_DEADLINE_MS) {
14197
14493
  if (this.startupTimer !== null) clearInterval(this.startupTimer);
14198
14494
  this.startupTimer = null;
@@ -14362,6 +14658,11 @@ var PI_BASE_CAPABILITIES = {
14362
14658
  (§6.35.1). Medido en Windows; fuera, la tecla es Alt+Up y no esta medida.
14363
14659
  */
14364
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,
14365
14666
  // Hito 49: sin `edit` ni `write` (`PI_REVIEW_TOOLS`), pero su consola puede escribir: a medias (§16.19).
14366
14667
  readOnlyLaunch: { mode: null, guard: "tools" }
14367
14668
  };
@@ -14471,6 +14772,8 @@ function createPiAdapter(options = {}) {
14471
14772
  a mandar lo que devolvio a su editor (hito 45, §16.18).
14472
14773
  */
14473
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 },
14474
14777
  // Sacar la cola para editarla en el cuadro (§6.35.1): vuelve lo que la app le mando.
14475
14778
  queueEdit: createPiQueueEdit(),
14476
14779
  // `settings.json` no se abre: sin modelo por defecto que anunciar. Al reanudar,
@@ -15196,12 +15499,13 @@ var ConversationHub = class extends EventEmitter {
15196
15499
  /**
15197
15500
  * Avisa la pregunta leida de la pantalla si cambio. No se avisa la que el
15198
15501
  * historial ya tiene abierta: esa tarjeta ya esta en el hilo, con el id de la
15199
- * 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).
15200
15504
  */
15201
15505
  refreshLiveQuestion(terminalId, entry) {
15202
15506
  if (this.entries.get(terminalId) !== entry) return;
15203
15507
  const read2 = entry.live?.current()?.part ?? null;
15204
- const pending = read2 === null ? null : this.pendingQuestionOf(entry);
15508
+ const pending = read2 === null ? null : this.pendingQuestionOf(terminalId, entry);
15205
15509
  const item = read2?.questions[0];
15206
15510
  const shown = read2 !== null && item !== void 0 && !(pending?.questions.some((each) => sameQuestionItem(each, item)) ?? false) ? read2 : null;
15207
15511
  if (shown?.toolUseId === entry.liveShown?.toolUseId) return;
@@ -15290,10 +15594,10 @@ var ConversationHub = class extends EventEmitter {
15290
15594
  */
15291
15595
  refreshOpenToolCall(terminalId, entry) {
15292
15596
  const launchedAt = this.isBlind(entry) ? this.registry.launchedAtOf(terminalId) : null;
15293
- const open9 = launchedAt !== null && entry.follower.hasOpenToolCall(launchedAt);
15294
- if (open9 === entry.openToolCall) return;
15295
- entry.openToolCall = open9;
15296
- 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);
15297
15601
  }
15298
15602
  /**
15299
15603
  * Recalcula los subagentes que siguen trabajando y avisa si cambiaron
@@ -15741,10 +16045,15 @@ var ConversationHub = class extends EventEmitter {
15741
16045
  * Se recorre desde el final: la ultima pregunta sin su `tool_result` es la
15742
16046
  * abierta. Si ya tiene resultado —porque se contesto desde la solapa CLI—
15743
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.
15744
16053
  */
15745
16054
  getPendingQuestion(terminalId) {
15746
16055
  const entry = this.entries.get(terminalId);
15747
- return entry === void 0 ? null : this.pendingQuestionOf(entry);
16056
+ return entry === void 0 ? null : this.pendingQuestionOf(terminalId, entry);
15748
16057
  }
15749
16058
  /**
15750
16059
  * La pregunta leida de la pantalla que el hilo esta mostrando (§5.5.4), o
@@ -15774,7 +16083,7 @@ var ConversationHub = class extends EventEmitter {
15774
16083
  if (entry === void 0 || entry.live === null || item === void 0) return "unknown";
15775
16084
  return entry.live.check(item);
15776
16085
  }
15777
- pendingQuestionOf(entry) {
16086
+ pendingQuestionOf(terminalId, entry) {
15778
16087
  const { events } = entry.follower.getTail(PENDING_QUESTION_LOOKBACK);
15779
16088
  const answered = /* @__PURE__ */ new Set();
15780
16089
  for (const event2 of events) {
@@ -15783,11 +16092,13 @@ var ConversationHub = class extends EventEmitter {
15783
16092
  }
15784
16093
  }
15785
16094
  for (let index = events.length - 1; index >= 0; index -= 1) {
15786
- const parts = events[index]?.parts ?? [];
16095
+ const event2 = events[index];
16096
+ const parts = event2?.parts ?? [];
15787
16097
  for (let at = parts.length - 1; at >= 0; at -= 1) {
15788
16098
  const part = parts[at];
15789
16099
  if (part?.kind !== "question") continue;
15790
- 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;
15791
16102
  }
15792
16103
  }
15793
16104
  return null;
@@ -18081,9 +18392,9 @@ function findMarkers(content, lines, ignoredFences) {
18081
18392
  }
18082
18393
  continue;
18083
18394
  }
18084
- const open9 = FENCE_OPEN.exec(text);
18085
- const opening = open9?.[1];
18086
- 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("`"))) {
18087
18398
  fence = { line: index, char: opening[0] ?? "`", length: opening.length };
18088
18399
  continue;
18089
18400
  }
@@ -19299,8 +19610,8 @@ function buildContinuationMessage(input) {
19299
19610
  }
19300
19611
  return sanitizeForPaste(lines.join("\n"));
19301
19612
  }
19302
- function transcriptReferenceFor(path74, style) {
19303
- return style === "at-quoted" ? fileReference(path74, "at-quoted") : `"${path74}"`;
19613
+ function transcriptReferenceFor(path78, style) {
19614
+ return style === "at-quoted" ? fileReference(path78, "at-quoted") : `"${path78}"`;
19304
19615
  }
19305
19616
  function planTranscript(input) {
19306
19617
  const turns = splitTurns(input.events);
@@ -20241,7 +20552,8 @@ function defaultAppSettings() {
20241
20552
  vault: { enabled: false, dir: null, toolResultMaxChars: VAULT_TOOL_RESULT_DEFAULT_CHARS },
20242
20553
  remote: { enabled: false, port: REMOTE_DEFAULT_PORT },
20243
20554
  console: { shell: "auto" },
20244
- agents: { disabled: [] }
20555
+ agents: { disabled: [] },
20556
+ scheduledMessages: { enabled: false }
20245
20557
  };
20246
20558
  }
20247
20559
  function isAbsoluteDir(dir, platform) {
@@ -20263,6 +20575,8 @@ function parseAppSettings(value, platform, warn = () => void 0) {
20263
20575
  const agentsRecord = asRecord(record["agents"]);
20264
20576
  const disabled = agentsRecord === null ? null : asAgentIdList(agentsRecord["disabled"]);
20265
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;
20266
20580
  const remote = asRecord(record["remote"]);
20267
20581
  if (remote !== null) {
20268
20582
  settings2.remote.enabled = asBoolean(remote["enabled"]) ?? false;
@@ -20286,7 +20600,8 @@ function frozen(settings2) {
20286
20600
  vault: Object.freeze({ ...settings2.vault }),
20287
20601
  remote: Object.freeze({ ...settings2.remote }),
20288
20602
  console: Object.freeze({ ...settings2.console }),
20289
- 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 })
20290
20605
  });
20291
20606
  }
20292
20607
  var SettingsStore = class {
@@ -20342,6 +20657,23 @@ var SettingsStore = class {
20342
20657
  if (settings2 === null) return;
20343
20658
  this.current = frozen({ ...this.current, remote: settings2.remote });
20344
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
+ }
20345
20677
  /** Una marca que cambia cuando el archivo cambia (fecha y tamano), o '' si no esta. */
20346
20678
  async stamp() {
20347
20679
  try {
@@ -20363,12 +20695,14 @@ var SettingsStore = class {
20363
20695
  update(patch) {
20364
20696
  const run2 = async () => {
20365
20697
  if (patch.remote === void 0) await this.reloadRemote();
20698
+ if (patch.scheduledMessages === void 0) await this.reloadScheduledMessages();
20366
20699
  const next = {
20367
20700
  version: SETTINGS_VERSION,
20368
20701
  vault: { ...this.current.vault },
20369
20702
  remote: { ...this.current.remote },
20370
20703
  console: { ...this.current.console },
20371
- agents: { disabled: [...this.current.agents.disabled] }
20704
+ agents: { disabled: [...this.current.agents.disabled] },
20705
+ scheduledMessages: { ...this.current.scheduledMessages }
20372
20706
  };
20373
20707
  const vault = patch.vault ?? {};
20374
20708
  if (vault.enabled !== void 0) next.vault.enabled = vault.enabled;
@@ -20383,6 +20717,7 @@ var SettingsStore = class {
20383
20717
  }
20384
20718
  if (patch.console?.shell !== void 0) next.console.shell = patch.console.shell;
20385
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;
20386
20721
  const remote = patch.remote ?? {};
20387
20722
  if (remote.enabled !== void 0) next.remote.enabled = remote.enabled;
20388
20723
  if (remote.port !== void 0) {
@@ -20942,10 +21277,353 @@ var DraftStore = class {
20942
21277
  }
20943
21278
  };
20944
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
+
20945
21623
  // packages/server/src/shell-locator.ts
20946
21624
  import { access as access2 } from "node:fs/promises";
20947
21625
  import { constants as fsConstants2 } from "node:fs";
20948
- import path56 from "node:path";
21626
+ import path58 from "node:path";
20949
21627
  async function isExecutable(candidate) {
20950
21628
  try {
20951
21629
  await access2(candidate, process.platform === "win32" ? fsConstants2.R_OK : fsConstants2.X_OK);
@@ -20963,21 +21641,21 @@ var SYSTEM_PROBE = {
20963
21641
  function windowsPowerShellPath(env) {
20964
21642
  const systemRoot = env["SystemRoot"] ?? env["windir"];
20965
21643
  if (systemRoot === void 0 || systemRoot.length === 0) return null;
20966
- return path56.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
21644
+ return path58.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
20967
21645
  }
20968
21646
  function gitBashCandidates(gitPath, env) {
20969
21647
  const candidates = [];
20970
21648
  if (gitPath !== null) {
20971
- let dir = path56.win32.dirname(gitPath);
21649
+ let dir = path58.win32.dirname(gitPath);
20972
21650
  for (let level = 0; level < 3; level++) {
20973
- candidates.push(path56.win32.join(dir, "bin", "bash.exe"));
20974
- const parent = path56.win32.dirname(dir);
21651
+ candidates.push(path58.win32.join(dir, "bin", "bash.exe"));
21652
+ const parent = path58.win32.dirname(dir);
20975
21653
  if (parent === dir) break;
20976
21654
  dir = parent;
20977
21655
  }
20978
21656
  }
20979
- for (const base of [env["ProgramFiles"], env["LOCALAPPDATA"] === void 0 ? void 0 : path56.win32.join(env["LOCALAPPDATA"], "Programs")]) {
20980
- 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"));
20981
21659
  }
20982
21660
  const systemRoot = (env["SystemRoot"] ?? env["windir"] ?? "").toLowerCase();
20983
21661
  return [...new Set(candidates)].filter((candidate) => systemRoot.length === 0 || !candidate.toLowerCase().startsWith(systemRoot));
@@ -21008,7 +21686,7 @@ async function locatePosixShells(probe) {
21008
21686
  const found = [];
21009
21687
  const preferred = probe.env["SHELL"];
21010
21688
  if (preferred !== void 0 && preferred.length > 0 && await probe.isExecutable(preferred)) {
21011
- const name = path56.posix.basename(preferred);
21689
+ const name = path58.posix.basename(preferred);
21012
21690
  found.push({ id: "login", label: name, location: { file: preferred, args: [], name } });
21013
21691
  }
21014
21692
  for (const [id, file] of [["bash", "/bin/bash"], ["sh", "/bin/sh"], ["zsh", "/bin/zsh"]]) {
@@ -21057,9 +21735,18 @@ var AppSettingsService = class extends EventEmitter5 {
21057
21735
  consoleShells: this.options.shells.map(({ id, label }) => ({ id, label })),
21058
21736
  shellName: this.currentShell()?.name ?? null,
21059
21737
  disabledAgents: [...settings2.agents.disabled],
21060
- disabledAtStart: [...this.disabledAtStart]
21738
+ disabledAtStart: [...this.disabledAtStart],
21739
+ scheduledMessages: settings2.scheduledMessages.enabled
21061
21740
  };
21062
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
+ }
21063
21750
  /** Guarda el cambio, avisa al registro si cambio la consola, y emite `status`. */
21064
21751
  async update(change) {
21065
21752
  if (change.consoleShell !== void 0 && !this.offered(change.consoleShell)) {
@@ -21068,7 +21755,8 @@ var AppSettingsService = class extends EventEmitter5 {
21068
21755
  const before = this.currentShell();
21069
21756
  await this.options.settings.update({
21070
21757
  ...change.consoleShell !== void 0 ? { console: { shell: change.consoleShell } } : {},
21071
- ...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 } } : {}
21072
21760
  });
21073
21761
  const after = this.currentShell();
21074
21762
  if (after !== before) this.options.onShellChange(after);
@@ -21080,13 +21768,13 @@ var AppSettingsService = class extends EventEmitter5 {
21080
21768
  };
21081
21769
 
21082
21770
  // packages/server/src/session-watcher.ts
21083
- import { stat as stat34 } from "node:fs/promises";
21771
+ import { stat as stat35 } from "node:fs/promises";
21084
21772
  import chokidar3 from "chokidar";
21085
21773
  var INDEX_DEBOUNCE_MS = 750;
21086
21774
  var ROOT_PROBE_MS = 5e3;
21087
21775
  async function isDirectory3(target) {
21088
21776
  try {
21089
- return (await stat34(target)).isDirectory();
21777
+ return (await stat35(target)).isDirectory();
21090
21778
  } catch {
21091
21779
  return false;
21092
21780
  }
@@ -21145,9 +21833,9 @@ function watchSessions(index, hub, agents, options = {}) {
21145
21833
  const probe = setInterval(() => {
21146
21834
  if (checking) return;
21147
21835
  checking = true;
21148
- void isDirectory3(root.path).then((exists6) => {
21836
+ void isDirectory3(root.path).then((exists7) => {
21149
21837
  checking = false;
21150
- if (!exists6 || stopped || !probes.has(probe)) return;
21838
+ if (!exists7 || stopped || !probes.has(probe)) return;
21151
21839
  clearInterval(probe);
21152
21840
  probes.delete(probe);
21153
21841
  watchRoot(agent, root, true);
@@ -21162,9 +21850,9 @@ function watchSessions(index, hub, agents, options = {}) {
21162
21850
  subscribeRoot(adapter.id, root, root.watch);
21163
21851
  continue;
21164
21852
  }
21165
- void isDirectory3(root.path).then((exists6) => {
21853
+ void isDirectory3(root.path).then((exists7) => {
21166
21854
  if (stopped) return;
21167
- if (exists6) watchRoot(adapter.id, root, false);
21855
+ if (exists7) watchRoot(adapter.id, root, false);
21168
21856
  else probeRoot(adapter.id, root);
21169
21857
  });
21170
21858
  }
@@ -21189,7 +21877,7 @@ function watchSessions(index, hub, agents, options = {}) {
21189
21877
  // packages/server/src/terminal-registry.ts
21190
21878
  import { randomUUID as randomUUID5 } from "node:crypto";
21191
21879
  import { EventEmitter as EventEmitter6 } from "node:events";
21192
- import { stat as stat35 } from "node:fs/promises";
21880
+ import { stat as stat36 } from "node:fs/promises";
21193
21881
 
21194
21882
  // packages/server/src/launch-hook-slot.ts
21195
21883
  var LaunchHookSlot = class {
@@ -21490,9 +22178,9 @@ async function resolveLaunchPlan(launch) {
21490
22178
  }
21491
22179
 
21492
22180
  // packages/server/src/workspace-store.ts
21493
- import { mkdir as mkdir14, readFile as readFile24, writeFile as writeFile9 } from "node:fs/promises";
21494
- import path57 from "node:path";
21495
- 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;
21496
22184
  var WRITE_DEBOUNCE_MS4 = 400;
21497
22185
  var LEGACY_TAB_AGENT = "claude-code";
21498
22186
  function placeByPosition(base, placed) {
@@ -21556,7 +22244,7 @@ function parseState4(raw) {
21556
22244
  try {
21557
22245
  const parsed = JSON.parse(raw);
21558
22246
  if (!isRecord(parsed)) return null;
21559
- if (parsed["version"] !== STATE_VERSION4) return null;
22247
+ if (parsed["version"] !== STATE_VERSION5) return null;
21560
22248
  const rawTabs = parsed["tabs"];
21561
22249
  if (!Array.isArray(rawTabs)) return null;
21562
22250
  const base = [];
@@ -21594,7 +22282,7 @@ function serializeState(state) {
21594
22282
  otherTabs.push({ ...entry.tab, position });
21595
22283
  }
21596
22284
  }
21597
- 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 };
21598
22286
  }
21599
22287
  var EMPTY_STATE = () => ({ tabs: [], foreignTabs: [] });
21600
22288
  function persistableTabs(descriptors) {
@@ -21618,7 +22306,7 @@ var WorkspaceStore = class {
21618
22306
  pending = null;
21619
22307
  async load() {
21620
22308
  try {
21621
- const raw = await readFile24(workspaceStatePath(), "utf8");
22309
+ const raw = await readFile25(workspaceStatePath(), "utf8");
21622
22310
  return parseState4(raw) ?? EMPTY_STATE();
21623
22311
  } catch {
21624
22312
  return EMPTY_STATE();
@@ -21648,10 +22336,10 @@ var WorkspaceStore = class {
21648
22336
  }
21649
22337
  async writeNow(state) {
21650
22338
  try {
21651
- await mkdir14(appConfigDir(), { recursive: true });
22339
+ await mkdir16(appConfigDir(), { recursive: true });
21652
22340
  const target = workspaceStatePath();
21653
- const temporary = path57.join(path57.dirname(target), `workspace.${process.pid}.tmp`);
21654
- 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");
21655
22343
  const { rename: rename8 } = await import("node:fs/promises");
21656
22344
  await rename8(temporary, target);
21657
22345
  } catch (error) {
@@ -21919,7 +22607,7 @@ var TerminalRegistry = class extends EventEmitter6 {
21919
22607
  async assertDirectory(cwd) {
21920
22608
  let info;
21921
22609
  try {
21922
- info = await stat35(cwd);
22610
+ info = await stat36(cwd);
21923
22611
  } catch {
21924
22612
  throw new TerminalOpenError("invalid-cwd", serverText("cwdMissing", { cwd }));
21925
22613
  }
@@ -22377,26 +23065,26 @@ import { WebSocketServer } from "ws";
22377
23065
 
22378
23066
  // packages/server/src/directory-picker.ts
22379
23067
  import { randomUUID as randomUUID6 } from "node:crypto";
22380
- 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";
22381
23069
  import { homedir as homedir14 } from "node:os";
22382
- import path58 from "node:path";
23070
+ import path60 from "node:path";
22383
23071
  var MAX_ENTRIES = 500;
22384
23072
  var MAX_PICKERS = 8;
22385
23073
  var SKIPPED = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "bin", "obj", "$RECYCLE.BIN"]);
22386
23074
  var DirectoryPickerError = class extends ServerTextError {
22387
23075
  };
22388
23076
  function isInsideProtected(target, dirs) {
22389
- const resolved = path58.resolve(target);
23077
+ const resolved = path60.resolve(target);
22390
23078
  return dirs.some((dir) => {
22391
- const protectedDir = path58.resolve(dir);
22392
- return resolved === protectedDir || resolved.startsWith(protectedDir + path58.sep);
23079
+ const protectedDir = path60.resolve(dir);
23080
+ return resolved === protectedDir || resolved.startsWith(protectedDir + path60.sep);
22393
23081
  });
22394
23082
  }
22395
23083
  async function isListedDirectory(parent, entry, readLink = lstat) {
22396
23084
  if (entry.isDirectory()) return true;
22397
23085
  if (!entry.isSymbolicLink()) return false;
22398
23086
  try {
22399
- const info = await readLink(path58.join(parent, entry.name));
23087
+ const info = await readLink(path60.join(parent, entry.name));
22400
23088
  return info.isDirectory() && !info.isSymbolicLink();
22401
23089
  } catch {
22402
23090
  return false;
@@ -22411,7 +23099,7 @@ async function listRoots() {
22411
23099
  letters.map(async (letter) => {
22412
23100
  const drive = `${letter}:\\`;
22413
23101
  try {
22414
- await stat36(drive);
23102
+ await stat37(drive);
22415
23103
  return drive;
22416
23104
  } catch {
22417
23105
  return null;
@@ -22463,14 +23151,14 @@ var DirectoryPickers = class {
22463
23151
  async enter(pickerId, name) {
22464
23152
  const picker = this.require(pickerId);
22465
23153
  if (name === "..") {
22466
- const parent = path58.dirname(picker.current);
23154
+ const parent = path60.dirname(picker.current);
22467
23155
  if (parent !== picker.current) picker.current = parent;
22468
23156
  return this.listing(picker);
22469
23157
  }
22470
23158
  if (!picker.entries.has(name)) {
22471
23159
  throw new DirectoryPickerError(serverText("pickerNotListed"));
22472
23160
  }
22473
- const target = path58.join(picker.current, name);
23161
+ const target = path60.join(picker.current, name);
22474
23162
  if (isInsideProtected(target, this.protectedDirs)) {
22475
23163
  throw new DirectoryPickerError(serverText("pickerCliFolder"));
22476
23164
  }
@@ -22510,12 +23198,12 @@ var DirectoryPickers = class {
22510
23198
  throw new DirectoryPickerError(serverText("pickerNameChars"));
22511
23199
  }
22512
23200
  }
22513
- const target = path58.join(picker.current, clean);
23201
+ const target = path60.join(picker.current, clean);
22514
23202
  if (isInsideProtected(target, this.protectedDirs)) {
22515
23203
  throw new DirectoryPickerError(serverText("pickerNoProjectsHere"));
22516
23204
  }
22517
23205
  try {
22518
- await mkdir15(target);
23206
+ await mkdir17(target);
22519
23207
  } catch (error) {
22520
23208
  const code = error.code;
22521
23209
  throw new DirectoryPickerError(
@@ -22546,9 +23234,9 @@ var DirectoryPickers = class {
22546
23234
  let names = [];
22547
23235
  let truncated = false;
22548
23236
  try {
22549
- const found = await readdir14(picker.current, { withFileTypes: true });
23237
+ const found = await readdir16(picker.current, { withFileTypes: true });
22550
23238
  const listed = await Promise.all(found.map((entry) => isListedDirectory(picker.current, entry)));
22551
- 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));
22552
23240
  truncated = dirs.length > MAX_ENTRIES;
22553
23241
  names = dirs.slice(0, MAX_ENTRIES);
22554
23242
  } catch {
@@ -22559,7 +23247,7 @@ var DirectoryPickers = class {
22559
23247
  path: picker.current,
22560
23248
  entries: names,
22561
23249
  roots,
22562
- canGoUp: path58.dirname(picker.current) !== picker.current,
23250
+ canGoUp: path60.dirname(picker.current) !== picker.current,
22563
23251
  truncated
22564
23252
  };
22565
23253
  }
@@ -22619,7 +23307,7 @@ var PlanLimitsBoard = class {
22619
23307
 
22620
23308
  // packages/server/src/cli-folders.ts
22621
23309
  import { realpath as realpath4 } from "node:fs/promises";
22622
- import path59 from "node:path";
23310
+ import path61 from "node:path";
22623
23311
  function isSameOrInside(child, parent, platform) {
22624
23312
  const childKey = normalizeCwdKey(child, platform);
22625
23313
  const parentKey = normalizeCwdKey(parent, platform);
@@ -22631,7 +23319,7 @@ function isSameOrInside(child, parent, platform) {
22631
23319
  async function cliFolders(dirs, platform = process.platform) {
22632
23320
  const known = [];
22633
23321
  for (const dir of dirs) {
22634
- known.push(path59.resolve(dir));
23322
+ known.push(path61.resolve(dir));
22635
23323
  try {
22636
23324
  known.push(await realpath4(dir));
22637
23325
  } catch {
@@ -22654,8 +23342,8 @@ async function cliFolders(dirs, platform = process.platform) {
22654
23342
 
22655
23343
  // packages/server/src/file-browser.ts
22656
23344
  import { createHash as createHash4 } from "node:crypto";
22657
- import { readFile as readFile25, readdir as readdir15, realpath as realpath5, stat as stat37 } from "node:fs/promises";
22658
- 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";
22659
23347
  var MAX_ENTRIES2 = 1e3;
22660
23348
  var SEARCH_MAX_RESULTS = 200;
22661
23349
  var SEARCH_MAX_DIRECTORIES = 4e3;
@@ -22718,7 +23406,7 @@ function languageFor(fileName) {
22718
23406
  const lower = fileName.toLowerCase();
22719
23407
  const byName = LANGUAGE_BY_NAME[lower];
22720
23408
  if (byName !== void 0) return byName;
22721
- const extension = path60.extname(lower);
23409
+ const extension = path62.extname(lower);
22722
23410
  return LANGUAGE_BY_EXTENSION[extension] ?? null;
22723
23411
  }
22724
23412
  function looksBinary2(buffer) {
@@ -22755,7 +23443,7 @@ async function listDirectory(cwd, relativePath, folders, options = {}) {
22755
23443
  let sizeBytes = 0;
22756
23444
  let modifiedAt = 0;
22757
23445
  try {
22758
- const info = await stat37(path60.join(absolute, candidate.name));
23446
+ const info = await stat38(path62.join(absolute, candidate.name));
22759
23447
  sizeBytes = candidate.isDirectory ? 0 : info.size;
22760
23448
  modifiedAt = info.mtimeMs;
22761
23449
  } catch {
@@ -22781,7 +23469,7 @@ async function scanLevel(absolute, repoRoot2, folders) {
22781
23469
  return level;
22782
23470
  }
22783
23471
  async function readLevel(absolute, folders) {
22784
- const dirents = await readdir15(absolute, { withFileTypes: true });
23472
+ const dirents = await readdir17(absolute, { withFileTypes: true });
22785
23473
  const realLevel = await realpath5(absolute);
22786
23474
  let skipped = 0;
22787
23475
  const candidates = [];
@@ -22789,8 +23477,8 @@ async function readLevel(absolute, folders) {
22789
23477
  const always = ALWAYS_SKIP.has(dirent.name.toLowerCase());
22790
23478
  if (dirent.isSymbolicLink()) {
22791
23479
  try {
22792
- const linked = path60.join(absolute, dirent.name);
22793
- const target = await stat37(linked);
23480
+ const linked = path62.join(absolute, dirent.name);
23481
+ const target = await stat38(linked);
22794
23482
  if (folders.holds(await realpath5(linked))) continue;
22795
23483
  candidates.push({
22796
23484
  name: dirent.name,
@@ -22806,7 +23494,7 @@ async function readLevel(absolute, folders) {
22806
23494
  skipped += 1;
22807
23495
  continue;
22808
23496
  }
22809
- if (folders.holds(path60.join(realLevel, dirent.name))) continue;
23497
+ if (folders.holds(path62.join(realLevel, dirent.name))) continue;
22810
23498
  candidates.push({
22811
23499
  name: dirent.name,
22812
23500
  isDirectory: dirent.isDirectory(),
@@ -22821,7 +23509,7 @@ async function markIgnored(repoRoot2, levels) {
22821
23509
  for (const level of levels) {
22822
23510
  for (const entry of level.entries) {
22823
23511
  if (entry.hidden !== null) continue;
22824
- 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))));
22825
23513
  }
22826
23514
  }
22827
23515
  if (keys.size === 0) return;
@@ -22883,7 +23571,7 @@ async function searchFiles(cwd, query, folders, options = {}) {
22883
23571
  let sizeBytes = 0;
22884
23572
  let modifiedAt = 0;
22885
23573
  try {
22886
- const info = await stat37(path60.join(level.absolute, candidate.name));
23574
+ const info = await stat38(path62.join(level.absolute, candidate.name));
22887
23575
  sizeBytes = candidate.isDirectory ? 0 : info.size;
22888
23576
  modifiedAt = info.mtimeMs;
22889
23577
  } catch {
@@ -22915,16 +23603,19 @@ async function searchFiles(cwd, query, folders, options = {}) {
22915
23603
  truncated: truncated || found.length > SEARCH_MAX_RESULTS
22916
23604
  };
22917
23605
  }
22918
- async function existingFiles(cwd, relativePaths, folders) {
22919
- const existing = [];
23606
+ async function existingPaths(cwd, relativePaths, folders) {
23607
+ const files = [];
23608
+ const directories = [];
22920
23609
  for (const relativePath of relativePaths) {
22921
23610
  try {
22922
23611
  const absolute = await resolveForFiles(cwd, relativePath, folders);
22923
- if ((await stat37(absolute)).isFile()) existing.push(relativePath);
23612
+ const info = await stat38(absolute);
23613
+ if (info.isFile()) files.push(relativePath);
23614
+ else if (info.isDirectory()) directories.push(relativePath);
22924
23615
  } catch {
22925
23616
  }
22926
23617
  }
22927
- return existing;
23618
+ return { files, directories };
22928
23619
  }
22929
23620
  function contentHash(content) {
22930
23621
  return createHash4("sha256").update(content).digest("hex");
@@ -22934,12 +23625,12 @@ function isEditableText(content, text) {
22934
23625
  }
22935
23626
  async function readPreview(cwd, relativePath, folders) {
22936
23627
  const absolute = await resolveForFiles(cwd, relativePath, folders);
22937
- const info = await stat37(absolute);
23628
+ const info = await stat38(absolute);
22938
23629
  if (info.isDirectory()) {
22939
23630
  throw new Error("It's a directory, not a file.");
22940
23631
  }
22941
- const content = await readFile25(absolute);
22942
- const fileName = path60.basename(absolute);
23632
+ const content = await readFile26(absolute);
23633
+ const fileName = path62.basename(absolute);
22943
23634
  if (looksBinary2(content)) {
22944
23635
  return {
22945
23636
  path: relativePath,
@@ -22965,7 +23656,7 @@ async function readPreview(cwd, relativePath, folders) {
22965
23656
 
22966
23657
  // packages/server/src/file-write.ts
22967
23658
  import { constants } from "node:fs";
22968
- 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";
22969
23660
  var FileWriteError = class extends ServerTextError {
22970
23661
  };
22971
23662
  function withLineEndings(text, original) {
@@ -22990,9 +23681,9 @@ async function saveProjectFile(cwd, relativePath, text, baseHash, folders) {
22990
23681
  }
22991
23682
  if (await folders.contains(absolute)) throw new InvalidPathError(serverText("cliFolderClosed"));
22992
23683
  const target = await realpath6(absolute);
22993
- const info = await stat38(target);
23684
+ const info = await stat39(target);
22994
23685
  if (!info.isFile()) throw new FileWriteError(serverText("fileSaveFailed"));
22995
- const current = await readFile26(target);
23686
+ const current = await readFile27(target);
22996
23687
  if (contentHash(current) !== baseHash) {
22997
23688
  return { outcome: "changed", preview: await readPreview(cwd, relativePath, folders).catch(() => null) };
22998
23689
  }
@@ -23302,6 +23993,109 @@ function prefillReasonFor(outcome) {
23302
23993
  }
23303
23994
  }
23304
23995
 
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
+
23305
24099
  // packages/server/src/vault/read-session.ts
23306
24100
  var MAX_POLLS = 5;
23307
24101
  var MAX_TOTAL_POLLS = 20;
@@ -23664,14 +24458,14 @@ var TerminalWriteQueue = class {
23664
24458
  };
23665
24459
 
23666
24460
  // packages/server/src/vault/service.ts
23667
- import { mkdir as mkdir18, stat as stat42 } from "node:fs/promises";
24461
+ import { mkdir as mkdir20, stat as stat43 } from "node:fs/promises";
23668
24462
  import { homedir as homedir15 } from "node:os";
23669
- import path64 from "node:path";
24463
+ import path66 from "node:path";
23670
24464
 
23671
24465
  // packages/server/src/vault/move.ts
23672
24466
  import { constants as constants2 } from "node:fs";
23673
- 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";
23674
- 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";
23675
24469
  var MOVED_ENTRIES = ["vault.json", "sessions", "memory"];
23676
24470
  var OWN_TEMP_FILE = /\.\d+\.[0-9a-f]{12}\.tmp$/;
23677
24471
  async function copyTree(source, target, result) {
@@ -23682,14 +24476,14 @@ async function copyTree(source, target, result) {
23682
24476
  return;
23683
24477
  }
23684
24478
  if (info.isDirectory()) {
23685
- await mkdir16(target, { recursive: true });
23686
- for (const name of await readdir16(source)) {
23687
- 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);
23688
24482
  }
23689
24483
  return;
23690
24484
  }
23691
24485
  if (!info.isFile() || OWN_TEMP_FILE.test(source)) return;
23692
- await mkdir16(path61.dirname(target), { recursive: true });
24486
+ await mkdir18(path63.dirname(target), { recursive: true });
23693
24487
  try {
23694
24488
  await copyFile2(source, target, constants2.COPYFILE_EXCL);
23695
24489
  } catch (error) {
@@ -23704,9 +24498,9 @@ async function copyTree(source, target, result) {
23704
24498
  }
23705
24499
  async function moveVault(from, to) {
23706
24500
  const result = { copied: 0, skipped: 0 };
23707
- await mkdir16(to, { recursive: true });
24501
+ await mkdir18(to, { recursive: true });
23708
24502
  for (const entry of MOVED_ENTRIES) {
23709
- await copyTree(path61.join(from, entry), path61.join(to, entry), result);
24503
+ await copyTree(path63.join(from, entry), path63.join(to, entry), result);
23710
24504
  }
23711
24505
  return result;
23712
24506
  }
@@ -23738,16 +24532,16 @@ function checkVaultTarget(current, target, options) {
23738
24532
  return { kind: "ok" };
23739
24533
  }
23740
24534
  async function canWriteInto(dir) {
23741
- const probe = temporaryPathFor(path61.join(dir, ".escritura"));
24535
+ const probe = temporaryPathFor(path63.join(dir, ".escritura"));
23742
24536
  try {
23743
- await mkdir16(dir, { recursive: true });
23744
- if (!(await stat39(dir)).isDirectory()) return false;
23745
- await writeFile10(probe, "");
24537
+ await mkdir18(dir, { recursive: true });
24538
+ if (!(await stat40(dir)).isDirectory()) return false;
24539
+ await writeFile11(probe, "");
23746
24540
  return true;
23747
24541
  } catch {
23748
24542
  return false;
23749
24543
  } finally {
23750
- await rm5(probe, { force: true }).catch(() => void 0);
24544
+ await rm7(probe, { force: true }).catch(() => void 0);
23751
24545
  }
23752
24546
  }
23753
24547
 
@@ -23844,12 +24638,12 @@ function serializeSession(input) {
23844
24638
  }
23845
24639
 
23846
24640
  // packages/server/src/vault/writer.ts
23847
- import { readdir as readdir18, rm as rm6, stat as stat41 } from "node:fs/promises";
23848
- 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";
23849
24643
 
23850
24644
  // packages/server/src/vault/memory-copy.ts
23851
- import { mkdir as mkdir17, readdir as readdir17, readFile as readFile27, stat as stat40, utimes as utimes2 } from "node:fs/promises";
23852
- 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";
23853
24647
  var MEMORY_FILE_MAX_BYTES = 1024 * 1024;
23854
24648
  var MEMORY_MAX_FILES = 500;
23855
24649
  var MEMORY_SOURCE_FILE = "source.json";
@@ -23864,7 +24658,7 @@ async function listMemoryNotes(cwd) {
23864
24658
  }
23865
24659
  let entries;
23866
24660
  try {
23867
- entries = await readdir17(folder, { withFileTypes: true });
24661
+ entries = await readdir19(folder, { withFileTypes: true });
23868
24662
  } catch {
23869
24663
  return [];
23870
24664
  }
@@ -23874,7 +24668,7 @@ async function listMemoryNotes(cwd) {
23874
24668
  let absolute;
23875
24669
  try {
23876
24670
  absolute = await resolveInside(cwd, `${MEMORY_DIR}/${name}`, { mustExist: true });
23877
- const info = await stat40(absolute);
24671
+ const info = await stat41(absolute);
23878
24672
  if (!info.isFile() || info.size > MEMORY_FILE_MAX_BYTES) continue;
23879
24673
  notes.push({ name, absolute, size: info.size, mtimeMs: info.mtimeMs });
23880
24674
  } catch {
@@ -23888,7 +24682,7 @@ async function measureProjectMemory(cwd) {
23888
24682
  }
23889
24683
  async function isUpToDate(target, note) {
23890
24684
  try {
23891
- const info = await stat40(target);
24685
+ const info = await stat41(target);
23892
24686
  return info.isFile() && info.size === note.size && Math.abs(info.mtimeMs - note.mtimeMs) < SAME_MTIME_MS;
23893
24687
  } catch {
23894
24688
  return false;
@@ -23896,7 +24690,7 @@ async function isUpToDate(target, note) {
23896
24690
  }
23897
24691
  async function exists3(file) {
23898
24692
  try {
23899
- await stat40(file);
24693
+ await stat41(file);
23900
24694
  return true;
23901
24695
  } catch {
23902
24696
  return false;
@@ -23912,14 +24706,14 @@ async function copyProjectMemory(dir, cwd, platform, deps = DEFAULT_WRITE_DEPS)
23912
24706
  if (prepared) return;
23913
24707
  prepared = true;
23914
24708
  await ensureVaultMarker(dir, deps);
23915
- await mkdir17(target, { recursive: true });
24709
+ await mkdir19(target, { recursive: true });
23916
24710
  };
23917
24711
  for (const note of notes) {
23918
- const destination = path62.join(target, note.name);
24712
+ const destination = path64.join(target, note.name);
23919
24713
  if (await isUpToDate(destination, note)) continue;
23920
24714
  let content;
23921
24715
  try {
23922
- content = await readFile27(note.absolute);
24716
+ content = await readFile28(note.absolute);
23923
24717
  } catch {
23924
24718
  continue;
23925
24719
  }
@@ -23931,7 +24725,7 @@ async function copyProjectMemory(dir, cwd, platform, deps = DEFAULT_WRITE_DEPS)
23931
24725
  result.files += 1;
23932
24726
  result.bytes += content.length;
23933
24727
  }
23934
- const source = path62.join(target, MEMORY_SOURCE_FILE);
24728
+ const source = path64.join(target, MEMORY_SOURCE_FILE);
23935
24729
  if (result.files > 0 || !await exists3(source)) {
23936
24730
  await prepare();
23937
24731
  await writeFileAtomic(source, `${JSON.stringify({ cwd, copiedAt: deps.now() })}
@@ -24425,7 +25219,7 @@ var VaultWriter = class {
24425
25219
  }
24426
25220
  async assetOnDisk(dir, agent, sessionId, asset, bytes) {
24427
25221
  try {
24428
- const info = await stat41(assetFile(dir, agent, sessionId, asset));
25222
+ const info = await stat42(assetFile(dir, agent, sessionId, asset));
24429
25223
  return info.isFile() && info.size === bytes;
24430
25224
  } catch {
24431
25225
  return false;
@@ -24525,30 +25319,30 @@ var VaultWriter = class {
24525
25319
  const folders = [dir];
24526
25320
  const subfolders = async (parent) => {
24527
25321
  try {
24528
- 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));
24529
25323
  } catch {
24530
25324
  return [];
24531
25325
  }
24532
25326
  };
24533
25327
  for (const agentDir of await subfolders(sessionsRoot2(dir))) {
24534
- if (!SESSION_AGENT_IDS.includes(path63.basename(agentDir))) continue;
25328
+ if (!SESSION_AGENT_IDS.includes(path65.basename(agentDir))) continue;
24535
25329
  folders.push(agentDir);
24536
25330
  folders.push(...(await subfolders(agentDir)).filter((folder) => folder.endsWith(".assets")));
24537
25331
  }
24538
- folders.push(...await subfolders(path63.join(dir, "memory")));
25332
+ folders.push(...await subfolders(path65.join(dir, "memory")));
24539
25333
  for (const folder of folders) {
24540
25334
  let names;
24541
25335
  try {
24542
- names = await readdir18(folder);
25336
+ names = await readdir20(folder);
24543
25337
  } catch {
24544
25338
  continue;
24545
25339
  }
24546
25340
  for (const name of names) {
24547
25341
  if (!OWN_TEMP_FILE2.test(name)) continue;
24548
- const file = path63.join(folder, name);
25342
+ const file = path65.join(folder, name);
24549
25343
  try {
24550
- const info = await stat41(file);
24551
- 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 });
24552
25346
  } catch {
24553
25347
  }
24554
25348
  }
@@ -24735,14 +25529,14 @@ var VaultService = class {
24735
25529
  async setDir(target) {
24736
25530
  const { platform, agents, settings: settings2, catalog } = this.options;
24737
25531
  const current = this.dir();
24738
- const protectedDirs = [...agents.protectedDirs(), path64.join(this.options.homeDir ?? homedir15(), ".gemini")];
25532
+ const protectedDirs = [...agents.protectedDirs(), path66.join(this.options.homeDir ?? homedir15(), ".gemini")];
24739
25533
  const check = checkVaultTarget(current, target, { protectedDirs, platform });
24740
25534
  if (check.kind === "refused") throw new VaultError(check.text);
24741
25535
  if (check.kind === "same") return;
24742
25536
  try {
24743
25537
  await this.writer.exclusive("moving", async () => {
24744
25538
  if (!await canWriteInto(target)) throw new VaultError(serverText("vaultCannotWrite"));
24745
- const hadCopy = await stat42(vaultMarkerFile(current)).then(
25539
+ const hadCopy = await stat43(vaultMarkerFile(current)).then(
24746
25540
  () => true,
24747
25541
  () => false
24748
25542
  );
@@ -24776,7 +25570,7 @@ var VaultService = class {
24776
25570
  /** Abre la carpeta de la copia con el explorador del sistema. */
24777
25571
  async reveal() {
24778
25572
  const dir = this.activeDir();
24779
- const isFolder = await stat42(dir).then(
25573
+ const isFolder = await stat43(dir).then(
24780
25574
  (info) => info.isDirectory(),
24781
25575
  () => false
24782
25576
  );
@@ -24800,7 +25594,7 @@ var VaultService = class {
24800
25594
  fromCopy: true
24801
25595
  });
24802
25596
  const file = sessionExportFile(dir, agent, sessionId);
24803
- await mkdir18(path64.dirname(file), { recursive: true });
25597
+ await mkdir20(path66.dirname(file), { recursive: true });
24804
25598
  await writeFileAtomic(file, text, this.writeDeps);
24805
25599
  this.openWithSystem(file);
24806
25600
  return file;
@@ -24887,7 +25681,7 @@ var VaultService = class {
24887
25681
  const project = this.options.index.getProjects().find((candidate) => candidate.key === projectKey);
24888
25682
  if (project === void 0) throw new VaultError(serverText("vaultProjectGone"));
24889
25683
  const folder = projectExportDirFor(this.activeDir(), project, this.options.platform);
24890
- await mkdir18(folder, { recursive: true });
25684
+ await mkdir20(folder, { recursive: true });
24891
25685
  const entries = [];
24892
25686
  const names = /* @__PURE__ */ new Set();
24893
25687
  let skipped = 0;
@@ -24904,7 +25698,7 @@ var VaultService = class {
24904
25698
  continue;
24905
25699
  }
24906
25700
  const fileName = this.exportFileName(summary, names);
24907
- await writeFileAtomic(path64.join(folder, fileName), text, this.writeDeps);
25701
+ await writeFileAtomic(path66.join(folder, fileName), text, this.writeDeps);
24908
25702
  entries.push({
24909
25703
  updatedAt: summary.updatedAt,
24910
25704
  agentLabel: this.labelOf(summary.agent),
@@ -24915,9 +25709,9 @@ var VaultService = class {
24915
25709
  });
24916
25710
  await nextTurn3();
24917
25711
  }
24918
- 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;
24919
25713
  const index = renderProjectIndex({ name, cwd: project.cwd, exportedAt: this.now(), entries, skipped });
24920
- await writeFileAtomic(path64.join(folder, "index.md"), index, this.writeDeps);
25714
+ await writeFileAtomic(path66.join(folder, "index.md"), index, this.writeDeps);
24921
25715
  this.openWithSystem(folder);
24922
25716
  return { sessions: entries.length, skipped, folder };
24923
25717
  }
@@ -25055,14 +25849,14 @@ var VaultService = class {
25055
25849
  // packages/server/src/worktrees/service.ts
25056
25850
  import { randomBytes as randomBytes6, randomUUID as randomUUID7 } from "node:crypto";
25057
25851
  import { EventEmitter as EventEmitter7 } from "node:events";
25058
- import { mkdir as mkdir22, rename as rename7, rm as rm9, rmdir, stat as stat44 } from "node:fs/promises";
25059
- import path70 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";
25060
25854
 
25061
25855
  // packages/server/src/worktrees/git.ts
25062
25856
  import { execFile as execFile6 } from "node:child_process";
25063
- import { mkdtemp, rm as rm7 } from "node:fs/promises";
25857
+ import { mkdtemp, rm as rm9, stat as stat44 } from "node:fs/promises";
25064
25858
  import { tmpdir as tmpdir5 } from "node:os";
25065
- import path65 from "node:path";
25859
+ import path67 from "node:path";
25066
25860
  var READ_TIMEOUT_MS = 15e3;
25067
25861
  var WRITE_TIMEOUT_MS = 12e4;
25068
25862
  var MAX_BUFFER = 32 * 1024 * 1024;
@@ -25073,7 +25867,7 @@ function run(cwd, args, options) {
25073
25867
  return new Promise((resolve, reject) => {
25074
25868
  const child = execFile6(
25075
25869
  "git",
25076
- ["-c", "core.quotepath=false", ...options.write ? [] : ["--no-optional-locks"], ...args],
25870
+ ["-c", "core.quotepath=false", "-c", "core.longpaths=true", ...options.write ? [] : ["--no-optional-locks"], ...args],
25077
25871
  {
25078
25872
  cwd,
25079
25873
  timeout: options.write ? WRITE_TIMEOUT_MS : READ_TIMEOUT_MS,
@@ -25108,10 +25902,10 @@ async function repoLayout(cwd) {
25108
25902
  if (result.code !== 0) return null;
25109
25903
  const [toplevel, gitDir, commonDir] = result.stdout.split(/\r?\n/).map((line) => line.trim());
25110
25904
  if (!toplevel || !gitDir || !commonDir) return null;
25111
- const absoluteCommon = path65.resolve(cwd, commonDir);
25112
- const linked = path65.resolve(gitDir).toLowerCase() !== absoluteCommon.toLowerCase();
25113
- const mainRoot = path65.basename(absoluteCommon).toLowerCase() === ".git" ? path65.dirname(absoluteCommon) : null;
25114
- 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 };
25115
25909
  }
25116
25910
  async function currentBranch(cwd) {
25117
25911
  const result = await read(cwd, ["symbolic-ref", "--quiet", "HEAD"]);
@@ -25126,15 +25920,42 @@ async function localBranches(cwd) {
25126
25920
  return result.stdout.split(/\r?\n/).map((line) => branchOfRef(line.trim())).filter((branch) => branch !== null);
25127
25921
  }
25128
25922
  async function listWorktrees(cwd) {
25923
+ return await readWorktreeList(cwd) ?? [];
25924
+ }
25925
+ async function readWorktreeList(cwd) {
25129
25926
  const result = await read(cwd, ["worktree", "list", "--porcelain", "-z"]);
25130
- return result.code === 0 ? parseWorktreeListZ(result.stdout) : [];
25927
+ return result.code === 0 ? parseWorktreeListZ(result.stdout) : null;
25131
25928
  }
25132
- async function uncommittedPaths(cwd) {
25929
+ async function uncommittedChanges(cwd) {
25133
25930
  const result = await read(cwd, ["status", "--porcelain=v2", "-z", "--untracked-files=normal"]);
25134
25931
  if (result.code !== 0) return null;
25932
+ const parsed = parsePorcelainV2(result.stdout);
25135
25933
  const seen = /* @__PURE__ */ new Set();
25136
- for (const change of parsePorcelainV2(result.stdout).changes) seen.add(change.path);
25137
- return [...seen];
25934
+ const untracked = [];
25935
+ for (const change of parsed.changes) {
25936
+ seen.add(change.path);
25937
+ if (change.kind === "untracked") untracked.push(change.path);
25938
+ }
25939
+ return { paths: [...seen], untracked, complete: !parsed.truncated };
25940
+ }
25941
+ async function uncommittedPaths(cwd) {
25942
+ return (await uncommittedChanges(cwd))?.paths ?? null;
25943
+ }
25944
+ async function hasLoadedSubmodule(cwd) {
25945
+ const [modules, staged] = await Promise.all([read(cwd, ["rev-parse", "--git-path", "modules"]), read(cwd, ["ls-files", "--stage", "-z"])]);
25946
+ if (modules.code !== 0 || staged.code !== 0) return null;
25947
+ if (await isDirectory4(path67.resolve(cwd, modules.stdout.trim()))) return true;
25948
+ const submodules = staged.stdout.split("\0").filter((line) => line.startsWith("160000 ")).map((line) => line.slice(line.indexOf(" ") + 1));
25949
+ for (const submodule of submodules) {
25950
+ if (await exists4(path67.join(cwd, submodule, ".git"))) return true;
25951
+ }
25952
+ return false;
25953
+ }
25954
+ async function isDirectory4(candidate) {
25955
+ return (await stat44(candidate).catch(() => null))?.isDirectory() === true;
25956
+ }
25957
+ async function exists4(candidate) {
25958
+ return await stat44(candidate).catch(() => null) !== null;
25138
25959
  }
25139
25960
  async function isIntegrated(cwd, branch, base) {
25140
25961
  const result = await read(cwd, ["merge-base", "--is-ancestor", `refs/heads/${branch}`, `refs/heads/${base}`]);
@@ -25282,7 +26103,7 @@ async function branchFileDiff(cwd, base, branch, file, oldPath) {
25282
26103
  async function objectsDir(mainRoot) {
25283
26104
  const result = await read(mainRoot, ["rev-parse", "--git-path", "objects"]);
25284
26105
  const answer = result.stdout.trim();
25285
- 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;
25286
26107
  }
25287
26108
  function alternatesValue(folder, platform = process.platform) {
25288
26109
  const separator = platform === "win32" ? ";" : ":";
@@ -25292,7 +26113,7 @@ function alternatesValue(folder, platform = process.platform) {
25292
26113
  async function mergeTreeCheck(mainRoot, base, branch) {
25293
26114
  const objects = await objectsDir(mainRoot);
25294
26115
  if (objects === null) return null;
25295
- 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);
25296
26117
  if (scratch === null) return null;
25297
26118
  try {
25298
26119
  const result = await read(
@@ -25303,7 +26124,7 @@ async function mergeTreeCheck(mainRoot, base, branch) {
25303
26124
  );
25304
26125
  return parseMergeTree(result);
25305
26126
  } finally {
25306
- 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);
25307
26128
  }
25308
26129
  }
25309
26130
  function parseMergeTree(result) {
@@ -25342,10 +26163,247 @@ function fastForward(baseFolder, target) {
25342
26163
  return run(baseFolder, fastForwardArgs(target), { write: true });
25343
26164
  }
25344
26165
 
26166
+ // packages/server/src/worktrees/holders.ts
26167
+ import { spawn as spawn4 } from "node:child_process";
26168
+ import path68 from "node:path";
26169
+ var SCAN_MAX_ENTRIES = 2e5;
26170
+ var SCAN_MAX_MS = 4e3;
26171
+ var HOLDERS_TIMEOUT_MS = 15e3;
26172
+ var OUTPUT_MAX_CHARS = 256 * 1024;
26173
+ var CSHARP = String.raw`
26174
+ using System;
26175
+ using System.Collections.Generic;
26176
+ using System.Diagnostics;
26177
+ using System.IO;
26178
+ using System.Runtime.InteropServices;
26179
+ using System.Text;
26180
+ using Microsoft.Win32.SafeHandles;
26181
+
26182
+ public static class AwFolderHolders {
26183
+ [StructLayout(LayoutKind.Sequential)]
26184
+ struct IoStatus { public IntPtr Status; public IntPtr Information; }
26185
+
26186
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
26187
+ struct FindData {
26188
+ public uint Attributes;
26189
+ public uint Created1, Created2, Accessed1, Accessed2, Written1, Written2;
26190
+ public uint SizeHigh, SizeLow, Reserved0, Reserved1;
26191
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string Name;
26192
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)] public string ShortName;
26193
+ }
26194
+
26195
+ [DllImport("ntdll.dll")]
26196
+ static extern int NtQueryInformationFile(SafeFileHandle file, out IoStatus status, IntPtr buffer, int length, int infoClass);
26197
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
26198
+ static extern SafeFileHandle CreateFileW(string name, uint access, uint share, IntPtr security, uint disposition, uint flags, IntPtr template);
26199
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
26200
+ static extern IntPtr FindFirstFileExW(string name, int level, out FindData data, int search, IntPtr filter, int flags);
26201
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
26202
+ static extern bool FindNextFileW(IntPtr find, out FindData data);
26203
+ [DllImport("kernel32.dll")]
26204
+ static extern bool FindClose(IntPtr find);
26205
+ [DllImport("kernel32.dll", SetLastError = true)]
26206
+ static extern IntPtr OpenProcess(uint access, bool inherit, int pid);
26207
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
26208
+ static extern bool QueryFullProcessImageNameW(IntPtr process, int flags, StringBuilder name, ref int size);
26209
+ [DllImport("kernel32.dll")]
26210
+ static extern bool CloseHandle(IntPtr handle);
26211
+
26212
+ const uint ReadData = 0x1, ReadAttributes = 0x80, Delete = 0x10000, ShareAll = 7, OpenExisting = 3;
26213
+ /* BACKUP_SEMANTICS abre carpetas; OPEN_REPARSE_POINT abre el enlace y no su destino. */
26214
+ const uint OpenFlags = 0x02000000 | 0x00200000;
26215
+ const int ProcessIdsUsingFile = 47, QueryLimited = 0x1000, MaxNamed = 32;
26216
+ static readonly IntPtr NoFind = new IntPtr(-1);
26217
+
26218
+ static bool Taken(string path, bool top) {
26219
+ using (SafeFileHandle file = CreateFileW(path, top ? Delete : ReadData, top ? ShareAll : 0, IntPtr.Zero, OpenExisting, OpenFlags, IntPtr.Zero)) {
26220
+ if (!file.IsInvalid) return false;
26221
+ int error = Marshal.GetLastWin32Error();
26222
+ return error != 2 && error != 3;
26223
+ }
26224
+ }
26225
+
26226
+ static void Collect(string path, HashSet<int> seen, List<int> found, int self) {
26227
+ using (SafeFileHandle file = CreateFileW(path, ReadAttributes, ShareAll, IntPtr.Zero, OpenExisting, OpenFlags, IntPtr.Zero)) {
26228
+ if (file.IsInvalid) return;
26229
+ for (int size = 4096; size <= 1 << 20; size *= 4) {
26230
+ IntPtr buffer = Marshal.AllocHGlobal(size);
26231
+ try {
26232
+ IoStatus status;
26233
+ int code = NtQueryInformationFile(file, out status, buffer, size, ProcessIdsUsingFile);
26234
+ if (code == unchecked((int)0xC0000004) || code == unchecked((int)0x80000005)) continue;
26235
+ if (code != 0) return;
26236
+ int count = Marshal.ReadInt32(buffer);
26237
+ if (IntPtr.Size * (count + 1) > size) continue;
26238
+ for (int i = 0; i < count; i++) {
26239
+ int pid = (int)Marshal.ReadIntPtr(buffer, IntPtr.Size * (i + 1)).ToInt64();
26240
+ if (pid != self && seen.Add(pid)) found.Add(pid);
26241
+ }
26242
+ return;
26243
+ } finally { Marshal.FreeHGlobal(buffer); }
26244
+ }
26245
+ }
26246
+ }
26247
+
26248
+ static string Json(string value) {
26249
+ if (value == null) return "null";
26250
+ StringBuilder text = new StringBuilder("\"");
26251
+ foreach (char c in value) {
26252
+ if (c == '"' || c == '\\') text.Append('\\').Append(c);
26253
+ else if (c < 0x20 || c > 0x7E) text.Append("\\u").Append(((int)c).ToString("x4"));
26254
+ else text.Append(c);
26255
+ }
26256
+ return text.Append('"').ToString();
26257
+ }
26258
+
26259
+ public static string Scan(string root, int maxEntries, int maxMs) {
26260
+ Stopwatch clock = Stopwatch.StartNew();
26261
+ int self = Process.GetCurrentProcess().Id;
26262
+ HashSet<int> seen = new HashSet<int>();
26263
+ List<int> found = new List<int>();
26264
+ List<string> files = new List<string>();
26265
+ Queue<string> dirs = new Queue<string>();
26266
+ bool complete = true;
26267
+ int entries = 0;
26268
+ if (Taken(root, true)) Collect(root, seen, found, self);
26269
+ dirs.Enqueue(root);
26270
+ while (dirs.Count > 0) {
26271
+ if (++entries > maxEntries || clock.ElapsedMilliseconds > maxMs) { complete = false; break; }
26272
+ string dir = dirs.Dequeue();
26273
+ if (dir != root && Taken(dir, false)) Collect(dir, seen, found, self);
26274
+ FindData data;
26275
+ IntPtr find = FindFirstFileExW(dir + "\\*", 1, out data, 0, IntPtr.Zero, 2);
26276
+ if (find == NoFind) continue;
26277
+ try {
26278
+ do {
26279
+ if (data.Name == "." || data.Name == "..") continue;
26280
+ string child = dir + "\\" + data.Name;
26281
+ if ((data.Attributes & 0x10) != 0 && (data.Attributes & 0x400) == 0) dirs.Enqueue(child);
26282
+ else if (files.Count < maxEntries) files.Add(child);
26283
+ } while (FindNextFileW(find, out data));
26284
+ } finally { FindClose(find); }
26285
+ }
26286
+ if (complete) {
26287
+ foreach (string file in files) {
26288
+ if (++entries > maxEntries || clock.ElapsedMilliseconds > maxMs) { complete = false; break; }
26289
+ if (Taken(file, false)) Collect(file, seen, found, self);
26290
+ }
26291
+ }
26292
+ StringBuilder json = new StringBuilder("{\"complete\":");
26293
+ json.Append(complete ? "true" : "false").Append(",\"ms\":").Append(clock.ElapsedMilliseconds).Append(",\"holders\":[");
26294
+ int named = 0;
26295
+ foreach (int pid in found) {
26296
+ if (named >= MaxNamed) break;
26297
+ string name = null, description = null;
26298
+ IntPtr process = OpenProcess(QueryLimited, false, pid);
26299
+ if (process != IntPtr.Zero) {
26300
+ try {
26301
+ StringBuilder image = new StringBuilder(32768);
26302
+ int length = image.Capacity;
26303
+ if (QueryFullProcessImageNameW(process, 0, image, ref length)) {
26304
+ string full = image.ToString(0, length);
26305
+ name = Path.GetFileName(full);
26306
+ try { description = FileVersionInfo.GetVersionInfo(full).FileDescription; } catch { }
26307
+ }
26308
+ } finally { CloseHandle(process); }
26309
+ }
26310
+ if (name == null) {
26311
+ try { name = pid == 4 ? "System" : Process.GetProcessById(pid).ProcessName + ".exe"; } catch { continue; }
26312
+ }
26313
+ json.Append(named++ == 0 ? "" : ",").Append("{\"pid\":").Append(pid).Append(",\"name\":").Append(Json(name))
26314
+ .Append(",\"description\":").Append(Json(description)).Append('}');
26315
+ }
26316
+ return json.Append("]}").ToString();
26317
+ }
26318
+ }
26319
+ `;
26320
+ function longPath(folder) {
26321
+ const absolute = path68.win32.resolve(folder);
26322
+ if (absolute.startsWith("\\\\?\\")) return absolute;
26323
+ return absolute.startsWith("\\\\") ? `\\\\?\\UNC\\${absolute.slice(2)}` : `\\\\?\\${absolute}`;
26324
+ }
26325
+ function holdersScript(folder) {
26326
+ const encoded = Buffer.from(longPath(folder), "utf8").toString("base64");
26327
+ return [
26328
+ "$ErrorActionPreference = 'Stop'",
26329
+ `$source = @'
26330
+ ${CSHARP}
26331
+ '@`,
26332
+ "Add-Type -TypeDefinition $source",
26333
+ `$root = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encoded}'))`,
26334
+ `[Console]::Out.WriteLine([AwFolderHolders]::Scan($root, ${SCAN_MAX_ENTRIES}, ${SCAN_MAX_MS}))`,
26335
+ // Con `-Command -`, la última línea corre recién con la línea en blanco de después.
26336
+ "",
26337
+ ""
26338
+ ].join("\n");
26339
+ }
26340
+ function parseHoldersOutput(output, selfPid) {
26341
+ const line = output.split(/\r?\n/).map((candidate) => candidate.trim()).find((candidate) => candidate.startsWith('{"complete":'));
26342
+ const record = line === void 0 ? null : asRecord(parseJson(line));
26343
+ if (record === null || typeof record["complete"] !== "boolean" || !Array.isArray(record["holders"])) return null;
26344
+ const holders = [];
26345
+ let self = false;
26346
+ for (const item of record["holders"]) {
26347
+ const holder = parseWorktreeHolder(item);
26348
+ if (holder === null || holders.some((other) => other.pid === holder.pid)) continue;
26349
+ if (holder.pid === selfPid) self = true;
26350
+ else if (holders.length < WORKTREE_HOLDERS_MAX) holders.push(holder);
26351
+ }
26352
+ return { holders, complete: record["complete"], self };
26353
+ }
26354
+ function powershellPath() {
26355
+ const systemRoot = process.env["SystemRoot"] ?? process.env["windir"] ?? "C:\\Windows";
26356
+ return path68.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
26357
+ }
26358
+ function runPowerShell(script) {
26359
+ return new Promise((resolve) => {
26360
+ const child = spawn4(powershellPath(), ["-NoProfile", "-NonInteractive", "-Command", "-"], {
26361
+ stdio: ["pipe", "pipe", "ignore"],
26362
+ windowsHide: true
26363
+ });
26364
+ let output = "";
26365
+ let settled = false;
26366
+ const finish = (value) => {
26367
+ if (settled) return;
26368
+ settled = true;
26369
+ clearTimeout(timer);
26370
+ resolve(value);
26371
+ };
26372
+ const timer = setTimeout(() => {
26373
+ child.kill();
26374
+ finish(null);
26375
+ }, HOLDERS_TIMEOUT_MS);
26376
+ timer.unref();
26377
+ child.stdout.setEncoding("utf8");
26378
+ child.stdout.on("data", (chunk) => {
26379
+ output += chunk;
26380
+ if (output.length <= OUTPUT_MAX_CHARS) return;
26381
+ child.kill();
26382
+ finish(null);
26383
+ });
26384
+ child.on("error", () => finish(null));
26385
+ child.on("close", () => finish(output));
26386
+ child.stdin.on("error", () => void 0);
26387
+ child.stdin.end(script);
26388
+ });
26389
+ }
26390
+ async function folderHolders(folder) {
26391
+ if (process.platform !== "win32") return [];
26392
+ const output = await runPowerShell(holdersScript(folder));
26393
+ const parsed = output === null ? null : parseHoldersOutput(output, process.pid);
26394
+ if (parsed === null) {
26395
+ console.warn(`[worktrees] couldn't tell which programs have ${folder} open`);
26396
+ return [];
26397
+ }
26398
+ if (parsed.self) console.warn(`[worktrees] this server has ${folder} open`);
26399
+ debugLog("worktrees", `holders of ${folder}: ${parsed.holders.length}, complete ${parsed.complete}`);
26400
+ return parsed.holders;
26401
+ }
26402
+
25345
26403
  // packages/server/src/worktrees/include.ts
25346
26404
  import { createHash as createHash6 } from "node:crypto";
25347
- import { lstat as lstat3, mkdir as mkdir19, readdir as readdir19, readFile as readFile28, writeFile as writeFile11 } from "node:fs/promises";
25348
- import path66 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";
25349
26407
 
25350
26408
  // packages/server/src/worktrees/merge-notes.ts
25351
26409
  var MERGE_MAX_EDITS = 2e3;
@@ -25521,7 +26579,7 @@ var CLI_CONFIG_DIRS = /* @__PURE__ */ new Set([".claude", ".codex", ".gemini", "
25521
26579
  var CLI_CONFIG_FILES = /* @__PURE__ */ new Set([".mcp.json", "opencode.json", "opencode.jsonc"]);
25522
26580
  var INCLUDE_FILE = ".worktreeinclude";
25523
26581
  var INCLUDE_FILE_MAX_BYTES = 64 * 1024;
25524
- var toPosix2 = (value) => value.split(path66.sep).join("/");
26582
+ var toPosix2 = (value) => value.split(path69.sep).join("/");
25525
26583
  function hashOf(content) {
25526
26584
  return createHash6("sha256").update(content).digest("hex");
25527
26585
  }
@@ -25572,7 +26630,7 @@ async function walkEntry(mainRoot, relative, out, budget) {
25572
26630
  if (!info.isDirectory()) return;
25573
26631
  let names;
25574
26632
  try {
25575
- names = (await readdir19(absolute)).sort();
26633
+ names = (await readdir21(absolute)).sort();
25576
26634
  } catch {
25577
26635
  return;
25578
26636
  }
@@ -25626,10 +26684,10 @@ async function copyToWorktree(mainRoot, folder, files, bases) {
25626
26684
  const source = await resolveInside(mainRoot, file.path, { mustExist: true });
25627
26685
  const info = await lstat3(source);
25628
26686
  if (!info.isFile()) continue;
25629
- content = await readFile28(source);
26687
+ content = await readFile29(source);
25630
26688
  const target = await resolveInside(folder, file.path);
25631
- await mkdir19(path66.dirname(target), { recursive: true });
25632
- await writeFile11(target, content, { flag: "wx" });
26689
+ await mkdir21(path69.dirname(target), { recursive: true });
26690
+ await writeFile12(target, content, { flag: "wx" });
25633
26691
  copied.push({ path: file.path, hash: hashOf(content) });
25634
26692
  } catch (error) {
25635
26693
  console.warn(`[worktrees] ${file.path} wasn't copied to the branch folder:`, error);
@@ -25646,7 +26704,7 @@ async function readInside(root, relative) {
25646
26704
  const absolute = await resolveInside(root, relative, { mustExist: true });
25647
26705
  const info = await lstat3(absolute);
25648
26706
  if (!info.isFile()) return null;
25649
- return await readFile28(absolute);
26707
+ return await readFile29(absolute);
25650
26708
  } catch {
25651
26709
  return null;
25652
26710
  }
@@ -25715,7 +26773,7 @@ async function filesUnder(folder, relative, out) {
25715
26773
  return;
25716
26774
  }
25717
26775
  if (!info.isDirectory()) return;
25718
- for (const name of (await readdir19(absolute).catch(() => [])).sort()) {
26776
+ for (const name of (await readdir21(absolute).catch(() => [])).sort()) {
25719
26777
  await filesUnder(folder, `${relative}/${name}`, out);
25720
26778
  }
25721
26779
  }
@@ -25768,7 +26826,7 @@ async function returnNotes(mainRoot, folder, files, bases, options = {}) {
25768
26826
  result.skipped.push(file);
25769
26827
  continue;
25770
26828
  }
25771
- await mkdir19(path66.dirname(target), { recursive: true });
26829
+ await mkdir21(path69.dirname(target), { recursive: true });
25772
26830
  await writeAtomically(target, outcome.content, {
25773
26831
  beforeRename: async () => {
25774
26832
  const now = await readInside(mainRoot, file);
@@ -25790,15 +26848,15 @@ async function returnNotes(mainRoot, folder, files, bases, options = {}) {
25790
26848
  }
25791
26849
 
25792
26850
  // packages/server/src/worktrees/names.ts
25793
- import path67 from "node:path";
25794
- function worktreeFolderFor(mainRoot, branch, pathApi = path67) {
26851
+ import path70 from "node:path";
26852
+ function worktreeFolderFor(mainRoot, branch, pathApi = path70) {
25795
26853
  const parent = pathApi.dirname(mainRoot);
25796
26854
  return pathApi.join(parent, worktreesDirName(pathApi.basename(mainRoot)), worktreeFolderName(branch));
25797
26855
  }
25798
- function worktreesParentFor(mainRoot, pathApi = path67) {
26856
+ function worktreesParentFor(mainRoot, pathApi = path70) {
25799
26857
  return pathApi.join(pathApi.dirname(mainRoot), worktreesDirName(pathApi.basename(mainRoot)));
25800
26858
  }
25801
- function mainRootFromShape(folder, pathApi = path67) {
26859
+ function mainRootFromShape(folder, pathApi = path70) {
25802
26860
  const parent = pathApi.dirname(folder);
25803
26861
  const parentName = pathApi.basename(parent);
25804
26862
  if (parentName.length > ".worktrees".length && parentName.toLowerCase().endsWith(".worktrees")) {
@@ -25813,8 +26871,8 @@ function mainRootFromShape(folder, pathApi = path67) {
25813
26871
 
25814
26872
  // packages/server/src/worktrees/note-bases.ts
25815
26873
  import { createHash as createHash7 } from "node:crypto";
25816
- import { mkdir as mkdir20, readdir as readdir20, readFile as readFile29, rm as rm8, stat as stat43, utimes as utimes3 } from "node:fs/promises";
25817
- import path68 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";
25818
26876
  var TEXT_NOTE_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".mdx", ".markdown", ".txt", ".rst", ".adoc"]);
25819
26877
  var NOTE_BASE_MAX_BYTES = 2 * 1024 * 1024;
25820
26878
  var PRUNE_GRACE_MS = 10 * 6e4;
@@ -25823,7 +26881,7 @@ function hashOf2(content) {
25823
26881
  return createHash7("sha256").update(content).digest("hex");
25824
26882
  }
25825
26883
  function keepsNoteBase(relative, content) {
25826
- return TEXT_NOTE_EXTENSIONS.has(path68.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);
25827
26885
  }
25828
26886
  var NoteBases = class {
25829
26887
  constructor(dir = worktreeNotesDir(), options = {}) {
@@ -25843,21 +26901,21 @@ var NoteBases = class {
25843
26901
  async save(relative, content) {
25844
26902
  if (!keepsNoteBase(relative, content)) return;
25845
26903
  const hash = hashOf2(content);
25846
- const file = path68.join(this.dir, hash);
25847
- const current = await readFile29(file).catch(() => null);
26904
+ const file = path71.join(this.dir, hash);
26905
+ const current = await readFile30(file).catch(() => null);
25848
26906
  if (current !== null && hashOf2(current) === hash) {
25849
26907
  const now = new Date(this.now());
25850
26908
  await utimes3(file, now, now).catch(() => void 0);
25851
26909
  return;
25852
26910
  }
25853
- await mkdir20(this.dir, { recursive: true });
26911
+ await mkdir22(this.dir, { recursive: true });
25854
26912
  await writeAtomically(file, content);
25855
26913
  }
25856
26914
  /** La copia de origen con esa huella, o null si no hay (o lo que hay no es eso). */
25857
26915
  async load(hash) {
25858
26916
  if (!HASH.test(hash)) return null;
25859
26917
  try {
25860
- const content = await readFile29(path68.join(this.dir, hash));
26918
+ const content = await readFile30(path71.join(this.dir, hash));
25861
26919
  return hashOf2(content) === hash ? content : null;
25862
26920
  } catch {
25863
26921
  return null;
@@ -25867,18 +26925,18 @@ var NoteBases = class {
25867
26925
  async prune(keep) {
25868
26926
  let names;
25869
26927
  try {
25870
- names = await readdir20(this.dir);
26928
+ names = await readdir22(this.dir);
25871
26929
  } catch {
25872
26930
  return;
25873
26931
  }
25874
26932
  const now = this.now();
25875
26933
  for (const name of names) {
25876
26934
  if (!HASH.test(name) || keep.has(name)) continue;
25877
- const file = path68.join(this.dir, name);
26935
+ const file = path71.join(this.dir, name);
25878
26936
  try {
25879
- const info = await stat43(file);
26937
+ const info = await stat45(file);
25880
26938
  if (!info.isFile() || now - info.mtimeMs < this.graceMs) continue;
25881
- await rm8(file, { force: true });
26939
+ await rm10(file, { force: true });
25882
26940
  } catch {
25883
26941
  }
25884
26942
  }
@@ -25929,7 +26987,7 @@ function catchUpPrompt(base, conflicts, uncommitted) {
25929
26987
  }
25930
26988
  function commitPrompt(files, count) {
25931
26989
  const more = count > files.length ? ` and ${count - files.length} more` : "";
25932
- return `This branch folder has uncommitted changes: ${files.join(", ")}${more}. Commit them with a clear message, or tell me first if any of them should be discarded instead. Don't push.`;
26990
+ return `This branch folder has uncommitted changes: ${files.join(", ")}${more}. Commit them with a clear message. If any of them shouldn't be committed, leave it as it is and tell me which and why. Don't push.`;
25933
26991
  }
25934
26992
  function lastReplyText(events) {
25935
26993
  const textOf3 = (event2) => event2.parts.flatMap((part) => part.kind === "text" ? [part.text] : []).join("\n").trim();
@@ -25963,9 +27021,9 @@ function readVerdict(reply) {
25963
27021
  }
25964
27022
 
25965
27023
  // packages/server/src/worktrees/store.ts
25966
- import { mkdir as mkdir21, readFile as readFile30 } from "node:fs/promises";
25967
- import path69 from "node:path";
25968
- 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;
25969
27027
  var MAX_REMOVED = 300;
25970
27028
  var MAX_REVIEW_SESSIONS = 20;
25971
27029
  function parseNoteRequest(value) {
@@ -26045,7 +27103,7 @@ function parseWorktreeRecord(value) {
26045
27103
  function parseWorktreesState(raw) {
26046
27104
  try {
26047
27105
  const parsed = asRecord(JSON.parse(raw));
26048
- if (parsed === null || parsed["version"] !== STATE_VERSION5) return [];
27106
+ if (parsed === null || parsed["version"] !== STATE_VERSION6) return [];
26049
27107
  return asArrayFiltered(parsed["worktrees"], parseWorktreeRecord) ?? [];
26050
27108
  } catch {
26051
27109
  return [];
@@ -26060,7 +27118,7 @@ var WorktreeStore = class {
26060
27118
  writing = Promise.resolve();
26061
27119
  async load() {
26062
27120
  try {
26063
- this.records = parseWorktreesState(await readFile30(this.file, "utf8"));
27121
+ this.records = parseWorktreesState(await readFile31(this.file, "utf8"));
26064
27122
  } catch {
26065
27123
  this.records = [];
26066
27124
  }
@@ -26100,18 +27158,108 @@ var WorktreeStore = class {
26100
27158
  );
26101
27159
  this.records = this.records.filter((record) => !forget.has(record.id));
26102
27160
  }
26103
- 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");
26104
27162
  this.writing = this.writing.catch(() => void 0).then(async () => {
26105
- await mkdir21(path69.dirname(this.file), { recursive: true });
27163
+ await mkdir23(path72.dirname(this.file), { recursive: true });
26106
27164
  await writeAtomically(this.file, content);
26107
27165
  });
26108
27166
  return this.writing;
26109
27167
  }
26110
27168
  };
26111
27169
 
27170
+ // packages/server/src/worktrees/untracked.ts
27171
+ import { createHash as createHash8 } from "node:crypto";
27172
+ import { lstat as lstat4, readdir as readdir23, realpath as realpath7, rm as rm11 } from "node:fs/promises";
27173
+ import path73 from "node:path";
27174
+ var sameName = (a, b) => a.toLowerCase() === b.toLowerCase();
27175
+ function cleanEntry(entry) {
27176
+ const clean = entry.replace(/\/+$/, "");
27177
+ if (clean.length === 0 || clean.includes("\\")) return null;
27178
+ const parts = clean.split("/");
27179
+ return parts.some((part) => part.length === 0 || part === "." || part === ".." || sameName(part, ".git")) ? null : clean;
27180
+ }
27181
+ async function scanUntracked(folder, entries, maxFiles = WORKTREE_UNTRACKED_MAX_FILES) {
27182
+ const walk2 = { hash: createHash8("sha256"), files: 0, bytes: 0, max: maxFiles, blocked: null };
27183
+ const targets = [];
27184
+ for (const entry of [...entries].sort()) {
27185
+ const clean = cleanEntry(entry);
27186
+ if (clean === null) return null;
27187
+ let target;
27188
+ let real;
27189
+ try {
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));
27193
+ } catch {
27194
+ return null;
27195
+ }
27196
+ targets.push(target);
27197
+ walk2.hash.update(`${clean}\0`);
27198
+ await visit(target, real, clean, walk2);
27199
+ if (walk2.blocked !== null) return { files: walk2.files, bytes: walk2.bytes, stamp: null, blocked: walk2.blocked, targets: [] };
27200
+ }
27201
+ return { files: walk2.files, bytes: walk2.bytes, stamp: walk2.hash.digest("hex"), blocked: null, targets };
27202
+ }
27203
+ async function visit(absolute, real, relative, walk2) {
27204
+ if (walk2.files >= walk2.max) {
27205
+ walk2.blocked = "too-many";
27206
+ return;
27207
+ }
27208
+ let info;
27209
+ try {
27210
+ info = await lstat4(absolute);
27211
+ } catch {
27212
+ walk2.hash.update(`${relative}\0?
27213
+ `);
27214
+ return;
27215
+ }
27216
+ const linked = !info.isDirectory() || !sameName(await realpath7(absolute).catch(() => ""), real);
27217
+ if (linked) {
27218
+ walk2.files += 1;
27219
+ walk2.bytes += info.size;
27220
+ walk2.hash.update(`${relative}\0${info.isSymbolicLink() || info.isDirectory() ? "l" : "f"}\0${info.size}\0${info.mtimeMs}
27221
+ `);
27222
+ return;
27223
+ }
27224
+ walk2.hash.update(`${relative}\0d
27225
+ `);
27226
+ let names;
27227
+ try {
27228
+ names = (await readdir23(absolute)).sort();
27229
+ } catch {
27230
+ walk2.hash.update(`${relative}\0!
27231
+ `);
27232
+ return;
27233
+ }
27234
+ if (names.some((name) => sameName(name, ".git"))) {
27235
+ walk2.blocked = "nested-repo";
27236
+ return;
27237
+ }
27238
+ for (const name of names) {
27239
+ await visit(path73.join(absolute, name), path73.join(real, name), `${relative}/${name}`, walk2);
27240
+ if (walk2.blocked !== null) return;
27241
+ }
27242
+ }
27243
+ async function deleteUntracked(targets) {
27244
+ for (const target of targets) {
27245
+ try {
27246
+ await rm11(target, { recursive: true, maxRetries: 3 });
27247
+ } catch (error) {
27248
+ if (error.code !== "ENOENT") throw error;
27249
+ }
27250
+ }
27251
+ }
27252
+
26112
27253
  // packages/server/src/worktrees/service.ts
26113
27254
  var WorktreeError = class extends ServerTextError {
26114
27255
  };
27256
+ var WorktreeInUseError = class extends WorktreeError {
27257
+ constructor(plan) {
27258
+ super(serverText("worktreeInUse"));
27259
+ this.plan = plan;
27260
+ }
27261
+ plan;
27262
+ };
26115
27263
  var ROWS_TTL_MS = 5e3;
26116
27264
  var RELEASE_TIMEOUT_MS = 5e3;
26117
27265
  var RELEASE_POLL_MS = 100;
@@ -26120,17 +27268,20 @@ var REPLY_LIMITS = { textMaxChars: 2e5, toolInputMaxChars: 200, toolResultMaxCha
26120
27268
  var REPLY_EVENTS = 300;
26121
27269
  var REPLY_CACHE_MAX = 50;
26122
27270
  var REVIEW_ACTIVITY_DELAY_MS = 1e3;
26123
- async function exists4(candidate) {
27271
+ async function leftWithoutGit(folder) {
27272
+ return await exists5(folder) && await lstat5(path74.join(folder, ".git")).catch(() => null) === null;
27273
+ }
27274
+ async function exists5(candidate) {
26124
27275
  try {
26125
- await stat44(candidate);
27276
+ await stat46(candidate);
26126
27277
  return true;
26127
27278
  } catch {
26128
27279
  return false;
26129
27280
  }
26130
27281
  }
26131
27282
  function isInside4(outer, inner) {
26132
- const relative = path70.relative(path70.resolve(outer), path70.resolve(inner));
26133
- return relative.length === 0 || !relative.startsWith("..") && !path70.isAbsolute(relative);
27283
+ const relative = path74.relative(path74.resolve(outer), path74.resolve(inner));
27284
+ return relative.length === 0 || !relative.startsWith("..") && !path74.isAbsolute(relative);
26134
27285
  }
26135
27286
  function displayCommand(args) {
26136
27287
  return ["git", ...args].map((arg) => /[\s"]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg).join(" ");
@@ -26186,7 +27337,7 @@ var WorktreeService = class extends EventEmitter7 {
26186
27337
  /** La carpeta principal del repo de un proyecto o de una pestaña. */
26187
27338
  async mainRootOf(source) {
26188
27339
  const cwd = source.kind === "terminal" ? this.deps.registry.get(source.terminalId)?.cwd ?? null : this.deps.projectCwd(source.projectKey);
26189
- if (cwd === null || !await exists4(cwd)) throw new WorktreeError(serverText("worktreeProjectUnknown"));
27340
+ if (cwd === null || !await exists5(cwd)) throw new WorktreeError(serverText("worktreeProjectUnknown"));
26190
27341
  const layout = await repoLayout(cwd);
26191
27342
  if (layout === null || layout.mainRoot === null) throw new WorktreeError(serverText("worktreeNotRepo"));
26192
27343
  return layout.mainRoot;
@@ -26205,7 +27356,7 @@ var WorktreeService = class extends EventEmitter7 {
26205
27356
  return {
26206
27357
  mainRoot,
26207
27358
  parentDir: worktreesParentFor(mainRoot),
26208
- separator: path70.sep,
27359
+ separator: path74.sep,
26209
27360
  currentBranch: current,
26210
27361
  branches: branches.map((name) => ({ name, checkedOut: checkedOut.has(name) })),
26211
27362
  mainChanges: changes?.length ?? 0,
@@ -26227,9 +27378,9 @@ var WorktreeService = class extends EventEmitter7 {
26227
27378
  throw new WorktreeError(serverText("worktreeBranchInUse", { branch }));
26228
27379
  }
26229
27380
  const folder = worktreeFolderFor(mainRoot, branch);
26230
- if (await exists4(folder)) throw new WorktreeError(serverText("worktreeFolderExists", { folder }));
27381
+ if (await exists5(folder)) throw new WorktreeError(serverText("worktreeFolderExists", { folder }));
26231
27382
  const copy = await planCopy(mainRoot);
26232
- await mkdir22(path70.dirname(folder), { recursive: true });
27383
+ await mkdir24(path74.dirname(folder), { recursive: true });
26233
27384
  const added = await addWorktree(mainRoot, folder, branch, mode === "new" ? base : null);
26234
27385
  if (added.code !== 0) throw new WorktreeError(serverText("worktreeGitFailed", { detail: gitFailureText(added) }));
26235
27386
  const { copied, copiedDirs } = await this.copyInto(mainRoot, folder, copy, request.copyNotes);
@@ -26274,13 +27425,13 @@ var WorktreeService = class extends EventEmitter7 {
26274
27425
  /** Las filas de "Ramas aparte" para la pestaña que pregunta. Vacío fuera de git. */
26275
27426
  async rows(terminalId) {
26276
27427
  const descriptor = this.deps.registry.get(terminalId);
26277
- if (descriptor === null || !await exists4(descriptor.cwd)) return [];
27428
+ if (descriptor === null || !await exists5(descriptor.cwd)) return [];
26278
27429
  const layout = await repoLayout(descriptor.cwd);
26279
27430
  if (layout === null || layout.mainRoot === null) return [];
26280
27431
  const mainRoot = layout.mainRoot;
26281
27432
  const tips = await branchTips(mainRoot);
26282
27433
  const tipsStamp = [...tips].map(([branch, commit]) => `${branch}\0${commit}`).join("\n");
26283
- const cacheKey2 = path70.resolve(mainRoot).toLowerCase();
27434
+ const cacheKey2 = path74.resolve(mainRoot).toLowerCase();
26284
27435
  const cached = this.rowsCache.get(cacheKey2);
26285
27436
  const fresh = cached !== void 0 && cached.tips === tipsStamp && this.now() - cached.at < ROWS_TTL_MS;
26286
27437
  const generation = this.rowsGeneration;
@@ -26289,7 +27440,12 @@ var WorktreeService = class extends EventEmitter7 {
26289
27440
  return rows.map((row) => ({ ...row, isCurrent: samePathShape(row.path, layout.toplevel) }));
26290
27441
  }
26291
27442
  async readRows(mainRoot, tips) {
26292
- const entries = (await listWorktrees(mainRoot)).filter((entry) => !entry.bare);
27443
+ const listed = await readWorktreeList(mainRoot);
27444
+ const entries = [
27445
+ ...(listed ?? []).filter((entry) => !entry.bare),
27446
+ // Las que git soltó a medias: sin su fila, no había cómo terminar de quitarlas.
27447
+ ...listed === null ? [] : await this.releasedEntries(mainRoot, listed)
27448
+ ];
26293
27449
  const mainBranch = entries.find((entry) => entry.isMain)?.branch ?? null;
26294
27450
  const read2 = await Promise.all(
26295
27451
  entries.map(async (entry) => {
@@ -26336,7 +27492,7 @@ var WorktreeService = class extends EventEmitter7 {
26336
27492
  const baseTip = tips.get(base);
26337
27493
  const branchTip = tips.get(branch);
26338
27494
  if (baseTip === void 0 || branchTip === void 0) return Promise.resolve(null);
26339
- const key = [path70.resolve(mainRoot).toLowerCase(), base, branch, baseTip, branchTip].join("\0");
27495
+ const key = [path74.resolve(mainRoot).toLowerCase(), base, branch, baseTip, branchTip].join("\0");
26340
27496
  const cached = this.pairs.get(key);
26341
27497
  if (cached !== void 0) {
26342
27498
  this.pairs.delete(key);
@@ -26381,12 +27537,12 @@ var WorktreeService = class extends EventEmitter7 {
26381
27537
  /** Una rama aparte que existe en disco, para lo que la lee o le deja un pedido. */
26382
27538
  async presentRecordOrThrow(id) {
26383
27539
  const record = this.recordOrThrow(id);
26384
- if (!await exists4(record.path)) throw new WorktreeError(serverText("worktreeUnknown"));
27540
+ if (!await exists5(record.path)) throw new WorktreeError(serverText("worktreeUnknown"));
26385
27541
  return record;
26386
27542
  }
26387
27543
  /** Las pestañas y consolas abiertas dentro de la carpeta de la rama. */
26388
27544
  tabsIn(folder) {
26389
- return this.deps.registry.list().filter((tab) => isInside4(folder, tab.cwd)).map((tab) => ({ terminalId: tab.terminalId, kind: tab.kind, label: tab.label || path70.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) }));
26390
27546
  }
26391
27547
  /**
26392
27548
  * Las pestañas de una carpeta de trabajo, sin las de otra que viva adentro
@@ -26399,17 +27555,70 @@ var WorktreeService = class extends EventEmitter7 {
26399
27555
  return !nested.some((inner) => isInside4(inner, cwd));
26400
27556
  });
26401
27557
  }
27558
+ /**
27559
+ * Los archivos nuevos que se pueden perder al quitar la rama (23-10-2026):
27560
+ * sólo si todo lo que no está commiteado es nuevo y git lo listó entero. null
27561
+ * si no; si git se va a negar a quitarla igual, bloqueada o con un submódulo
27562
+ * cargado (no se borra nada antes de saberlo); o si una entrada no se puede
27563
+ * borrar sin salir de la carpeta. Con algo adentro que la app copió de la
27564
+ * principal y la rama no ignora, no se ofrece (`copied`): no vuelve como una
27565
+ * nota, y se perdería lo que la rama le cambió.
27566
+ */
27567
+ async untrackedIn(record, changes) {
27568
+ const onlyNew = changes.complete && changes.paths.length > 0 && changes.untracked.length === changes.paths.length;
27569
+ if (!onlyNew) return null;
27570
+ const [entries, submodule] = await Promise.all([listWorktrees(record.mainRoot), hasLoadedSubmodule(record.path)]);
27571
+ if (submodule !== false || entries.some((entry) => entry.locked && samePathShape(entry.path, record.path))) return null;
27572
+ const entryOf = (file) => changes.untracked.some((entry) => {
27573
+ const clean = entry.replace(/\/+$/, "").toLowerCase();
27574
+ return file.toLowerCase() === clean || file.toLowerCase().startsWith(`${clean}/`);
27575
+ });
27576
+ const copied = [];
27577
+ for (const file of record.copied) {
27578
+ if (entryOf(file.path) && await exists5(path74.join(record.path, file.path))) copied.push(file.path);
27579
+ }
27580
+ if (copied.length > 0) {
27581
+ const ignored = await ignoredAmong(record.path, copied);
27582
+ if (ignored === null || copied.some((file) => !ignored.has(file))) return { files: 0, bytes: 0, stamp: null, blocked: "copied", targets: [] };
27583
+ }
27584
+ return scanUntracked(record.path, changes.untracked);
27585
+ }
27586
+ /**
27587
+ * Borra los archivos nuevos que el usuario eligió perder, si siguen siendo
27588
+ * los que vio: se mira justo antes, con las pestañas de la rama cerradas y
27589
+ * las notas ya de vuelta. Si desaparecieron solos, no hay nada que borrar.
27590
+ * Devuelve cuántos archivos se perdieron.
27591
+ */
27592
+ async discardNew(record, stamp2) {
27593
+ const changes = await uncommittedChanges(record.path);
27594
+ if (changes === null) throw new WorktreeError(serverText("worktreeGitFailed", { detail: "git status" }));
27595
+ if (changes.paths.length === 0) return 0;
27596
+ const scan = await this.untrackedIn(record, changes);
27597
+ if (scan === null || scan.stamp === null || scan.stamp !== stamp2) throw new WorktreeError(serverText("worktreeUntrackedChanged"));
27598
+ try {
27599
+ await deleteUntracked(scan.targets);
27600
+ } catch (error) {
27601
+ throw new WorktreeError(serverText("worktreeDiscardFailed", { detail: error instanceof Error ? error.message : String(error) }));
27602
+ }
27603
+ return scan.files;
27604
+ }
26402
27605
  async planFor(record) {
26403
- const present = await exists4(record.path);
26404
- const [uncommitted, notes, integrated] = await Promise.all([
26405
- present ? uncommittedPaths(record.path) : Promise.resolve([]),
27606
+ const onDisk = await exists5(record.path);
27607
+ const present = onDisk && !await this.releasedByGit(record);
27608
+ const [changes, notes, integrated] = await Promise.all([
27609
+ present ? uncommittedChanges(record.path) : Promise.resolve({ paths: [], untracked: [], complete: true }),
26406
27610
  present ? reviewNotes(record.mainRoot, record.path, record, this.loadNoteBase) : Promise.resolve({ returning: [], merging: [], resolved: [], conflicts: [] }),
26407
27611
  record.createdBranch ? isIntegrated(record.mainRoot, record.branch, record.base) : Promise.resolve(false)
26408
27612
  ]);
26409
- if (uncommitted === null) throw new WorktreeError(serverText("worktreeGitFailed", { detail: "git status" }));
27613
+ if (changes === null) throw new WorktreeError(serverText("worktreeGitFailed", { detail: "git status" }));
27614
+ const uncommitted = changes.paths;
27615
+ const untracked = await this.untrackedIn(record, changes);
26410
27616
  const tabs = this.tabsIn(record.path);
26411
27617
  const deletesBranch = integrated === true;
26412
27618
  const branchUnused = deletesBranch && stillAtStart(record, await branchCommit(record.mainRoot, record.branch) ?? void 0);
27619
+ let inUse = !onDisk ? false : tabs.length > 0 ? null : await folderInUse(record.path);
27620
+ const holders = inUse === true ? await this.holdersOf(record.path) : [];
27621
+ if (inUse === true && holders.length === 0) inUse = await folderInUse(record.path);
26413
27622
  return {
26414
27623
  id: record.id,
26415
27624
  branch: record.branch,
@@ -26420,13 +27629,15 @@ var WorktreeService = class extends EventEmitter7 {
26420
27629
  ],
26421
27630
  uncommitted: uncommitted.slice(0, WORKTREE_PLAN_PATHS_MAX),
26422
27631
  uncommittedCount: uncommitted.length,
27632
+ // Sin las rutas absolutas de `targets`: al cliente le basta con lo que muestra y la huella.
27633
+ untracked: untracked === null ? null : { files: untracked.files, bytes: untracked.bytes, stamp: untracked.stamp, blocked: untracked.blocked },
26423
27634
  tabs,
26424
27635
  returning: notes.returning,
26425
27636
  merging: notes.merging,
26426
27637
  resolved: notes.resolved,
26427
27638
  conflicts: notes.conflicts,
26428
- // Con pestañas de la app adentro, la prueba daría "en uso" por ellas mismas: se mira al confirmar.
26429
- inUse: !present ? false : tabs.length > 0 ? null : await folderInUse(record.path),
27639
+ inUse,
27640
+ holders,
26430
27641
  deletesBranch,
26431
27642
  branchUnused,
26432
27643
  createdBranch: record.createdBranch
@@ -26439,34 +27650,46 @@ var WorktreeService = class extends EventEmitter7 {
26439
27650
  /**
26440
27651
  * Quita la rama aparte. Rehace el plan y se detiene en el primer "no" (ver
26441
27652
  * el principio del archivo). `closeTab` cierra una pestaña como lo hace el
26442
- * socket, soltando todo lo que la seguía.
27653
+ * socket, soltando todo lo que la seguía. `discardUntracked` es la huella de
27654
+ * los archivos nuevos que el usuario eligió perder (23-10-2026).
26443
27655
  */
26444
- async remove(id, discardConflicts, closeTab) {
27656
+ async remove(id, discardConflicts, closeTab, discardUntracked = null) {
26445
27657
  const record = this.recordOrThrow(id);
26446
27658
  const plan = await this.planFor(record);
26447
27659
  if (plan.uncommittedCount > 0) {
26448
- throw new WorktreeError(serverText("worktreeUncommitted", { count: plan.uncommittedCount }));
27660
+ if (plan.untracked === null || plan.untracked.stamp === null || discardUntracked === null) {
27661
+ throw new WorktreeError(serverText("worktreeUncommitted", { count: plan.uncommittedCount }));
27662
+ }
27663
+ if (plan.untracked.stamp !== discardUntracked) throw new WorktreeError(serverText("worktreeUntrackedChanged"));
26449
27664
  }
26450
27665
  if (plan.conflicts.length > 0 && !discardConflicts) {
26451
27666
  throw new WorktreeError(serverText("worktreeNotesConflict", { count: plan.conflicts.length }));
26452
27667
  }
26453
- if (plan.inUse === true) throw new WorktreeError(serverText("worktreeInUse"));
27668
+ if (plan.inUse === true) throw new WorktreeInUseError(plan);
26454
27669
  let returned = 0;
26455
- if (await exists4(record.path)) {
27670
+ let discardedFiles = 0;
27671
+ if (await exists5(record.path)) {
26456
27672
  for (const tab of plan.tabs) closeTab(tab.terminalId);
26457
- if (!await this.waitUntilFree(record.path)) throw new WorktreeError(serverText("worktreeInUse"));
26458
- const bases = new Map(record.copied.map((file) => [file.path, file.hash]));
26459
- try {
26460
- returned = (await returnNotes(record.mainRoot, record.path, plan.returning, bases, {
26461
- loadBase: this.loadNoteBase,
26462
- onWritten: (file, branch) => this.noteReturned(record.id, file, branch),
26463
- notesAsked: record.notesAsked
26464
- })).returned.length;
26465
- } catch (error) {
26466
- throw new WorktreeError(serverText("worktreeNotesFailed", { detail: error instanceof Error ? error.message : String(error) }));
27673
+ if (!await this.waitUntilFree(record.path)) {
27674
+ throw new WorktreeInUseError({ ...plan, tabs: [], inUse: true, holders: await this.holdersOf(record.path) });
27675
+ }
27676
+ if (!await this.releasedByGit(record)) {
27677
+ const bases = new Map(record.copied.map((file) => [file.path, file.hash]));
27678
+ try {
27679
+ returned = (await returnNotes(record.mainRoot, record.path, plan.returning, bases, {
27680
+ loadBase: this.loadNoteBase,
27681
+ onWritten: (file, branch) => this.noteReturned(record.id, file, branch),
27682
+ notesAsked: record.notesAsked
27683
+ })).returned.length;
27684
+ } catch (error) {
27685
+ throw new WorktreeError(serverText("worktreeNotesFailed", { detail: error instanceof Error ? error.message : String(error) }));
27686
+ }
27687
+ if (plan.uncommittedCount > 0) discardedFiles = await this.discardNew(record, discardUntracked);
27688
+ const removed = await removeWorktree(record.mainRoot, record.path);
27689
+ if (removed.code !== 0 && !await this.releasedByGit(record)) {
27690
+ throw new WorktreeError(serverText("worktreeGitFailed", { detail: gitFailureText(removed) }));
27691
+ }
26467
27692
  }
26468
- const removed = await removeWorktree(record.mainRoot, record.path);
26469
- if (removed.code !== 0) throw new WorktreeError(serverText("worktreeGitFailed", { detail: gitFailureText(removed) }));
26470
27693
  await this.removeLeftovers(record);
26471
27694
  }
26472
27695
  const endCommit = await branchCommit(record.mainRoot, record.branch);
@@ -26474,7 +27697,15 @@ var WorktreeService = class extends EventEmitter7 {
26474
27697
  await this.deps.store.update(id, { removedAt: this.now(), endCommit: endCommit ?? record.endCommit });
26475
27698
  await this.pruneNoteBases();
26476
27699
  this.changed();
26477
- return { branchDeleted, returned, discarded: discardConflicts ? plan.conflicts.length : 0 };
27700
+ return { branchDeleted, returned, discarded: discardConflicts ? plan.conflicts.length : 0, discardedFiles };
27701
+ }
27702
+ /** Los programas que tienen la carpeta, para el diálogo. Sin saberlo, ninguno: el diálogo dice lo de antes. */
27703
+ async holdersOf(folder) {
27704
+ try {
27705
+ return await (this.deps.folderHolders ?? folderHolders)(folder);
27706
+ } catch {
27707
+ return [];
27708
+ }
26478
27709
  }
26479
27710
  /** Espera, con tope, a que ningún proceso tenga la carpeta. */
26480
27711
  async waitUntilFree(folder) {
@@ -26492,16 +27723,40 @@ var WorktreeService = class extends EventEmitter7 {
26492
27723
  * contenga, ni una carpeta que git todavía tenga registrada.
26493
27724
  */
26494
27725
  async removeLeftovers(record) {
26495
- const folder = path70.resolve(record.path);
26496
- if (!await exists4(folder)) return this.removeEmptyParent(folder);
26497
- if (isInside4(folder, record.mainRoot) || path70.dirname(folder) === folder) return;
27726
+ const folder = path74.resolve(record.path);
27727
+ if (!await exists5(folder)) return this.removeEmptyParent(folder);
27728
+ if (isInside4(folder, record.mainRoot) || path74.dirname(folder) === folder) return;
26498
27729
  if ((await listWorktrees(record.mainRoot)).some((entry) => samePathShape(entry.path, folder))) return;
26499
- await rm9(folder, { recursive: true, maxRetries: 3 });
27730
+ await rm12(folder, { recursive: true, maxRetries: 3 });
26500
27731
  await this.removeEmptyParent(folder);
26501
27732
  }
27733
+ /**
27734
+ * true si git ya soltó la rama sin terminar de borrar su carpeta
27735
+ * (23-10-2026). `worktree remove` borra después de sus controles y, si algo
27736
+ * no se deja borrar a mitad de camino, quita su registro igual (medido con git
27737
+ * 2.56: una ruta de más de 260 caracteres sin `core.longpaths`, o un nombre
27738
+ * terminado en punto). Lo que quedó ya pasó esos controles, y lo termina de
27739
+ * borrar la app. Sólo sin `.git` adentro, y si git contesta que no la tiene:
27740
+ * una carpeta con su `.git` puede tener trabajo aunque git ya no la conozca.
27741
+ */
27742
+ async releasedByGit(record) {
27743
+ if (!await leftWithoutGit(record.path)) return false;
27744
+ const entries = await readWorktreeList(record.mainRoot);
27745
+ return entries !== null && !entries.some((entry) => samePathShape(entry.path, record.path));
27746
+ }
27747
+ /** Las ramas de la app que git soltó a medias (`releasedByGit`), como si git las listara. */
27748
+ async releasedEntries(mainRoot, listed) {
27749
+ const released = [];
27750
+ for (const record of this.deps.store.list()) {
27751
+ if (record.removedAt !== null || !samePathShape(record.mainRoot, mainRoot)) continue;
27752
+ if (listed.some((entry) => samePathShape(entry.path, record.path)) || !await leftWithoutGit(record.path)) continue;
27753
+ released.push({ path: path74.resolve(record.path), branch: record.branch, isMain: false, bare: false, locked: false, prunable: true });
27754
+ }
27755
+ return released;
27756
+ }
26502
27757
  /** La carpeta `<proyecto>.worktrees`, sólo si quedó vacía. */
26503
27758
  async removeEmptyParent(folder) {
26504
- await rmdir(path70.dirname(folder)).catch(() => void 0);
27759
+ await rmdir(path74.dirname(folder)).catch(() => void 0);
26505
27760
  }
26506
27761
  /**
26507
27762
  * Vuelve a abrir la rama de una sesión cuya carpeta se quitó: la saca otra
@@ -26513,7 +27768,7 @@ var WorktreeService = class extends EventEmitter7 {
26513
27768
  const session = this.deps.findSession(agent, sessionId);
26514
27769
  if (session === null || session.cwd.length === 0) throw new WorktreeError(serverText("worktreeUnknown"));
26515
27770
  const record = this.removedRecordAbove(session.cwd);
26516
- if (record === null || await exists4(record.path) || !await exists4(record.mainRoot)) {
27771
+ if (record === null || await exists5(record.path) || !await exists5(record.mainRoot)) {
26517
27772
  throw new WorktreeError(serverText("worktreeUnknown"));
26518
27773
  }
26519
27774
  const branchExists = await branchCommit(record.mainRoot, record.branch) !== null;
@@ -26525,7 +27780,7 @@ var WorktreeService = class extends EventEmitter7 {
26525
27780
  throw new WorktreeError(serverText("worktreeBranchInUse", { branch: record.branch }));
26526
27781
  }
26527
27782
  const copy = await planCopy(record.mainRoot);
26528
- await mkdir22(path70.dirname(record.path), { recursive: true });
27783
+ await mkdir24(path74.dirname(record.path), { recursive: true });
26529
27784
  const added = await addWorktree(record.mainRoot, record.path, record.branch, recreateAt);
26530
27785
  if (added.code !== 0) throw new WorktreeError(serverText("worktreeGitFailed", { detail: gitFailureText(added) }));
26531
27786
  const { copied, copiedDirs } = await this.copyInto(record.mainRoot, record.path, copy, record.copyNotes);
@@ -26541,7 +27796,7 @@ var WorktreeService = class extends EventEmitter7 {
26541
27796
  */
26542
27797
  async keptBranches(terminalId) {
26543
27798
  const descriptor = this.deps.registry.get(terminalId);
26544
- if (descriptor === null || !await exists4(descriptor.cwd)) return [];
27799
+ if (descriptor === null || !await exists5(descriptor.cwd)) return [];
26545
27800
  const layout = await repoLayout(descriptor.cwd);
26546
27801
  if (layout === null || layout.mainRoot === null) return [];
26547
27802
  const mainRoot = layout.mainRoot;
@@ -26612,11 +27867,11 @@ var WorktreeService = class extends EventEmitter7 {
26612
27867
  return { branch: plan.branch };
26613
27868
  }
26614
27869
  removedRecordAbove(cwd) {
26615
- let current = path70.resolve(cwd);
27870
+ let current = path74.resolve(cwd);
26616
27871
  for (let depth = 0; depth < 24; depth += 1) {
26617
27872
  const record = this.deps.store.byPath(current);
26618
27873
  if (record !== null) return record.removedAt === null ? null : record;
26619
- const parent = path70.dirname(current);
27874
+ const parent = path74.dirname(current);
26620
27875
  if (parent === current) return null;
26621
27876
  current = parent;
26622
27877
  }
@@ -26725,10 +27980,10 @@ var WorktreeService = class extends EventEmitter7 {
26725
27980
  * reanudada; y si tampoco hay, una nueva en la carpeta de la rama.
26726
27981
  */
26727
27982
  async authorTarget(record) {
26728
- const open9 = this.authorTab(record);
26729
- if (open9 !== null) return { terminal: open9, opened: false };
27983
+ const open10 = this.authorTab(record);
27984
+ if (open10 !== null) return { terminal: open10, opened: false };
26730
27985
  const session = this.authorSession(record);
26731
- if (session !== null && isAgentId(session.agent) && session.cwd.length > 0 && await exists4(session.cwd)) {
27986
+ if (session !== null && isAgentId(session.agent) && session.cwd.length > 0 && await exists5(session.cwd)) {
26732
27987
  const terminal = await this.deps.registry.open({ cwd: session.cwd, agent: session.agent, resumeSessionId: session.sessionId });
26733
27988
  return { terminal, opened: true };
26734
27989
  }
@@ -26945,11 +28200,11 @@ var WorktreeService = class extends EventEmitter7 {
26945
28200
  const baseEntry = entries.find((entry) => !entry.bare && !entry.prunable && entry.branch === record.base) ?? null;
26946
28201
  const [baseChanges, branchChanges, target] = await Promise.all([
26947
28202
  baseEntry === null ? Promise.resolve([]) : uncommittedPaths(baseEntry.path),
26948
- await exists4(record.path) ? uncommittedPaths(record.path) : Promise.resolve([]),
28203
+ await exists5(record.path) ? uncommittedPaths(record.path) : Promise.resolve([]),
26949
28204
  baseEntry === null ? Promise.resolve(`refs/heads/${record.branch}`) : mergeTarget(baseEntry.path, record.branch)
26950
28205
  ]);
26951
28206
  const localConflicts = pathsTouching(baseChanges ?? [], files);
26952
- const notes = baseEntry?.isMain === true && await exists4(record.path) ? await reviewNotes(mainRoot, record.path, record, this.loadNoteBase) : null;
28207
+ const notes = baseEntry?.isMain === true && await exists5(record.path) ? await reviewNotes(mainRoot, record.path, record, this.loadNoteBase) : null;
26953
28208
  const blocker = pair.ahead === 0 ? "nothing" : pair.behind > 0 ? "behind" : baseEntry === null ? "no-base-folder" : localConflicts.length > 0 ? "local-changes" : null;
26954
28209
  const plan = {
26955
28210
  id: record.id,
@@ -27065,6 +28320,549 @@ var WorktreeService = class extends EventEmitter7 {
27065
28320
  }
27066
28321
  };
27067
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
+
27068
28866
  // packages/server/src/terminal-socket.ts
27069
28867
  var NOTE_SEND_TIMEOUT_MS = 15e3;
27070
28868
  var REWIND_EXTRA_STEPS = 30;
@@ -27111,7 +28909,8 @@ function attachTerminalSocket(options) {
27111
28909
  defaultCwd,
27112
28910
  vault,
27113
28911
  remote,
27114
- worktrees
28912
+ worktrees,
28913
+ scheduled
27115
28914
  } = options;
27116
28915
  const pasteStore = options.pasteStore ?? new PasteStore();
27117
28916
  const writeQueue = new TerminalWriteQueue();
@@ -27176,12 +28975,12 @@ function attachTerminalSocket(options) {
27176
28975
  agents: agents.list(),
27177
28976
  defaultAgent: agents.defaultAgent()
27178
28977
  });
27179
- const writeToTerminal = (socket, terminalId, data) => {
28978
+ const writeToTerminal = (socket, terminalId, data, refusedAs) => {
27180
28979
  if (registry.write(terminalId, data)) return true;
27181
28980
  if (registry.get(terminalId) === null) {
27182
- sendError(socket, "unknown-terminal", serverText("terminalGone"));
28981
+ sendError(socket, refusedAs ?? "unknown-terminal", serverText("terminalGone"));
27183
28982
  } else {
27184
- sendError(socket, "terminal-asleep", serverText("terminalAsleep"));
28983
+ sendError(socket, refusedAs ?? "terminal-asleep", serverText("terminalAsleep"));
27185
28984
  }
27186
28985
  return false;
27187
28986
  };
@@ -27194,6 +28993,13 @@ function attachTerminalSocket(options) {
27194
28993
  for (const { from, to } of moved) drafts.move(from, to);
27195
28994
  const entries = draftEntriesOf(appeared, drafts).filter((entry) => !isEmptyComposerDraft(entry.draft));
27196
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();
27197
29003
  };
27198
29004
  const onRegistryExit = (terminalId, exitCode, signal) => broadcast({ type: "terminal.exit", terminalId, exitCode, signal });
27199
29005
  const onRegistryActivity = (terminalId, activity, offlineReason) => {
@@ -27202,6 +29008,7 @@ function attachTerminalSocket(options) {
27202
29008
  offlineReason === null ? { type: "terminal.activity", terminalId, activity } : { type: "terminal.activity", terminalId, activity, offlineReason }
27203
29009
  );
27204
29010
  if (registry.get(terminalId)?.review === true) worktrees.reviewActivity();
29011
+ scheduler.nudge();
27205
29012
  };
27206
29013
  const onIndexStatus = (status) => broadcast({ type: "index.status", status });
27207
29014
  const onIndexProjects = (projects, replace) => broadcast({ type: "index.projects", projects, replace });
@@ -27237,7 +29044,7 @@ function attachTerminalSocket(options) {
27237
29044
  const onConversationState = (terminalId, state) => broadcast({ type: "conversation.state", terminalId, state });
27238
29045
  const onConversationMode = (terminalId, mode) => broadcast({ type: "conversation.mode", terminalId, mode });
27239
29046
  const onConversationWaiting = (terminalId, waitingFor) => broadcast({ type: "conversation.waiting", terminalId, waitingFor });
27240
- const onConversationToolCall = (terminalId, open9) => broadcast({ type: "conversation.toolCall", terminalId, open: open9 });
29047
+ const onConversationToolCall = (terminalId, open10) => broadcast({ type: "conversation.toolCall", terminalId, open: open10 });
27241
29048
  const onConversationSubagents = (terminalId, subagents) => broadcast({ type: "conversation.subagents", terminalId, subagents });
27242
29049
  const onConversationLiveQuestion = (terminalId, question) => broadcast({ type: "conversation.liveQuestion", terminalId, question });
27243
29050
  const onConversationQueue = (terminalId, queue) => broadcast({ type: "conversation.queue", terminalId, queue });
@@ -27251,6 +29058,7 @@ function attachTerminalSocket(options) {
27251
29058
  return await adapter.defaults(descriptor.cwd, sessionModel) ?? NO_DEFAULTS;
27252
29059
  };
27253
29060
  const pieceWriter = (socket, terminalId) => (piece) => writeToTerminal(socket, terminalId, piece);
29061
+ const answerWriter = (socket, terminalId) => (piece) => writeToTerminal(socket, terminalId, piece, "answer-failed");
27254
29062
  const interruptAgent = (socket, terminalId, options2) => {
27255
29063
  writeQueue.interrupt(terminalId);
27256
29064
  const [first, ...rest] = buildInterruptKeys(inputOf(terminalId).interruptPresses);
@@ -27325,6 +29133,12 @@ function attachTerminalSocket(options) {
27325
29133
  const draftOwner = draftOwnerOf(registry.get(terminalId));
27326
29134
  if (!registry.close(terminalId)) return false;
27327
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
+ }
27328
29142
  conversations.drop(terminalId);
27329
29143
  repos.drop(terminalId);
27330
29144
  memory.drop(terminalId);
@@ -27346,6 +29160,111 @@ function attachTerminalSocket(options) {
27346
29160
  noteSubmitted: (terminalId, text) => registry.noteSubmitted(terminalId, text),
27347
29161
  enqueue: (terminalId, job, enqueueOptions) => writeQueue.enqueue(terminalId, job, enqueueOptions)
27348
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);
27349
29268
  const continueDeps = {
27350
29269
  registry,
27351
29270
  closeTab: (terminalId) => {
@@ -27414,7 +29333,11 @@ function attachTerminalSocket(options) {
27414
29333
  const stopRemoteStatus = remote.onChange(
27415
29334
  () => broadcastToHost({ type: "remote.status", status: remote.status() })
27416
29335
  );
27417
- 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
+ };
27418
29341
  appSettings.on("status", onAppSettingsStatus);
27419
29342
  const stopRemoteRevoked = remote.onRevoked((deviceId) => {
27420
29343
  for (const [socket, id] of remoteSockets) {
@@ -27542,6 +29465,7 @@ function attachTerminalSocket(options) {
27542
29465
  send(socket, { type: "index.projects", projects: index.getProjects(), replace: true });
27543
29466
  send(socket, { type: "notes.list", notes: notes.list() });
27544
29467
  send(socket, { type: "composer.drafts", drafts: draftEntriesOf(registry.list(), drafts) });
29468
+ send(socket, { type: "schedule.list", messages: scheduler.snapshot() });
27545
29469
  send(socket, { type: "vault.status", status: vault.status() });
27546
29470
  if (deviceId === null) send(socket, { type: "remote.status", status: remote.status() });
27547
29471
  send(socket, { type: "settings.status", settings: appSettings.status() });
@@ -27623,58 +29547,19 @@ function attachTerminalSocket(options) {
27623
29547
  break;
27624
29548
  }
27625
29549
  const submitting = adapterOf(terminalId);
27626
- const isBlind = () => submitting !== null && conversations.isBlindToApprovals(terminalId, submitting);
27627
- const guardsPending = submitting?.capabilities.permissionCycle?.approvesPendingOnCycle === true;
27628
- const blocksWhileWaiting = submitting?.capabilities.waitingBlocksSubmit === true;
27629
- if (submitting !== null) {
27630
- const launchedAt = registry.launchedAtOf(terminalId);
27631
- const blind = isBlind();
27632
- const refusal2 = submitRefusal({
27633
- label: submitting.label,
27634
- approvesPendingOnCycle: guardsPending,
27635
- waitingFor: conversations.getWaitingFor(terminalId),
27636
- waitingBlocksSubmit: blocksWhileWaiting,
27637
- activity: registry.activityOf(terminalId),
27638
- blind,
27639
- openToolCall: blind && launchedAt !== null && conversations.hasOpenToolCall(terminalId, launchedAt)
27640
- });
27641
- if (refusal2 !== null) {
27642
- sendError(socket, "submit-failed", refusal2);
27643
- settle(false);
27644
- break;
27645
- }
29550
+ const plan = prepareSubmit(submitDeps, terminalId);
29551
+ if ("refusal" in plan) {
29552
+ sendError(socket, "submit-failed", plan.refusal);
29553
+ settle(false);
29554
+ break;
27646
29555
  }
27647
- const approvalGuard = submitting !== null && (guardsPending || blocksWhileWaiting || isBlind()) ? async () => {
27648
- const current = registry.launchedAtOf(terminalId);
27649
- if (current === null) return true;
27650
- if (blocksWhileWaiting && registry.activityOf(terminalId) === "waiting") return false;
27651
- if (isBlind() && await conversations.checkOpenToolCall(terminalId, current)) return false;
27652
- return !guardsPending || await conversations.checkWaitingFor(terminalId) === null;
27653
- } : void 0;
27654
- const input = inputOf(terminalId);
27655
- const startupDialog = input.startupDialog;
27656
- const firstMessage = startupDialog !== void 0 && (registry.get(terminalId)?.sessionId ?? "") === "";
27657
- let startupChecked = false;
27658
- let stoppedByDialog = false;
27659
- const pieceGuard = firstMessage || approvalGuard !== void 0 ? async () => {
27660
- if (firstMessage && startupDialog !== void 0 && !startupChecked) {
27661
- startupChecked = true;
27662
- const screen = registry.watchScreen(terminalId);
27663
- if (screen !== null) {
27664
- try {
27665
- stoppedByDialog = screenShowsDialog(await screen.lines(), startupDialog);
27666
- } finally {
27667
- screen.dispose();
27668
- }
27669
- if (stoppedByDialog) return false;
27670
- }
27671
- }
27672
- return approvalGuard === void 0 ? true : approvalGuard();
27673
- } : void 0;
27674
- const interrupted = () => sendError(socket, "submit-failed", serverText("submitInterrupted"));
27675
- void writeQueue.enqueue(
27676
- terminalId,
27677
- 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) => {
27678
29563
  const imagePaths = [];
27679
29564
  try {
27680
29565
  for (const image of images) {
@@ -27689,7 +29574,7 @@ function attachTerminalSocket(options) {
27689
29574
  error instanceof PasteImageError ? error.text : serverText("pasteImageFailed"),
27690
29575
  detail
27691
29576
  );
27692
- return;
29577
+ return null;
27693
29578
  }
27694
29579
  const attachmentLines = [];
27695
29580
  try {
@@ -27707,55 +29592,25 @@ function attachTerminalSocket(options) {
27707
29592
  error instanceof PasteFileError ? error.text : serverText("attachmentSaveFailed"),
27708
29593
  detail
27709
29594
  );
27710
- return;
29595
+ return null;
27711
29596
  }
27712
- const fullText = textWithAttachments(text2, attachmentLines);
27713
- const pieces = buildSubmissionWrites(fullText, imagePaths, input, {
27714
- send: message.send !== false
27715
- });
27716
- if (pieces === null) return;
27717
- registry.noteSubmitted(terminalId, fullText);
27718
- const startAfterMs = submitStartDelayMs(
27719
- registry.launchedAtOf(terminalId),
27720
- Date.now(),
27721
- input.readyAfterLaunchMs ?? 0
27722
- );
27723
- const outcome = await lane.writePieces(
27724
- pieces,
27725
- input.pieceGapMs,
27726
- pieceWriter(socket, terminalId),
27727
- pieceGuard,
27728
- { 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 })
27729
29611
  );
27730
- settle(outcome === "written");
27731
- if (outcome === "written" && message.send !== false && submitting?.promptQueue?.source === "app" && registry.activityOf(terminalId) === "busy" && !text2.trimStart().startsWith("/")) {
27732
- conversations.noteQueued(terminalId, { text: text2, images: images.length, at: Date.now() });
27733
- }
27734
- if (outcome === "written" && message.send !== false && submitting?.queuedSubmits !== void 0) {
27735
- queuedSubmits.note(
27736
- terminalId,
27737
- submissionPayload(fullText, imagePaths, input.imageReference) ?? fullText,
27738
- Date.now(),
27739
- fullText
27740
- );
27741
- }
27742
- if (outcome === "interrupted") interrupted();
27743
- if (outcome === "blocked" && stoppedByDialog) {
27744
- sendError(socket, "submit-failed", serverText("submitStartupDialog", { label: submitting?.label ?? "" }));
27745
- if (clearsDraft) send(socket, { type: "composer.prefill", terminalId, text: text2, reason: "not-ready" });
27746
- return;
27747
- }
27748
- if (outcome === "blocked") {
27749
- const waitingNow = blocksWhileWaiting && registry.activityOf(terminalId) === "waiting";
27750
- sendError(
27751
- socket,
27752
- "submit-failed",
27753
- waitingNow ? submitting === null ? serverText("submitStoppedWaitingNoLabel") : serverText("submitStoppedWaiting", { label: submitting.label }) : submitting === null ? serverText("submitStoppedToolNoLabel") : serverText("submitStoppedTool", { label: submitting.label })
27754
- );
27755
- }
27756
- },
27757
- { onDropped: interrupted }
27758
- ).then(() => settle(false));
29612
+ }
29613
+ }).finally(() => settle(false));
27759
29614
  break;
27760
29615
  }
27761
29616
  /*
@@ -27829,7 +29684,7 @@ function attachTerminalSocket(options) {
27829
29684
  sendError(socket, "answer-failed", check === "gone" ? ANSWER_NOT_PENDING_TEXT : ANSWER_MENU_CHANGED_TEXT);
27830
29685
  return;
27831
29686
  }
27832
- 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));
27833
29688
  if (outcome === "interrupted") interruptedAnswer();
27834
29689
  },
27835
29690
  { onDropped: interruptedAnswer }
@@ -27861,7 +29716,7 @@ function attachTerminalSocket(options) {
27861
29716
  const outcome = await lane.writePieces(
27862
29717
  keys,
27863
29718
  ANSWER_KEY_INTERVAL_MS,
27864
- pieceWriter(socket, message.terminalId)
29719
+ answerWriter(socket, message.terminalId)
27865
29720
  );
27866
29721
  if (outcome === "interrupted") interruptedAnswer();
27867
29722
  },
@@ -27927,10 +29782,10 @@ function attachTerminalSocket(options) {
27927
29782
  nunca esconde nada.
27928
29783
  */
27929
29784
  case "session.archive": {
27930
- const open9 = new Set(
29785
+ const open10 = new Set(
27931
29786
  registry.list().map((terminal) => terminal.sessionId).filter((id) => id.length > 0)
27932
29787
  );
27933
- 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;
27934
29789
  if (archived.set(targets, message.archived)) index.refresh();
27935
29790
  if (message.archived && targets.length < message.sessionIds.length) {
27936
29791
  sendError(
@@ -28644,17 +30499,24 @@ ${seen.join("\n-----\n")}`);
28644
30499
  })();
28645
30500
  break;
28646
30501
  /*
28647
- Cuales de esas rutas son archivos del proyecto (§6.33): lo que el
28648
- hilo vuelve enlace. Sin error para ningun caso —una pestana que ya no
28649
- esta, una ruta que cae fuera—: nada existe, y el hilo queda como
30502
+ Cuales de esas rutas son archivos o carpetas del proyecto (§6.33): lo
30503
+ que el hilo vuelve enlace. Sin error para ningun caso (una pestana que
30504
+ ya no esta, una ruta que cae fuera): nada existe, y el hilo queda como
28650
30505
  texto. Un cartel por una ruta que escribio el agente no le dice nada
28651
30506
  al usuario.
28652
30507
  */
28653
30508
  case "files.check":
28654
30509
  void (async () => {
28655
30510
  const cwd = cwdOf(message.terminalId);
28656
- const existing = cwd === null ? [] : await existingFiles(cwd, message.paths, await folders).catch(() => []);
28657
- send(socket, { type: "files.checked", terminalId: message.terminalId, requestId: message.requestId, existing });
30511
+ const none = { files: [], directories: [] };
30512
+ const { files, directories } = cwd === null ? none : await existingPaths(cwd, message.paths, await folders).catch(() => none);
30513
+ send(socket, {
30514
+ type: "files.checked",
30515
+ terminalId: message.terminalId,
30516
+ requestId: message.requestId,
30517
+ existing: files,
30518
+ directories
30519
+ });
28658
30520
  })();
28659
30521
  break;
28660
30522
  case "files.reveal":
@@ -29024,9 +30886,11 @@ ${seen.join("\n-----\n")}`);
29024
30886
  }
29025
30887
  case "worktree.remove": {
29026
30888
  const { requestId, worktreeId, discardConflicts } = message;
29027
- void worktrees.remove(worktreeId, discardConflicts, (terminalId) => void dropTab(terminalId)).then(
30889
+ const closeTab = (terminalId) => void dropTab(terminalId);
30890
+ void worktrees.remove(worktreeId, discardConflicts, closeTab, message.discardUntracked ?? null).then(
29028
30891
  (outcome) => send(socket, { type: "worktree.removed", requestId, worktreeId, outcome }),
29029
- (error) => sendWorktreeError(socket, error, requestId)
30892
+ // Otro programa tiene la carpeta: el plan otra vez, que dice cuál (23-10-2026).
30893
+ (error) => error instanceof WorktreeInUseError ? send(socket, { type: "worktree.removePlan", requestId, plan: error.plan }) : sendWorktreeError(socket, error, requestId)
29030
30894
  );
29031
30895
  break;
29032
30896
  }
@@ -29176,6 +31040,88 @@ ${seen.join("\n-----\n")}`);
29176
31040
  );
29177
31041
  break;
29178
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
+ }
29179
31125
  /*
29180
31126
  Ajustes (Hito 41, §6.32). Una consola que ya no esta, o un disco que no
29181
31127
  deja escribir, se dicen; lo demas lo valido el parser. A una ventana
@@ -29308,14 +31254,15 @@ ${seen.join("\n-----\n")}`);
29308
31254
  appSettings.off("status", onAppSettingsStatus);
29309
31255
  stopRemoteStatus();
29310
31256
  stopRemoteRevoked();
31257
+ void scheduler.stop();
29311
31258
  for (const client of wss.clients) client.terminate();
29312
31259
  wss.close();
29313
31260
  };
29314
31261
  }
29315
31262
 
29316
31263
  // packages/server/src/vault/catalog.ts
29317
- import { open as open8, readdir as readdir21, stat as stat45 } from "node:fs/promises";
29318
- import path71 from "node:path";
31264
+ import { open as open9, readdir as readdir24, stat as stat47 } from "node:fs/promises";
31265
+ import path75 from "node:path";
29319
31266
  import { createInterface as createInterface4 } from "node:readline";
29320
31267
  var HEADER_MAX_BYTES = 64 * 1024;
29321
31268
  var LOAD_CONCURRENCY = 16;
@@ -29327,7 +31274,7 @@ async function assetBytesOf(dir, agent, sessionId) {
29327
31274
  const folder = assetsDir(dir, agent, sessionId);
29328
31275
  let names;
29329
31276
  try {
29330
- names = await readdir21(folder);
31277
+ names = await readdir24(folder);
29331
31278
  } catch {
29332
31279
  return 0;
29333
31280
  }
@@ -29335,7 +31282,7 @@ async function assetBytesOf(dir, agent, sessionId) {
29335
31282
  for (const name of names) {
29336
31283
  if (!VAULT_ASSET_NAME_PATTERN.test(name)) continue;
29337
31284
  try {
29338
- const info = await stat45(path71.join(folder, name));
31285
+ const info = await stat47(path75.join(folder, name));
29339
31286
  if (info.isFile()) total += info.size;
29340
31287
  } catch {
29341
31288
  }
@@ -29346,7 +31293,7 @@ async function readEntry2(dir, agent, sessionId) {
29346
31293
  const file = sessionFile(dir, agent, sessionId);
29347
31294
  let sizeBytes;
29348
31295
  try {
29349
- const info = await stat45(file);
31296
+ const info = await stat47(file);
29350
31297
  if (!info.isFile()) return { kind: "absent" };
29351
31298
  sizeBytes = info.size;
29352
31299
  } catch {
@@ -29403,7 +31350,7 @@ var VaultCatalog = class {
29403
31350
  const keys = [];
29404
31351
  let agentFolders = [];
29405
31352
  try {
29406
- agentFolders = await readdir21(sessionsRoot2(dir));
31353
+ agentFolders = await readdir24(sessionsRoot2(dir));
29407
31354
  } catch {
29408
31355
  agentFolders = [];
29409
31356
  }
@@ -29411,7 +31358,7 @@ var VaultCatalog = class {
29411
31358
  if (!isSessionAgentId2(folder)) continue;
29412
31359
  let names;
29413
31360
  try {
29414
- names = await readdir21(agentSessionsDir(dir, folder));
31361
+ names = await readdir24(agentSessionsDir(dir, folder));
29415
31362
  } catch {
29416
31363
  continue;
29417
31364
  }
@@ -29500,7 +31447,7 @@ var VaultCatalog = class {
29500
31447
  async *bodyLines(agent, sessionId) {
29501
31448
  const dir = this.dir;
29502
31449
  if (dir === null || !this.entries.has(keyOf3(agent, sessionId))) return;
29503
- const handle = await open8(sessionFile(dir, agent, sessionId), "r").catch(() => null);
31450
+ const handle = await open9(sessionFile(dir, agent, sessionId), "r").catch(() => null);
29504
31451
  if (handle === null) return;
29505
31452
  const input = handle.createReadStream({ encoding: "utf8" });
29506
31453
  const lines = createInterface4({ input, crlfDelay: Number.POSITIVE_INFINITY });
@@ -29555,22 +31502,22 @@ var VaultCatalog = class {
29555
31502
  };
29556
31503
 
29557
31504
  // packages/server/src/worktrees/links.ts
29558
- import { stat as stat46 } from "node:fs/promises";
29559
- import path72 from "node:path";
29560
- async function exists5(candidate) {
31505
+ import { stat as stat48 } from "node:fs/promises";
31506
+ import path76 from "node:path";
31507
+ async function exists6(candidate) {
29561
31508
  try {
29562
- await stat46(candidate);
31509
+ await stat48(candidate);
29563
31510
  return true;
29564
31511
  } catch {
29565
31512
  return false;
29566
31513
  }
29567
31514
  }
29568
31515
  function recordAbove(registry, cwd) {
29569
- let current = path72.resolve(cwd);
31516
+ let current = path76.resolve(cwd);
29570
31517
  for (let depth = 0; depth < 24; depth += 1) {
29571
31518
  const record = registry.byPath(current);
29572
- if (record !== null) return { record, inner: path72.relative(current, path72.resolve(cwd)) };
29573
- const parent = path72.dirname(current);
31519
+ if (record !== null) return { record, inner: path76.relative(current, path76.resolve(cwd)) };
31520
+ const parent = path76.dirname(current);
29574
31521
  if (parent === current) return null;
29575
31522
  current = parent;
29576
31523
  }
@@ -29584,14 +31531,14 @@ var WorktreeLinks = class {
29584
31531
  /** La rama aparte de una carpeta del historial, o null si es la de un proyecto (o no es de git). */
29585
31532
  async sessionLink(cwd) {
29586
31533
  if (cwd.length === 0) return null;
29587
- if (await exists5(cwd)) {
31534
+ if (await exists6(cwd)) {
29588
31535
  const checkout = await readCheckout(cwd);
29589
31536
  if (checkout === null || !checkout.linked) return null;
29590
31537
  const record = this.registry.byPath(checkout.root);
29591
31538
  return {
29592
31539
  mainCwd: mainCwdFor(cwd, checkout),
29593
31540
  worktree: {
29594
- branch: checkout.branch ?? record?.branch ?? path72.basename(checkout.root),
31541
+ branch: checkout.branch ?? record?.branch ?? path76.basename(checkout.root),
29595
31542
  exists: true,
29596
31543
  reopenable: false
29597
31544
  }
@@ -29601,13 +31548,13 @@ var WorktreeLinks = class {
29601
31548
  if (found !== null) {
29602
31549
  const { record, inner } = found;
29603
31550
  return {
29604
- mainCwd: inner.length === 0 ? record.mainRoot : path72.join(record.mainRoot, inner),
31551
+ mainCwd: inner.length === 0 ? record.mainRoot : path76.join(record.mainRoot, inner),
29605
31552
  worktree: { branch: record.branch, exists: false, reopenable: await this.reopenable(record) }
29606
31553
  };
29607
31554
  }
29608
- const mainRoot = mainRootFromShape(path72.resolve(cwd));
31555
+ const mainRoot = mainRootFromShape(path76.resolve(cwd));
29609
31556
  if (mainRoot === null) return null;
29610
- return { mainCwd: mainRoot, worktree: { branch: path72.basename(cwd), exists: false, reopenable: false } };
31557
+ return { mainCwd: mainRoot, worktree: { branch: path76.basename(cwd), exists: false, reopenable: false } };
29611
31558
  }
29612
31559
  /**
29613
31560
  * Se puede volver a abrir una rama quitada si la creó la app, su carpeta no
@@ -29616,7 +31563,7 @@ var WorktreeLinks = class {
29616
31563
  */
29617
31564
  async reopenable(record) {
29618
31565
  if (record.removedAt === null) return false;
29619
- if (await exists5(record.path) || !await exists5(record.mainRoot)) return false;
31566
+ if (await exists6(record.path) || !await exists6(record.mainRoot)) return false;
29620
31567
  try {
29621
31568
  if (await branchCommit(record.mainRoot, record.branch) !== null) return true;
29622
31569
  return record.endCommit !== null && await commitExists(record.mainRoot, record.endCommit);
@@ -29637,15 +31584,15 @@ var WorktreeLinks = class {
29637
31584
  };
29638
31585
 
29639
31586
  // packages/server/src/index.ts
29640
- var serverDir = path73.dirname(fileURLToPath2(import.meta.url));
31587
+ var serverDir = path77.dirname(fileURLToPath2(import.meta.url));
29641
31588
  var isPackaged = true;
29642
- var webRoot = isPackaged ? serverDir : path73.resolve(serverDir, "../../web");
29643
- var webDist = isPackaged ? path73.join(serverDir, "web") : path73.join(webRoot, "dist");
29644
- var repoRoot = path73.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, "../../..");
29645
31592
  var isProduction = isPackaged || process.argv.includes("--prod");
29646
31593
  function resolveDefaultCwd() {
29647
31594
  const override = process.env["AGENT_WORKBENCH_CWD"];
29648
- if (override !== void 0 && override.length > 0) return path73.resolve(override);
31595
+ if (override !== void 0 && override.length > 0) return path77.resolve(override);
29649
31596
  if (isPackaged) return process.cwd();
29650
31597
  return repoRoot;
29651
31598
  }
@@ -29720,7 +31667,7 @@ async function mountDevUi(app, httpServer) {
29720
31667
  }
29721
31668
  var LOOKS_LIKE_FILE = /\.[a-zA-Z0-9]{1,8}$/;
29722
31669
  function mountBuiltUi(app) {
29723
- const indexHtml = path73.join(webDist, "index.html");
31670
+ const indexHtml = path77.join(webDist, "index.html");
29724
31671
  if (!existsSync4(indexHtml)) {
29725
31672
  throw new Error(
29726
31673
  `There's no compiled interface in ${webDist}.
@@ -29808,11 +31755,11 @@ async function main() {
29808
31755
  findSession: (agent, sessionId) => index.find(agent, sessionId),
29809
31756
  // Hito 49: las conversaciones de una rama (para dejarle un pedido a su autor) y la respuesta del revisor.
29810
31757
  sessionsUnder: (folder) => {
29811
- const root = path73.resolve(folder);
31758
+ const root = path77.resolve(folder);
29812
31759
  return index.getProjects().flatMap((project) => project.sessions).filter((session) => {
29813
31760
  if (session.archived || session.cwd.length === 0) return false;
29814
- const relative = path73.relative(root, path73.resolve(session.cwd));
29815
- return relative.length === 0 || !relative.startsWith("..") && !path73.isAbsolute(relative);
31761
+ const relative = path77.relative(root, path77.resolve(session.cwd));
31762
+ return relative.length === 0 || !relative.startsWith("..") && !path77.isAbsolute(relative);
29816
31763
  });
29817
31764
  },
29818
31765
  history: (agent) => agents.get(agent)?.adapter.history ?? null
@@ -29836,6 +31783,14 @@ async function main() {
29836
31783
  await notes.load();
29837
31784
  const drafts = new DraftStore();
29838
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
+ });
29839
31794
  const conversations = new ConversationHub(registry, agents);
29840
31795
  const repos = new RepoHub(registry);
29841
31796
  const memory = new MemoryHub(registry);
@@ -29887,21 +31842,30 @@ async function main() {
29887
31842
  defaultCwd,
29888
31843
  vault,
29889
31844
  remote,
29890
- worktrees
31845
+ worktrees,
31846
+ scheduled,
31847
+ restored,
31848
+ instances
29891
31849
  });
29892
31850
  void pasteStore.purgeStale();
29893
31851
  void index.start();
29894
31852
  const stopWatching = watchSessions(index, conversations, agents);
29895
31853
  conversations.on("historyGrew", (agent, filePath) => void index.refreshPath(agent, filePath));
29896
31854
  if (process.env["AGENT_WORKBENCH_NO_RESTORE"] !== "1") {
29897
- void store.load().then((state) => {
29898
- 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
+ });
29899
31861
  if (state.tabs.length === 0 && state.foreignTabs.length === 0) return;
29900
31862
  if (agents.anyAvailable()) {
29901
31863
  console.log(`Restoring ${state.tabs.length} tab(s) from the previous run...`);
29902
31864
  }
29903
31865
  return registry.restore(state);
29904
- });
31866
+ }).finally(markRestored);
31867
+ } else {
31868
+ markRestored();
29905
31869
  }
29906
31870
  const url = `http://${LOOPBACK_HOST}:${port}/?${TOKEN_QUERY_PARAM}=${token}`;
29907
31871
  const remoteStatus = remote.status();
@@ -29932,6 +31896,9 @@ async function main() {
29932
31896
  archived.flush(),
29933
31897
  notes.flush(),
29934
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(),
29935
31902
  remote.flush(),
29936
31903
  worktreeStore.flush(),
29937
31904
  agentsDisposed