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/README.md +22 -9
- package/dist/server.js +2382 -415
- package/dist/web/assets/de-5zSaN2t-.js +5 -0
- package/dist/web/assets/es-CFiCJcc8.js +5 -0
- package/dist/web/assets/fr-C7R68L38.js +5 -0
- package/dist/web/assets/{highlight-BVnJ3VdZ.js → highlight-BsYn5BK8.js} +1 -1
- package/dist/web/assets/index-fLdlIaV_.js +163 -0
- package/dist/web/assets/index-vwOssvpV.css +32 -0
- package/dist/web/assets/ja-CsWmKRl-.js +5 -0
- package/dist/web/assets/ko-DakWHUDB.js +5 -0
- package/dist/web/assets/pt-BR-CULiM_-O.js +5 -0
- package/dist/web/assets/ru-D8gI2TvJ.js +5 -0
- package/dist/web/assets/zh-CN-BYr9GmHu.js +5 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/de-CsDV7Kuv.js +0 -1
- package/dist/web/assets/es-1bnGoSgS.js +0 -1
- package/dist/web/assets/fr-BqyJYy32.js +0 -1
- package/dist/web/assets/index-B0Pd_BkE.css +0 -32
- package/dist/web/assets/index-DOfNPF9Z.js +0 -156
- package/dist/web/assets/ja-xHkS6kKf.js +0 -1
- package/dist/web/assets/ko-CC_KhzvE.js +0 -1
- package/dist/web/assets/pt-BR-Dctf7Mfd.js +0 -1
- package/dist/web/assets/ru-B7OPQhs_.js +0 -1
- package/dist/web/assets/zh-CN-Llsx2Xa1.js +0 -1
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
|
|
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
|
|
159
|
-
if (branch === null ||
|
|
160
|
-
return { branch, exists:
|
|
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
|
-
|
|
848
|
-
if (consoleShell ===
|
|
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
|
-
|
|
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
|
|
1368
|
-
return requestId === null || worktreeId === null ||
|
|
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
|
|
2956
|
-
const typed =
|
|
2957
|
-
return typed.trim().length > 0 ? typed :
|
|
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
|
|
3226
|
-
const close =
|
|
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 (
|
|
3230
|
-
text = content.slice(
|
|
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
|
|
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
|
|
6030
|
+
const normalized2 = value.replace(/\\/g, "/");
|
|
5882
6031
|
const marker = "/.claude/plans/";
|
|
5883
|
-
const at =
|
|
6032
|
+
const at = normalized2.toLowerCase().lastIndexOf(marker);
|
|
5884
6033
|
if (at === -1) return null;
|
|
5885
|
-
const name =
|
|
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
|
|
6114
|
+
const path78 = /* @__PURE__ */ new Set();
|
|
5966
6115
|
let current = leaf;
|
|
5967
|
-
while (current !== null && !
|
|
6116
|
+
while (current !== null && !path78.has(current)) {
|
|
5968
6117
|
const node = this.nodes.get(current);
|
|
5969
6118
|
if (node === void 0) break;
|
|
5970
|
-
|
|
6119
|
+
path78.add(current);
|
|
5971
6120
|
current = node.parent;
|
|
5972
6121
|
}
|
|
5973
|
-
return
|
|
6122
|
+
return path78;
|
|
5974
6123
|
}
|
|
5975
6124
|
recompute() {
|
|
5976
|
-
const
|
|
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 (!
|
|
5986
|
-
for (const uuid of
|
|
5987
|
-
for (const child of children.get(uuid) ?? []) if (!
|
|
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
|
|
8658
|
-
if (
|
|
8659
|
-
tab.submitted.push(
|
|
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
|
|
13072
|
-
return process.platform === "win32" ?
|
|
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
|
|
14194
|
-
this.live.setBlocked(this.sessionId,
|
|
14195
|
-
if (
|
|
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
|
|
15294
|
-
if (
|
|
15295
|
-
entry.openToolCall =
|
|
15296
|
-
this.emit("toolCall", terminalId,
|
|
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
|
|
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
|
-
|
|
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
|
|
18085
|
-
const opening =
|
|
18086
|
-
if (opening !== void 0 && !ignoredFences.has(index) && !(opening[0] === "`" && (
|
|
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(
|
|
19303
|
-
return style === "at-quoted" ? fileReference(
|
|
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
|
|
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
|
|
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 =
|
|
21649
|
+
let dir = path58.win32.dirname(gitPath);
|
|
20972
21650
|
for (let level = 0; level < 3; level++) {
|
|
20973
|
-
candidates.push(
|
|
20974
|
-
const parent =
|
|
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 :
|
|
20980
|
-
if (base !== void 0 && base.length > 0) candidates.push(
|
|
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 =
|
|
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
|
|
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
|
|
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((
|
|
21836
|
+
void isDirectory3(root.path).then((exists7) => {
|
|
21149
21837
|
checking = false;
|
|
21150
|
-
if (!
|
|
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((
|
|
21853
|
+
void isDirectory3(root.path).then((exists7) => {
|
|
21166
21854
|
if (stopped) return;
|
|
21167
|
-
if (
|
|
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
|
|
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
|
|
21494
|
-
import
|
|
21495
|
-
var
|
|
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"] !==
|
|
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:
|
|
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
|
|
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
|
|
22339
|
+
await mkdir16(appConfigDir(), { recursive: true });
|
|
21652
22340
|
const target = workspaceStatePath();
|
|
21653
|
-
const temporary =
|
|
21654
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
23077
|
+
const resolved = path60.resolve(target);
|
|
22390
23078
|
return dirs.some((dir) => {
|
|
22391
|
-
const protectedDir =
|
|
22392
|
-
return resolved === protectedDir || resolved.startsWith(protectedDir +
|
|
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(
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
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:
|
|
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
|
|
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(
|
|
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
|
|
22658
|
-
import
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
22793
|
-
const target = await
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
22919
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
22942
|
-
const fileName =
|
|
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
|
|
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
|
|
23684
|
+
const info = await stat39(target);
|
|
22994
23685
|
if (!info.isFile()) throw new FileWriteError(serverText("fileSaveFailed"));
|
|
22995
|
-
const current = await
|
|
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
|
|
24461
|
+
import { mkdir as mkdir20, stat as stat43 } from "node:fs/promises";
|
|
23668
24462
|
import { homedir as homedir15 } from "node:os";
|
|
23669
|
-
import
|
|
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
|
|
23674
|
-
import
|
|
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
|
|
23686
|
-
for (const name of await
|
|
23687
|
-
await copyTree(
|
|
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
|
|
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
|
|
24501
|
+
await mkdir18(to, { recursive: true });
|
|
23708
24502
|
for (const entry of MOVED_ENTRIES) {
|
|
23709
|
-
await copyTree(
|
|
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(
|
|
24535
|
+
const probe = temporaryPathFor(path63.join(dir, ".escritura"));
|
|
23742
24536
|
try {
|
|
23743
|
-
await
|
|
23744
|
-
if (!(await
|
|
23745
|
-
await
|
|
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
|
|
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
|
|
23848
|
-
import
|
|
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
|
|
23852
|
-
import
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
24709
|
+
await mkdir19(target, { recursive: true });
|
|
23916
24710
|
};
|
|
23917
24711
|
for (const note of notes) {
|
|
23918
|
-
const destination =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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 =
|
|
25342
|
+
const file = path65.join(folder, name);
|
|
24549
25343
|
try {
|
|
24550
|
-
const info = await
|
|
24551
|
-
if (info.isFile() && now - info.mtimeMs > TEMP_MAX_AGE_MS) await
|
|
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(),
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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 ?
|
|
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(
|
|
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
|
|
25059
|
-
import
|
|
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
|
|
25857
|
+
import { mkdtemp, rm as rm9, stat as stat44 } from "node:fs/promises";
|
|
25064
25858
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
25065
|
-
import
|
|
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 =
|
|
25112
|
-
const linked =
|
|
25113
|
-
const mainRoot =
|
|
25114
|
-
return { toplevel:
|
|
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
|
|
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
|
-
|
|
25137
|
-
|
|
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 ?
|
|
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(
|
|
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
|
|
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
|
|
25348
|
-
import
|
|
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(
|
|
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
|
|
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
|
|
26687
|
+
content = await readFile29(source);
|
|
25630
26688
|
const target = await resolveInside(folder, file.path);
|
|
25631
|
-
await
|
|
25632
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
25794
|
-
function worktreeFolderFor(mainRoot, branch, pathApi =
|
|
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 =
|
|
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 =
|
|
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
|
|
25817
|
-
import
|
|
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(
|
|
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 =
|
|
25847
|
-
const current = await
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
26935
|
+
const file = path71.join(this.dir, name);
|
|
25878
26936
|
try {
|
|
25879
|
-
const info = await
|
|
26937
|
+
const info = await stat45(file);
|
|
25880
26938
|
if (!info.isFile() || now - info.mtimeMs < this.graceMs) continue;
|
|
25881
|
-
await
|
|
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,
|
|
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
|
|
25967
|
-
import
|
|
25968
|
-
var
|
|
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"] !==
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
26133
|
-
return relative.length === 0 || !relative.startsWith("..") && !
|
|
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
|
|
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:
|
|
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
|
|
27381
|
+
if (await exists5(folder)) throw new WorktreeError(serverText("worktreeFolderExists", { folder }));
|
|
26231
27382
|
const copy = await planCopy(mainRoot);
|
|
26232
|
-
await
|
|
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
|
|
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 =
|
|
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
|
|
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 = [
|
|
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
|
|
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 ||
|
|
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
|
|
26404
|
-
const
|
|
26405
|
-
|
|
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 (
|
|
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
|
-
|
|
26429
|
-
|
|
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
|
-
|
|
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
|
|
27668
|
+
if (plan.inUse === true) throw new WorktreeInUseError(plan);
|
|
26454
27669
|
let returned = 0;
|
|
26455
|
-
|
|
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))
|
|
26458
|
-
|
|
26459
|
-
|
|
26460
|
-
|
|
26461
|
-
|
|
26462
|
-
|
|
26463
|
-
|
|
26464
|
-
|
|
26465
|
-
|
|
26466
|
-
|
|
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 =
|
|
26496
|
-
if (!await
|
|
26497
|
-
if (isInside4(folder, record.mainRoot) ||
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
26729
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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) =>
|
|
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
|
|
27627
|
-
|
|
27628
|
-
|
|
27629
|
-
|
|
27630
|
-
|
|
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
|
-
|
|
27648
|
-
|
|
27649
|
-
|
|
27650
|
-
|
|
27651
|
-
|
|
27652
|
-
|
|
27653
|
-
|
|
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
|
-
|
|
27713
|
-
|
|
27714
|
-
|
|
27715
|
-
|
|
27716
|
-
|
|
27717
|
-
|
|
27718
|
-
|
|
27719
|
-
|
|
27720
|
-
|
|
27721
|
-
|
|
27722
|
-
|
|
27723
|
-
|
|
27724
|
-
|
|
27725
|
-
|
|
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
|
-
|
|
27731
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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) => !
|
|
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
|
|
28648
|
-
hilo vuelve enlace. Sin error para ningun caso
|
|
28649
|
-
esta, una ruta que cae fuera
|
|
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
|
|
28657
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
29318
|
-
import
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
29559
|
-
import
|
|
29560
|
-
async function
|
|
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
|
|
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 =
|
|
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:
|
|
29573
|
-
const parent =
|
|
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
|
|
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 ??
|
|
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 :
|
|
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(
|
|
31555
|
+
const mainRoot = mainRootFromShape(path76.resolve(cwd));
|
|
29609
31556
|
if (mainRoot === null) return null;
|
|
29610
|
-
return { mainCwd: mainRoot, worktree: { branch:
|
|
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
|
|
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 =
|
|
31587
|
+
var serverDir = path77.dirname(fileURLToPath2(import.meta.url));
|
|
29641
31588
|
var isPackaged = true;
|
|
29642
|
-
var webRoot = isPackaged ? serverDir :
|
|
29643
|
-
var webDist = isPackaged ?
|
|
29644
|
-
var repoRoot =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
29815
|
-
return relative.length === 0 || !relative.startsWith("..") && !
|
|
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
|
-
|
|
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
|