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