@paradow/ledge 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/{attach-JNECAG6L.js → attach-IVICSJYB.js} +3 -3
- package/dist/cli/{chunk-CDME3WRV.js → chunk-ALEZJFJ3.js} +1 -1
- package/dist/cli/{chunk-ERN736NQ.js → chunk-EBBAOQ2Z.js} +4 -3
- package/dist/cli/{chunk-NVSBPIXY.js → chunk-EKXZ45MQ.js} +352 -231
- package/dist/cli/chunk-NFXFY5CT.js +6 -0
- package/dist/cli/{chunk-XR42PIDL.js → chunk-WRE3PUTV.js} +13 -8
- package/dist/cli/{daemon-DVPDCY3V.js → daemon-5FPZG5R3.js} +3 -3
- package/dist/cli/decide-2ZKSX7O5.js +56 -0
- package/dist/cli/{hook-ERYA6BKY.js → hook-KANGO46Y.js} +3 -3
- package/dist/cli/index.js +14 -7
- package/dist/cli/{launch-LAER5DI6.js → launch-DEKJ5HI4.js} +3 -3
- package/dist/cli/{task-K43WJIUX.js → task-UTRG4KA5.js} +1 -1
- package/dist/cli/{tui-AMERXSL7.js → tui-GEJHBWP5.js} +3 -3
- package/dist/cli/{version-BFNXT3H4.js → version-YUO6SLAF.js} +1 -1
- package/package.json +1 -1
- package/dist/cli/chunk-PFBZUINN.js +0 -6
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
shownCommand,
|
|
35
35
|
teammates,
|
|
36
36
|
withImages
|
|
37
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-WRE3PUTV.js";
|
|
38
38
|
import {
|
|
39
39
|
autonomyArgs,
|
|
40
40
|
findRole,
|
|
@@ -223,6 +223,7 @@ function jsonJournal(path, name, read) {
|
|
|
223
223
|
// src/daemon/tasks.ts
|
|
224
224
|
var CLOSED_TTL_MS = 7 * 24 * 3600 * 1e3;
|
|
225
225
|
var CLOSED_MAX = 300;
|
|
226
|
+
var PARKED_MS = 15 * 6e4;
|
|
226
227
|
var QUOTE = 200;
|
|
227
228
|
var quote = (s) => {
|
|
228
229
|
const flat = s.trim().replace(/\s+/g, " ");
|
|
@@ -286,20 +287,60 @@ function createTasks(d) {
|
|
|
286
287
|
};
|
|
287
288
|
const dropFor = (id) => {
|
|
288
289
|
for (const t of [...tasks.values()]) {
|
|
289
|
-
if (
|
|
290
|
+
if (taskClosed(t)) continue;
|
|
291
|
+
if (t.owner === LEDGE_GIVER && t.giver === id) {
|
|
292
|
+
put({ ...t, state: "dropped" }, "Ledge", `${t.giverName} s'est termin\xE9e avant la r\xE9ponse`, { op: "drop" });
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (t.owner !== id) continue;
|
|
290
296
|
const dropped = put({ ...t, state: "dropped" }, "Ledge", `${t.ownerName} s'est termin\xE9e sans la finir`, { op: "drop" });
|
|
291
297
|
log(`task ${t.id} dropped: ${t.ownerName} ended`);
|
|
292
298
|
tellGiver(dropped, "Ledge", `abandonn\xE9e : ${t.ownerName} s'est termin\xE9e sans la finir`);
|
|
293
299
|
}
|
|
294
300
|
};
|
|
295
|
-
for (const
|
|
296
|
-
const s = d.store.get(
|
|
297
|
-
if (!s || s.state === "exited") dropFor(
|
|
301
|
+
for (const id of new Set([...tasks.values()].filter((t) => !taskClosed(t)).map((t) => t.owner === LEDGE_GIVER ? t.giver : t.owner))) {
|
|
302
|
+
const s = d.store.get(id);
|
|
303
|
+
if (!s || s.state === "exited") dropFor(id);
|
|
298
304
|
}
|
|
305
|
+
const parkedMs = d.parkedMs ?? PARKED_MS;
|
|
306
|
+
const timers = /* @__PURE__ */ new Map();
|
|
307
|
+
const remindedFor = /* @__PURE__ */ new Map();
|
|
308
|
+
const heldBy = (id) => [...tasks.values()].filter((t) => t.owner === id && !taskClosed(t));
|
|
309
|
+
const setOf = (held) => held.map((t) => t.id).sort().join(",");
|
|
310
|
+
const unpark = (id) => {
|
|
311
|
+
const timer = timers.get(id);
|
|
312
|
+
if (timer) {
|
|
313
|
+
clearTimeout(timer);
|
|
314
|
+
timers.delete(id);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
const park = (id) => {
|
|
318
|
+
const s = d.store.get(id);
|
|
319
|
+
if (!s || s.state !== "idle" || timers.has(id) || !reachable(s)) return;
|
|
320
|
+
const held = heldBy(id);
|
|
321
|
+
if (!held.length || remindedFor.get(id) === setOf(held)) return;
|
|
322
|
+
timers.set(id, setTimeout(() => {
|
|
323
|
+
timers.delete(id);
|
|
324
|
+
const now_ = heldBy(id);
|
|
325
|
+
if (d.store.get(id)?.state !== "idle" || !now_.length || remindedFor.get(id) === setOf(now_)) return;
|
|
326
|
+
remindedFor.set(id, setOf(now_));
|
|
327
|
+
const list = now_.map((t) => `${t.id} \xAB ${t.title} \xBB (${t.state === "open" ? "\xE0 prendre" : "prise"})`).join(", ");
|
|
328
|
+
d.mailbox.notify(id, formatLedgeTask({ id: now_[0].id, from: "Ledge", text: `Tu es au repos et tu tiens encore ${list}. Prends ce qui est \xE0 prendre, termine (ledge task done <id> "<r\xE9sultat>"), passe (ledge task pass) ou refuse ce que tu ne feras pas ; si tu attends quelqu'un, ne fais rien.` }), `park:${id}:${setOf(now_)}`);
|
|
329
|
+
}, parkedMs));
|
|
330
|
+
};
|
|
331
|
+
for (const s of d.store.all()) park(s.id);
|
|
299
332
|
const unsubscribe = d.store.onChange((c) => {
|
|
300
333
|
if (c.type === "recents") return;
|
|
301
|
-
if (c.type === "removed")
|
|
302
|
-
|
|
334
|
+
if (c.type === "removed") {
|
|
335
|
+
unpark(c.id);
|
|
336
|
+
return dropFor(c.id);
|
|
337
|
+
}
|
|
338
|
+
if (c.session.state === "exited") {
|
|
339
|
+
unpark(c.session.id);
|
|
340
|
+
return dropFor(c.session.id);
|
|
341
|
+
}
|
|
342
|
+
if (c.session.state === "idle") park(c.session.id);
|
|
343
|
+
else unpark(c.session.id);
|
|
303
344
|
});
|
|
304
345
|
function run2(cmd) {
|
|
305
346
|
const refuse = (reason) => ({ type: "task.result", rid: cmd.rid, reason });
|
|
@@ -315,6 +356,29 @@ function createTasks(d) {
|
|
|
315
356
|
}
|
|
316
357
|
if (cmd.op === "give") {
|
|
317
358
|
if (!cmd.to || !cmd.title) return refuse("il faut un destinataire et un titre");
|
|
359
|
+
if (cmd.to.trim().toLowerCase() === LEDGE_GIVER) {
|
|
360
|
+
if (ledge) return refuse("tu ne peux pas te confier une t\xE2che");
|
|
361
|
+
const at2 = now();
|
|
362
|
+
const askerTeam = teamOf(me.id)?.id;
|
|
363
|
+
const task2 = {
|
|
364
|
+
id: newId(),
|
|
365
|
+
title: cmd.title.trim(),
|
|
366
|
+
...cmd.text?.trim() ? { text: cmd.text.trim() } : {},
|
|
367
|
+
giver: me.id,
|
|
368
|
+
giverName: me.name,
|
|
369
|
+
owner: LEDGE_GIVER,
|
|
370
|
+
ownerName: "Ledge",
|
|
371
|
+
...askerTeam ? { team: askerTeam } : {},
|
|
372
|
+
state: "open",
|
|
373
|
+
at: at2,
|
|
374
|
+
updatedAt: at2,
|
|
375
|
+
log: [{ at: at2, by: me.name, what: "confi\xE9e \xE0 Ledge (d\xE9cision)", ...sides("give", me, { id: LEDGE_GIVER, name: "Ledge" }) }]
|
|
376
|
+
};
|
|
377
|
+
tasks.set(task2.id, task2);
|
|
378
|
+
save();
|
|
379
|
+
d.onTask?.(task2, task2.log[0]);
|
|
380
|
+
return ok2(task2);
|
|
381
|
+
}
|
|
318
382
|
const t2 = target(cmd.to, ledge ? team : teamOf(me.id));
|
|
319
383
|
if (typeof t2 === "string") return refuse(t2);
|
|
320
384
|
if (t2.id === me.id) return refuse("tu ne peux pas te confier une t\xE2che");
|
|
@@ -339,6 +403,7 @@ function createTasks(d) {
|
|
|
339
403
|
save();
|
|
340
404
|
d.onTask?.(task, task.log[0]);
|
|
341
405
|
tellOwner(task, me.name, `T\xE2che de ${me.name} :`);
|
|
406
|
+
park(t2.id);
|
|
342
407
|
return ok2(task);
|
|
343
408
|
}
|
|
344
409
|
if (!cmd.task) return refuse("il faut l'id de la t\xE2che");
|
|
@@ -365,8 +430,9 @@ function createTasks(d) {
|
|
|
365
430
|
case "done": {
|
|
366
431
|
if (!owner) return refuse(`seule la session qui la tient (${t.ownerName}) peut la terminer`);
|
|
367
432
|
const result = cmd.text?.trim();
|
|
368
|
-
|
|
369
|
-
|
|
433
|
+
if (!result) return refuse('il faut un r\xE9sultat : ledge task done <id> "<ce qui est fait, o\xF9, tests>"');
|
|
434
|
+
const done = put({ ...t, state: "done", result }, me.name, `termin\xE9e : ${result}`, sides("done", ownerOf(t), giverOf(t)));
|
|
435
|
+
tellGiver(done, me.name, `termin\xE9e par ${me.name} : ${quote(result)}`);
|
|
370
436
|
return ok2(done);
|
|
371
437
|
}
|
|
372
438
|
case "refuse": {
|
|
@@ -384,7 +450,7 @@ function createTasks(d) {
|
|
|
384
450
|
return ok2(refused);
|
|
385
451
|
}
|
|
386
452
|
case "pass": {
|
|
387
|
-
if (!owner) return refuse(`seule la session qui la tient (${t.ownerName}) peut la passer`);
|
|
453
|
+
if (!owner && !giver) return refuse(`seule la session qui la tient (${t.ownerName}) ou celle qui l'a confi\xE9e (${t.giverName}) peut la passer`);
|
|
388
454
|
if (!cmd.to) return refuse("il faut le nom de la session \xE0 qui la passer");
|
|
389
455
|
const next2 = target(cmd.to, teamOf(me.id));
|
|
390
456
|
if (typeof next2 === "string") return refuse(next2);
|
|
@@ -400,12 +466,18 @@ function createTasks(d) {
|
|
|
400
466
|
{ ...sides("pass", ownerOf(t), next2), ...note ? { note } : {} }
|
|
401
467
|
);
|
|
402
468
|
tellOwner(passed, me.name, `T\xE2che pass\xE9e par ${me.name}${note ? ` (${quote(note)})` : ""}, confi\xE9e par ${t.giverName} :`);
|
|
469
|
+
park(next2.id);
|
|
470
|
+
if (!owner) d.mailbox.notify(t.owner, formatLedgeTask({ id: t.id, from: me.name, text: `T\xE2che \xAB ${t.title} \xBB pass\xE9e \xE0 ${next2.name} par ${me.name}. Arr\xEAte d'y travailler.` }), `${t.id}:${passed.log.length}:from`);
|
|
403
471
|
return ok2(passed);
|
|
404
472
|
}
|
|
405
473
|
}
|
|
406
474
|
return refuse("op\xE9ration inconnue");
|
|
407
475
|
}
|
|
408
|
-
return { run: run2, all: () => [...tasks.values()].sort((a, b) => a.at.localeCompare(b.at)), close:
|
|
476
|
+
return { run: run2, all: () => [...tasks.values()].sort((a, b) => a.at.localeCompare(b.at)), close: () => {
|
|
477
|
+
unsubscribe();
|
|
478
|
+
for (const timer of timers.values()) clearTimeout(timer);
|
|
479
|
+
timers.clear();
|
|
480
|
+
} };
|
|
409
481
|
}
|
|
410
482
|
function tasksJournal(path) {
|
|
411
483
|
return jsonJournal(path, "tasks", (raw) => Array.isArray(raw) ? raw : void 0);
|
|
@@ -1608,10 +1680,176 @@ function createRelay(d) {
|
|
|
1608
1680
|
};
|
|
1609
1681
|
}
|
|
1610
1682
|
|
|
1683
|
+
// src/daemon/handover.ts
|
|
1684
|
+
import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
1685
|
+
import { join as join3 } from "path";
|
|
1686
|
+
var BRIEF_MS = 10 * 6e4;
|
|
1687
|
+
var PROMPT_BRIEF_MAX = 6e3;
|
|
1688
|
+
function reliefOf(get, s) {
|
|
1689
|
+
let cur = s;
|
|
1690
|
+
for (let hops = 0; cur && hops < 50; hops++) {
|
|
1691
|
+
if (cur.state !== "exited" && !isArchived(cur)) return cur === s ? void 0 : cur;
|
|
1692
|
+
const to = cur.handover?.state === "done" ? cur.handover.to : void 0;
|
|
1693
|
+
cur = to ? get(to) : void 0;
|
|
1694
|
+
}
|
|
1695
|
+
return void 0;
|
|
1696
|
+
}
|
|
1697
|
+
function heldNames(all) {
|
|
1698
|
+
const byId = new Map(all.map((s) => [s.id, s]));
|
|
1699
|
+
return all.filter((s) => s.handover?.state === "done" && reliefOf((id) => byId.get(id), s)).map((s) => s.name);
|
|
1700
|
+
}
|
|
1701
|
+
var SECTIONS = [
|
|
1702
|
+
["Fait", /fait|done/],
|
|
1703
|
+
["En cours", /en cours|in progress/],
|
|
1704
|
+
["Suite", /suite|next/],
|
|
1705
|
+
["D\xE9cisions", /d[ée]cisions?/]
|
|
1706
|
+
].map(([name, re]) => [name, new RegExp(`(?:^|[\\s#*(-])(?:${re.source})\\s*\\**\\s*(?::|$)`, "im")]);
|
|
1707
|
+
var briefGaps = (text) => SECTIONS.filter(([, re]) => !re.test(text)).map(([name]) => name);
|
|
1708
|
+
var quote2 = (s, max) => s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
1709
|
+
function createHandovers(d) {
|
|
1710
|
+
const now = d.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
1711
|
+
const briefMs = d.briefMs ?? BRIEF_MS;
|
|
1712
|
+
const waiting2 = /* @__PURE__ */ new Map();
|
|
1713
|
+
const file = (id) => join3(d.dir, `${id}.md`);
|
|
1714
|
+
const mark = (id, h) => {
|
|
1715
|
+
const cur = d.store.get(id);
|
|
1716
|
+
if (cur) d.store.upsert({ ...cur, handover: { ...h, at: now() } });
|
|
1717
|
+
};
|
|
1718
|
+
const openTasks = (id) => d.tasks.all().filter((t) => t.owner === id && !taskClosed(t));
|
|
1719
|
+
const fallback = (s) => {
|
|
1720
|
+
const held = openTasks(s.id);
|
|
1721
|
+
const given = d.tasks.all().filter((t) => t.giver === s.id && !taskClosed(t));
|
|
1722
|
+
return [
|
|
1723
|
+
`Bilan \xE9crit par Ledge : ${s.name} n'a pas \xE9crit le sien. Il n'a not\xE9 ni d\xE9cisions ni pi\xE8ges : v\xE9rifie avant de d\xE9faire quoi que ce soit.`,
|
|
1724
|
+
...s.branch ? ["", `Branche : ${s.branch} (${s.workdir ?? s.cwd})`] : [],
|
|
1725
|
+
...s.lastReply?.text ? ["", "Sa derni\xE8re r\xE9ponse :", s.lastReply.text] : [],
|
|
1726
|
+
...held.length ? ["", "Ses t\xE2ches en cours :", ...held.map((t) => `- ${t.id} \xAB ${t.title} \xBB (confi\xE9e par ${t.giverName})`)] : [],
|
|
1727
|
+
...given.length ? ["", "Les t\xE2ches qu'il a confi\xE9es, encore ouvertes :", ...given.map((t) => `- ${t.id} \xAB ${t.title} \xBB \xE0 ${t.ownerName}`)] : []
|
|
1728
|
+
].join("\n");
|
|
1729
|
+
};
|
|
1730
|
+
async function relieve(id, brief, auto) {
|
|
1731
|
+
const s = d.store.get(id);
|
|
1732
|
+
if (!s) return;
|
|
1733
|
+
try {
|
|
1734
|
+
mkdirSync(d.dir, { recursive: true });
|
|
1735
|
+
writeFileSync(file(id), `${brief.trim()}
|
|
1736
|
+
`);
|
|
1737
|
+
} catch (e) {
|
|
1738
|
+
log(`brief of ${id} not kept: ${e.message}`);
|
|
1739
|
+
}
|
|
1740
|
+
mark(id, { state: "launching", ...auto ? { auto: true } : {} });
|
|
1741
|
+
const team = s.team ? d.teams.get(s.team) : void 0;
|
|
1742
|
+
const live3 = team && team.state !== "dissolved" ? team : void 0;
|
|
1743
|
+
const seat = s.role ? ` (@${s.role}${live3 ? `, \xE9quipe ${live3.name}` : ""})` : "";
|
|
1744
|
+
const prompt = [
|
|
1745
|
+
`Tu prends la rel\xE8ve de ${s.name}${seat}, dont le contexte \xE9tait plein. Voici son bilan :`,
|
|
1746
|
+
"",
|
|
1747
|
+
quote2(brief.trim(), PROMPT_BRIEF_MAX),
|
|
1748
|
+
"",
|
|
1749
|
+
"Ses t\xE2ches en cours te sont pass\xE9es par Ledge : elles t'arrivent une \xE0 une. Relis le bilan complet avec `ledge handover show`."
|
|
1750
|
+
].join("\n");
|
|
1751
|
+
let relief;
|
|
1752
|
+
try {
|
|
1753
|
+
relief = await d.launch({
|
|
1754
|
+
tool: s.tool,
|
|
1755
|
+
cwd: s.cwd,
|
|
1756
|
+
prompt,
|
|
1757
|
+
...s.model ? { model: s.model } : {},
|
|
1758
|
+
...s.effort ? { effort: s.effort } : {},
|
|
1759
|
+
...s.role ? { role: s.role } : {},
|
|
1760
|
+
...s.role && live3 ? { team: live3.id } : {}
|
|
1761
|
+
});
|
|
1762
|
+
} catch (e) {
|
|
1763
|
+
const reason = e.message;
|
|
1764
|
+
log(`relief of ${s.name} (${id}) not launched: ${reason}`);
|
|
1765
|
+
mark(id, { state: "failed", reason, ...auto ? { auto: true } : {} });
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
const at = now();
|
|
1769
|
+
const fresh = d.store.get(relief.id) ?? relief;
|
|
1770
|
+
d.store.upsert({ ...fresh, relieves: { id, name: s.name, at } });
|
|
1771
|
+
for (const t of openTasks(id)) {
|
|
1772
|
+
const r = d.tasks.run({ type: "task.cmd", rid: `handover-${t.id}`, by: id, op: "pass", task: t.id, to: relief.id, text: `rel\xE8ve de ${s.name}` });
|
|
1773
|
+
if (r.reason) log(`task ${t.id} not passed to ${relief.name}: ${r.reason}`);
|
|
1774
|
+
}
|
|
1775
|
+
if (s.team) d.store.patch(id, { team: void 0 });
|
|
1776
|
+
const cur = d.store.get(id);
|
|
1777
|
+
if (cur && cur.state !== "exited") await d.close(cur).catch((e) => log(`relieved ${id} not closed: ${e.message}`));
|
|
1778
|
+
mark(id, { state: "done", to: relief.id, toName: relief.name, ...auto ? { auto: true } : {} });
|
|
1779
|
+
log(`${s.name} (${id}) relieved by ${relief.name} (${relief.id})`);
|
|
1780
|
+
}
|
|
1781
|
+
const finish = (id, brief) => {
|
|
1782
|
+
const w = waiting2.get(id);
|
|
1783
|
+
if (!w) return;
|
|
1784
|
+
clearTimeout(w.timer);
|
|
1785
|
+
waiting2.delete(id);
|
|
1786
|
+
const s = d.store.get(id);
|
|
1787
|
+
if (!s) return;
|
|
1788
|
+
void relieve(id, brief ?? fallback(s), w.auto).catch((e) => {
|
|
1789
|
+
log(`relief of ${id} failed: ${e.message}`);
|
|
1790
|
+
mark(id, { state: "failed", reason: e.message });
|
|
1791
|
+
});
|
|
1792
|
+
};
|
|
1793
|
+
const unsubscribe = d.store.onChange((c) => {
|
|
1794
|
+
if (c.type === "removed") {
|
|
1795
|
+
const w = waiting2.get(c.id);
|
|
1796
|
+
if (w) {
|
|
1797
|
+
clearTimeout(w.timer);
|
|
1798
|
+
waiting2.delete(c.id);
|
|
1799
|
+
}
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
if (c.type === "updated" && c.session.state === "exited" && waiting2.has(c.session.id)) finish(c.session.id, void 0);
|
|
1803
|
+
});
|
|
1804
|
+
return {
|
|
1805
|
+
start(id, o = {}) {
|
|
1806
|
+
const s = d.store.get(id);
|
|
1807
|
+
if (!s) return "session inconnue";
|
|
1808
|
+
if (s.source !== "launcher") return `${s.name} n'a pas \xE9t\xE9 lanc\xE9e par Ledge`;
|
|
1809
|
+
if (s.state === "exited" || isArchived(s)) return `${s.name} est termin\xE9e`;
|
|
1810
|
+
if (s.tool === "omp" || !s.tmuxSession) return `${s.name} n'a pas de terminal o\xF9 demander son bilan`;
|
|
1811
|
+
if (waiting2.has(id) || s.handover?.state === "briefing" || s.handover?.state === "launching") return `la rel\xE8ve de ${s.name} est d\xE9j\xE0 en cours`;
|
|
1812
|
+
const share = s.context ? ` (${Math.round(s.context.used / s.context.window * 100)} %)` : "";
|
|
1813
|
+
const asked = d.mailbox.notify(id, `<ledge-handover>Ton contexte est presque plein${share} : une nouvelle session va prendre ta place. \xC9cris ton bilan de rel\xE8ve en une seule commande : ledge handover "<bilan>", avec ces sections : Fait : \u2026 En cours : \u2026 Suite : \u2026 D\xE9cisions : \u2026 (chacune avec sa raison), puis les pi\xE8ges et les fichiers cl\xE9s. Un bilan sans l'une d'elles est refus\xE9. Ne commence rien d'autre ensuite.</ledge-handover>`, `handover:${id}:${now()}`, { first: true });
|
|
1814
|
+
if (!asked) return `impossible d'\xE9crire \xE0 ${s.name}`;
|
|
1815
|
+
const auto = o.auto === true;
|
|
1816
|
+
waiting2.set(id, { timer: setTimeout(() => finish(id, void 0), briefMs), auto });
|
|
1817
|
+
mark(id, { state: "briefing", ...auto ? { auto: true } : {} });
|
|
1818
|
+
log(`relief of ${s.name} (${id}) asked${auto ? " by Ledge" : ""}`);
|
|
1819
|
+
return null;
|
|
1820
|
+
},
|
|
1821
|
+
brief(by, text) {
|
|
1822
|
+
if (!waiting2.has(by)) return "aucune rel\xE8ve ne t'attend : Ledge ne t'a pas demand\xE9 de bilan";
|
|
1823
|
+
const t = text.trim();
|
|
1824
|
+
if (!t) return "le bilan est vide";
|
|
1825
|
+
const gaps = briefGaps(t);
|
|
1826
|
+
if (gaps.length) return `bilan incomplet, il manque : ${gaps.join(", ")}. R\xE9\xE9cris-le en entier.`;
|
|
1827
|
+
finish(by, t.slice(0, BRIEF_MAX));
|
|
1828
|
+
return null;
|
|
1829
|
+
},
|
|
1830
|
+
show(by) {
|
|
1831
|
+
const s = d.store.get(by);
|
|
1832
|
+
if (!s) return { reason: "session inconnue" };
|
|
1833
|
+
const id = s.relieves?.id ?? by;
|
|
1834
|
+
try {
|
|
1835
|
+
return { text: readFileSync2(file(id), "utf8") };
|
|
1836
|
+
} catch {
|
|
1837
|
+
return { reason: s.relieves ? `le bilan de ${s.relieves.name} est introuvable` : "aucun bilan : tu ne prends la rel\xE8ve de personne" };
|
|
1838
|
+
}
|
|
1839
|
+
},
|
|
1840
|
+
close() {
|
|
1841
|
+
unsubscribe();
|
|
1842
|
+
for (const w of waiting2.values()) clearTimeout(w.timer);
|
|
1843
|
+
waiting2.clear();
|
|
1844
|
+
}
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1611
1848
|
// src/daemon/mailbox.ts
|
|
1612
1849
|
var MAX_SEEN = 1e3;
|
|
1613
1850
|
var BODY_MAX = 200;
|
|
1614
1851
|
var BODY_TTL_MS = 7 * 24 * 3600 * 1e3;
|
|
1852
|
+
var MESSAGES_PER_TURN = 6;
|
|
1615
1853
|
var PASSING = [
|
|
1616
1854
|
/^a decision is already in progress$/,
|
|
1617
1855
|
/^an? \w+ prompt takes accept or deny, not a reply$/,
|
|
@@ -1641,47 +1879,66 @@ function createMailbox(d) {
|
|
|
1641
1879
|
if (!reachable2(sender)) return;
|
|
1642
1880
|
notice(sender, formatLedgeNotice({ to: m.to, text: m.text, reason }), `${m.rid}:notice`);
|
|
1643
1881
|
}
|
|
1644
|
-
function notice(s, text, rid) {
|
|
1645
|
-
|
|
1882
|
+
function notice(s, text, rid, first = false) {
|
|
1883
|
+
const m = { kind: "notice", id: mailId(rid), rid, from: "", sender: { name: "Ledge", tool: s.tool }, target: s.id, to: s.name, text };
|
|
1884
|
+
if (first) queue.unshift(m);
|
|
1885
|
+
else queue.push(m);
|
|
1646
1886
|
queueMicrotask(() => kick(s.id));
|
|
1647
1887
|
}
|
|
1648
|
-
async function deliver(
|
|
1649
|
-
const
|
|
1888
|
+
async function deliver(batch) {
|
|
1889
|
+
const target2 = batch[0].target;
|
|
1890
|
+
const s = d.store.get(target2);
|
|
1650
1891
|
if (!s) {
|
|
1651
|
-
fail2(m, GONE);
|
|
1892
|
+
for (const m of batch) fail2(m, GONE);
|
|
1652
1893
|
save();
|
|
1653
|
-
queueMicrotask(() => kick(
|
|
1894
|
+
queueMicrotask(() => kick(target2));
|
|
1654
1895
|
return;
|
|
1655
1896
|
}
|
|
1656
|
-
d.typing.add(
|
|
1897
|
+
d.typing.add(target2);
|
|
1657
1898
|
try {
|
|
1658
1899
|
let refused;
|
|
1659
|
-
const text = m.kind === "notice" ? m.text : formatLedgeMessage({ from: m.sender.name, tool: m.sender.tool, text: m.text, to: s.tool, id: m.id, ...m.summary ? { summary: m.summary } : {} });
|
|
1900
|
+
const text = batch.map((m) => m.kind === "notice" ? m.text : formatLedgeMessage({ from: m.sender.name, tool: m.sender.tool, text: m.text, to: s.tool, id: m.id, ...m.summary ? { summary: m.summary } : {} })).join(" ");
|
|
1660
1901
|
try {
|
|
1661
|
-
refused = await d.replyTo(
|
|
1902
|
+
refused = await d.replyTo(target2, text);
|
|
1662
1903
|
} catch (e) {
|
|
1663
1904
|
refused = e.message;
|
|
1664
1905
|
}
|
|
1665
|
-
const live3 = d.store.get(
|
|
1906
|
+
const live3 = d.store.get(target2);
|
|
1666
1907
|
if (refused && PASSING.some((re) => re.test(refused)) && live3 && live3.state !== "exited") {
|
|
1667
|
-
log(`${
|
|
1668
|
-
queue.unshift(
|
|
1908
|
+
log(`${batch.length} item(s) to ${target2} wait: ${refused}`);
|
|
1909
|
+
queue.unshift(...batch);
|
|
1669
1910
|
return;
|
|
1670
1911
|
}
|
|
1671
|
-
if (refused
|
|
1672
|
-
|
|
1912
|
+
if (refused && batch.length > 1) {
|
|
1913
|
+
log(`batch to ${target2} refused (${refused}): each tried alone`);
|
|
1914
|
+
queue.unshift(...batch.map((m) => ({ ...m, alone: true })));
|
|
1915
|
+
} else for (const m of batch) {
|
|
1916
|
+
if (refused) fail2(m, refused);
|
|
1917
|
+
else if (m.kind === "message") d.sent.mark(m.from, m.rid, "delivered");
|
|
1918
|
+
}
|
|
1673
1919
|
save();
|
|
1674
|
-
if (refused) queueMicrotask(() => kick(
|
|
1920
|
+
if (refused) queueMicrotask(() => kick(target2));
|
|
1675
1921
|
} finally {
|
|
1676
|
-
d.typing.delete(
|
|
1922
|
+
d.typing.delete(target2);
|
|
1677
1923
|
}
|
|
1678
1924
|
}
|
|
1679
1925
|
function kick(id) {
|
|
1680
1926
|
if (!ready(id)) return;
|
|
1681
|
-
const i = queue.findIndex((
|
|
1927
|
+
const i = queue.findIndex((m) => m.target === id);
|
|
1682
1928
|
if (i < 0) return;
|
|
1683
|
-
const
|
|
1684
|
-
|
|
1929
|
+
const batch = [queue.splice(i, 1)[0]];
|
|
1930
|
+
if (batch[0].kind === "message" && !batch[0].alone) {
|
|
1931
|
+
for (let j = i; j < queue.length && batch.length < MESSAGES_PER_TURN; ) {
|
|
1932
|
+
const m = queue[j];
|
|
1933
|
+
if (m.target !== id) {
|
|
1934
|
+
j++;
|
|
1935
|
+
continue;
|
|
1936
|
+
}
|
|
1937
|
+
if (m.kind !== "message" || m.alone) break;
|
|
1938
|
+
batch.push(queue.splice(j, 1)[0]);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
void deliver(batch).catch((e) => log(`${batch.length} item(s) to ${id} failed: ${e.message}`));
|
|
1685
1942
|
}
|
|
1686
1943
|
function failFor(id, reason) {
|
|
1687
1944
|
let changed = false;
|
|
@@ -1701,7 +1958,11 @@ function createMailbox(d) {
|
|
|
1701
1958
|
});
|
|
1702
1959
|
const target = (to) => {
|
|
1703
1960
|
const name = bareName(to);
|
|
1704
|
-
|
|
1961
|
+
const all = d.store.all();
|
|
1962
|
+
const live3 = all.find((s) => s.name === name && s.state !== "exited" && !isArchived(s));
|
|
1963
|
+
if (live3) return live3;
|
|
1964
|
+
const relieved = all.filter((s) => s.name === name && s.handover?.state === "done").sort((a, b) => b.handover.at.localeCompare(a.handover.at))[0];
|
|
1965
|
+
return relieved ? reliefOf((id) => d.store.get(id), relieved) : void 0;
|
|
1705
1966
|
};
|
|
1706
1967
|
const remember = (rid, ack) => {
|
|
1707
1968
|
seen.set(rid, ack);
|
|
@@ -1760,10 +2021,10 @@ function createMailbox(d) {
|
|
|
1760
2021
|
if (by !== b.from && by !== b.target) return { type: "message.body", id, reason: `le message ${id} ne t'est pas adress\xE9` };
|
|
1761
2022
|
return { type: "message.body", id, from: b.fromName, to: b.to, at: b.at, text: b.text };
|
|
1762
2023
|
},
|
|
1763
|
-
notify(target2, text, rid) {
|
|
2024
|
+
notify(target2, text, rid, o = {}) {
|
|
1764
2025
|
const s = d.store.get(target2);
|
|
1765
2026
|
if (!reachable2(s)) return false;
|
|
1766
|
-
notice(s, text, rid);
|
|
2027
|
+
notice(s, text, rid, o.first);
|
|
1767
2028
|
save();
|
|
1768
2029
|
return true;
|
|
1769
2030
|
},
|
|
@@ -1933,7 +2194,7 @@ async function verifyGone(capture, stillVisible, opts = {}) {
|
|
|
1933
2194
|
|
|
1934
2195
|
// src/daemon/omp/host.ts
|
|
1935
2196
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
1936
|
-
import { basename, join as
|
|
2197
|
+
import { basename, join as join4 } from "path";
|
|
1937
2198
|
import { mkdir as mkdir3 } from "fs/promises";
|
|
1938
2199
|
|
|
1939
2200
|
// src/daemon/omp/rpc.ts
|
|
@@ -2490,7 +2751,7 @@ function createOmpHost(deps) {
|
|
|
2490
2751
|
const live3 = /* @__PURE__ */ new Map();
|
|
2491
2752
|
let generations = 0;
|
|
2492
2753
|
async function startProcess(id, o) {
|
|
2493
|
-
const dir =
|
|
2754
|
+
const dir = join4(deps.dir, id);
|
|
2494
2755
|
await mkdir3(dir, { recursive: true });
|
|
2495
2756
|
if (await reapOwner(dir)) log(`omp ${id}: ended the process a previous daemon left`);
|
|
2496
2757
|
const env = agentEnv({ ...await deps.env(), LEDGE_SESSION: id }, o.seatEnv ?? {});
|
|
@@ -2796,7 +3057,7 @@ import { execFile } from "child_process";
|
|
|
2796
3057
|
import { constants } from "fs";
|
|
2797
3058
|
import { open, stat as stat2 } from "fs/promises";
|
|
2798
3059
|
import { homedir } from "os";
|
|
2799
|
-
import { join as
|
|
3060
|
+
import { join as join5, resolve } from "path";
|
|
2800
3061
|
var GIT_TIMEOUT_MS = 2e3;
|
|
2801
3062
|
var MAX_GIT = 8;
|
|
2802
3063
|
var MIN_LEFT_MS = 100;
|
|
@@ -2834,7 +3095,7 @@ async function readGitFile(path, max = 64 * 1024) {
|
|
|
2834
3095
|
async function headBranch(dotGit) {
|
|
2835
3096
|
let head;
|
|
2836
3097
|
try {
|
|
2837
|
-
head = (await readGitFile(
|
|
3098
|
+
head = (await readGitFile(join5(dotGit, "HEAD"), MAX_HEAD_BYTES)).trim();
|
|
2838
3099
|
} catch {
|
|
2839
3100
|
return null;
|
|
2840
3101
|
}
|
|
@@ -2856,10 +3117,10 @@ async function inspectRepos(paths2, opts = {}) {
|
|
|
2856
3117
|
if (!isDir) return { path: raw, exists: false, branch: null };
|
|
2857
3118
|
let dotDir = false;
|
|
2858
3119
|
try {
|
|
2859
|
-
dotDir = (await stat2(
|
|
3120
|
+
dotDir = (await stat2(join5(path, ".git"))).isDirectory();
|
|
2860
3121
|
} catch {
|
|
2861
3122
|
}
|
|
2862
|
-
if (dotDir) return { path: raw, exists: true, branch: await headBranch(
|
|
3123
|
+
if (dotDir) return { path: raw, exists: true, branch: await headBranch(join5(path, ".git")) };
|
|
2863
3124
|
const deadline = Date.now() + GIT_TIMEOUT_MS;
|
|
2864
3125
|
const bounded = (args) => new Promise((res) => {
|
|
2865
3126
|
const t = setTimeout(() => res(null), Math.max(0, deadline - Date.now()));
|
|
@@ -2879,7 +3140,7 @@ async function inspectRepos(paths2, opts = {}) {
|
|
|
2879
3140
|
|
|
2880
3141
|
// src/daemon/scan.ts
|
|
2881
3142
|
import { readdir as fsReaddir } from "fs/promises";
|
|
2882
|
-
import { dirname, isAbsolute, join as
|
|
3143
|
+
import { dirname, isAbsolute, join as join6, resolve as resolve2, sep } from "path";
|
|
2883
3144
|
var SCAN_DEPTH = 4;
|
|
2884
3145
|
var SCAN_DIRS = 5e3;
|
|
2885
3146
|
var SCAN_MS = 4e3;
|
|
@@ -2934,17 +3195,17 @@ function originUrl(config) {
|
|
|
2934
3195
|
}
|
|
2935
3196
|
async function remoteOf(dir, dotIsFile, readFile6) {
|
|
2936
3197
|
try {
|
|
2937
|
-
let gitDir =
|
|
3198
|
+
let gitDir = join6(dir, ".git");
|
|
2938
3199
|
if (dotIsFile) {
|
|
2939
3200
|
const m = /^gitdir:\s*(.+)$/m.exec(await readFile6(gitDir));
|
|
2940
3201
|
if (!m) return void 0;
|
|
2941
3202
|
gitDir = resolve2(dir, m[1].trim());
|
|
2942
3203
|
try {
|
|
2943
|
-
gitDir = resolve2(gitDir, (await readFile6(
|
|
3204
|
+
gitDir = resolve2(gitDir, (await readFile6(join6(gitDir, "commondir"))).trim());
|
|
2944
3205
|
} catch {
|
|
2945
3206
|
}
|
|
2946
3207
|
}
|
|
2947
|
-
const url = originUrl(await readFile6(
|
|
3208
|
+
const url = originUrl(await readFile6(join6(gitDir, "config")));
|
|
2948
3209
|
return url ? repositoryId(url) : void 0;
|
|
2949
3210
|
} catch {
|
|
2950
3211
|
return void 0;
|
|
@@ -2964,9 +3225,9 @@ function manifestName(file, text) {
|
|
|
2964
3225
|
}
|
|
2965
3226
|
async function describe(dir, dotIsFile, home, readFile6, manifest) {
|
|
2966
3227
|
const [branch, remote, named] = await Promise.all([
|
|
2967
|
-
dotIsFile ? inspectRepos([dir], { home }).then((r) => r[0]?.branch ?? null) : headBranch(
|
|
3228
|
+
dotIsFile ? inspectRepos([dir], { home }).then((r) => r[0]?.branch ?? null) : headBranch(join6(dir, ".git")),
|
|
2968
3229
|
remoteOf(dir, dotIsFile, readFile6),
|
|
2969
|
-
manifest ? readFile6(
|
|
3230
|
+
manifest ? readFile6(join6(dir, manifest)).then((text) => manifestName(manifest, text), () => void 0) : void 0
|
|
2970
3231
|
]);
|
|
2971
3232
|
return { path: dir, branch, ...remote ? { remote } : {}, ...named ? { manifest: named } : {} };
|
|
2972
3233
|
}
|
|
@@ -3012,7 +3273,7 @@ async function scanRepos(opts) {
|
|
|
3012
3273
|
if (dot && d.path !== home || d.depth >= depth) return;
|
|
3013
3274
|
const names = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && !SKIP.has(e.name)).map((e) => e.name).sort();
|
|
3014
3275
|
for (const name of names) {
|
|
3015
|
-
const path =
|
|
3276
|
+
const path = join6(d.path, name);
|
|
3016
3277
|
if (!seen.has(path)) {
|
|
3017
3278
|
seen.add(path);
|
|
3018
3279
|
queue.push({ path, depth: d.depth + 1 });
|
|
@@ -3042,7 +3303,7 @@ async function scanRepos(opts) {
|
|
|
3042
3303
|
// src/daemon/folders.ts
|
|
3043
3304
|
import { mkdir as mkdir4, readdir as readdir2, stat as stat3 } from "fs/promises";
|
|
3044
3305
|
import { homedir as homedir2 } from "os";
|
|
3045
|
-
import { dirname as dirname2, isAbsolute as isAbsolute2, join as
|
|
3306
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join7, resolve as resolve3 } from "path";
|
|
3046
3307
|
async function resolveExact(raw, home) {
|
|
3047
3308
|
const expanded = raw === "~" ? home : raw.startsWith("~/") ? resolve3(home, raw.slice(2)) : raw;
|
|
3048
3309
|
if (!isAbsolute2(expanded)) throw new Error(`not an absolute path: ${raw}`);
|
|
@@ -3066,7 +3327,7 @@ async function subfolders(path) {
|
|
|
3066
3327
|
if (d.isDirectory()) return d.name;
|
|
3067
3328
|
if (!d.isSymbolicLink()) return null;
|
|
3068
3329
|
try {
|
|
3069
|
-
return (await stat3(
|
|
3330
|
+
return (await stat3(join7(path, d.name))).isDirectory() ? d.name : null;
|
|
3070
3331
|
} catch {
|
|
3071
3332
|
return null;
|
|
3072
3333
|
}
|
|
@@ -3074,7 +3335,7 @@ async function subfolders(path) {
|
|
|
3074
3335
|
return found.filter((n) => n !== null).sort(byName.compare);
|
|
3075
3336
|
}
|
|
3076
3337
|
async function gitBranchOf(dir, git) {
|
|
3077
|
-
const dot =
|
|
3338
|
+
const dot = join7(dir, ".git");
|
|
3078
3339
|
let s;
|
|
3079
3340
|
try {
|
|
3080
3341
|
s = await stat3(dot);
|
|
@@ -3091,7 +3352,7 @@ async function listFolder(input, home = homedir2(), requestId, git) {
|
|
|
3091
3352
|
const path = await resolveExact(asked, home);
|
|
3092
3353
|
const parent = dirname2(path);
|
|
3093
3354
|
const entries = await subfolders(path);
|
|
3094
|
-
const branches = await Promise.all(entries.map((name) => gitBranchOf(
|
|
3355
|
+
const branches = await Promise.all(entries.map((name) => gitBranchOf(join7(path, name), git)));
|
|
3095
3356
|
const repos = entries.flatMap((name, i) => branches[i] === null ? [] : [{ name, branch: branches[i] }]);
|
|
3096
3357
|
return { type: "folder.listing", path, parent: parent === path ? null : parent, entries, repos, ...tag };
|
|
3097
3358
|
} catch (e) {
|
|
@@ -3104,12 +3365,12 @@ async function createFolder(parent, name, home = homedir2(), requestId) {
|
|
|
3104
3365
|
const clean = name.trim();
|
|
3105
3366
|
if (!clean || clean === "." || clean === ".." || /[/\0]/.test(clean)) return { ...at, error: `invalid folder name: ${name}` };
|
|
3106
3367
|
try {
|
|
3107
|
-
await mkdir4(
|
|
3368
|
+
await mkdir4(join7(at.path, clean));
|
|
3108
3369
|
} catch (e) {
|
|
3109
3370
|
const code = e.code;
|
|
3110
3371
|
return { ...at, error: code === "EEXIST" ? `already exists: ${clean}` : `cannot create ${clean}: ${e.message}` };
|
|
3111
3372
|
}
|
|
3112
|
-
return listFolder(
|
|
3373
|
+
return listFolder(join7(at.path, clean), home, requestId);
|
|
3113
3374
|
}
|
|
3114
3375
|
|
|
3115
3376
|
// src/shell-path.ts
|
|
@@ -3146,12 +3407,12 @@ function loginShellEnv(opts = {}) {
|
|
|
3146
3407
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3147
3408
|
import { mkdir as mkdir6, readFile as readFile3, rm as rm2, writeFile as writeFile6 } from "fs/promises";
|
|
3148
3409
|
import { homedir as homedir4 } from "os";
|
|
3149
|
-
import { basename as basename3, dirname as dirname3, join as
|
|
3410
|
+
import { basename as basename3, dirname as dirname3, join as join11, resolve as resolve5 } from "path";
|
|
3150
3411
|
|
|
3151
3412
|
// src/chat/context.ts
|
|
3152
3413
|
import { mkdir as mkdir5, rm, writeFile as writeFile4 } from "fs/promises";
|
|
3153
3414
|
import { homedir as homedir3 } from "os";
|
|
3154
|
-
import { basename as basename2, join as
|
|
3415
|
+
import { basename as basename2, join as join8, resolve as resolve4 } from "path";
|
|
3155
3416
|
var STATE = {
|
|
3156
3417
|
starting: "starting",
|
|
3157
3418
|
working: "working",
|
|
@@ -3192,29 +3453,29 @@ async function writeContext(dir, input) {
|
|
|
3192
3453
|
const pane = await input.capture(s).catch(() => null);
|
|
3193
3454
|
if (pane?.length) {
|
|
3194
3455
|
const f = `pane-${s.id}.txt`;
|
|
3195
|
-
await writeFile4(
|
|
3456
|
+
await writeFile4(join8(dir, f), pane.join("\n"));
|
|
3196
3457
|
steps.set(f, { kind: "pane", id: s.id, name: s.name });
|
|
3197
3458
|
lines.push(`- terminal, last lines: ${f}`);
|
|
3198
3459
|
}
|
|
3199
3460
|
if (s.lastReply?.text.trim()) {
|
|
3200
3461
|
const f = `reply-${s.id}.md`;
|
|
3201
|
-
await writeFile4(
|
|
3462
|
+
await writeFile4(join8(dir, f), s.lastReply.text);
|
|
3202
3463
|
steps.set(f, { kind: "reply", id: s.id, name: s.name });
|
|
3203
3464
|
lines.push(`- last reply (${s.lastReply.at}): ${f}`);
|
|
3204
3465
|
}
|
|
3205
3466
|
const g = await input.git(s.cwd).catch(() => null);
|
|
3206
3467
|
if (g) {
|
|
3207
3468
|
const f = `git-${s.id}.txt`;
|
|
3208
|
-
await writeFile4(
|
|
3469
|
+
await writeFile4(join8(dir, f), g);
|
|
3209
3470
|
steps.set(f, { kind: "git", id: s.id, name: s.name });
|
|
3210
3471
|
lines.push(`- git status, log and diff stat: ${f}`);
|
|
3211
3472
|
}
|
|
3212
3473
|
lines.push("");
|
|
3213
3474
|
}
|
|
3214
|
-
await writeFile4(
|
|
3475
|
+
await writeFile4(join8(dir, "sessions.md"), lines.join("\n"));
|
|
3215
3476
|
steps.set("sessions.md", { kind: "sessions" });
|
|
3216
3477
|
const recents = input.recents ?? [];
|
|
3217
|
-
await writeFile4(
|
|
3478
|
+
await writeFile4(join8(dir, "folders.md"), [
|
|
3218
3479
|
"# Recent folders",
|
|
3219
3480
|
"",
|
|
3220
3481
|
"The folders the user started sessions in, most recent first.",
|
|
@@ -3222,7 +3483,7 @@ async function writeContext(dir, input) {
|
|
|
3222
3483
|
...recents.length ? recents.map((r) => `- ${r.project}: ${r.cwd} (last used ${r.lastAt})`) : ["- (none yet)"],
|
|
3223
3484
|
""
|
|
3224
3485
|
].join("\n"));
|
|
3225
|
-
if (withProjects) await writeFile4(
|
|
3486
|
+
if (withProjects) await writeFile4(join8(dir, "projects.md"), [
|
|
3226
3487
|
"# Projects",
|
|
3227
3488
|
"",
|
|
3228
3489
|
...projects.projects.length ? projects.projects.flatMap((p) => [`## ${oneLine(p.name)}`, ...p.description.trim() ? [oneLine(p.description)] : [], "", ...p.repos.map((r) => `- ${r.machine}: ${r.path}`), ""]) : ["No projects.", ""],
|
|
@@ -3264,7 +3525,7 @@ async function writeTeamsContext(dir, input, steps) {
|
|
|
3264
3525
|
""
|
|
3265
3526
|
);
|
|
3266
3527
|
}
|
|
3267
|
-
await writeFile4(
|
|
3528
|
+
await writeFile4(join8(dir, "roles.md"), roleLines.join("\n"));
|
|
3268
3529
|
const nameOf = new Map(input.sessions.map((s) => [s.id, s]));
|
|
3269
3530
|
const teams = (input.teams ?? []).filter((t) => t.state !== "dissolved");
|
|
3270
3531
|
const taskList = input.tasks ?? [];
|
|
@@ -3285,10 +3546,10 @@ async function writeTeamsContext(dir, input, steps) {
|
|
|
3285
3546
|
""
|
|
3286
3547
|
);
|
|
3287
3548
|
}
|
|
3288
|
-
await writeFile4(
|
|
3549
|
+
await writeFile4(join8(dir, "teams.md"), teamLines.join("\n"));
|
|
3289
3550
|
const day = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3290
3551
|
const shown = taskList.filter((t) => !taskClosed(t) || t.updatedAt.slice(0, 10) === day);
|
|
3291
|
-
await writeFile4(
|
|
3552
|
+
await writeFile4(join8(dir, "tasks.md"), [
|
|
3292
3553
|
"# Tasks",
|
|
3293
3554
|
"",
|
|
3294
3555
|
"Open tasks and those closed today. A giver `Ledge` is the user, through you.",
|
|
@@ -3316,7 +3577,7 @@ function stepFor(path, ctx) {
|
|
|
3316
3577
|
// src/chat/forge.ts
|
|
3317
3578
|
import { execFile as execFile3 } from "child_process";
|
|
3318
3579
|
import { writeFile as writeFile5 } from "fs/promises";
|
|
3319
|
-
import { join as
|
|
3580
|
+
import { join as join9 } from "path";
|
|
3320
3581
|
var FORGE_COMMANDS = {
|
|
3321
3582
|
gh: [
|
|
3322
3583
|
{ title: "Open issues", args: ["issue", "list", "--state", "open", "--limit", "20"] },
|
|
@@ -3408,19 +3669,19 @@ function createForge(deps = {}) {
|
|
|
3408
3669
|
return;
|
|
3409
3670
|
}
|
|
3410
3671
|
const parts = await Promise.all(local.map((r) => section(r.path)));
|
|
3411
|
-
await writeFile5(
|
|
3672
|
+
await writeFile5(join9(ctx.dir, file), parts.flatMap((s) => s.lines).join("\n"));
|
|
3412
3673
|
const sources = parts.flatMap((s) => s.source ? [s.source] : []);
|
|
3413
3674
|
ctx.steps.set(file, { kind: "repo", name, path: sources.join(", ") || file });
|
|
3414
3675
|
index.push(`- ${name}: ${file}`);
|
|
3415
3676
|
}));
|
|
3416
|
-
await writeFile5(
|
|
3677
|
+
await writeFile5(join9(ctx.dir, "forge.md"), [...index, ""].join("\n"));
|
|
3417
3678
|
}
|
|
3418
3679
|
};
|
|
3419
3680
|
}
|
|
3420
3681
|
|
|
3421
3682
|
// src/chat/claude.ts
|
|
3422
3683
|
import { spawn as nodeSpawn3 } from "child_process";
|
|
3423
|
-
import { readFileSync as
|
|
3684
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
3424
3685
|
|
|
3425
3686
|
// src/chat/run.ts
|
|
3426
3687
|
import { spawn as nodeSpawn2 } from "child_process";
|
|
@@ -3491,7 +3752,7 @@ function claudeArgs(o) {
|
|
|
3491
3752
|
function claudeInput(question, images) {
|
|
3492
3753
|
const content = [
|
|
3493
3754
|
{ type: "text", text: question },
|
|
3494
|
-
...images.map((i) => ({ type: "image", source: { type: "base64", media_type: i.mime, data:
|
|
3755
|
+
...images.map((i) => ({ type: "image", source: { type: "base64", media_type: i.mime, data: readFileSync3(i.path).toString("base64") } }))
|
|
3495
3756
|
];
|
|
3496
3757
|
return JSON.stringify({ type: "user", message: { role: "user", content } }) + "\n";
|
|
3497
3758
|
}
|
|
@@ -3570,7 +3831,7 @@ ${o.question}` }, parseCodexLine, onEvent, spawnImpl);
|
|
|
3570
3831
|
// src/chat/cursor.ts
|
|
3571
3832
|
import { spawn as nodeSpawn5 } from "child_process";
|
|
3572
3833
|
import { copyFileSync } from "fs";
|
|
3573
|
-
import { extname, join as
|
|
3834
|
+
import { extname, join as join10 } from "path";
|
|
3574
3835
|
function cursorArgs(o) {
|
|
3575
3836
|
return [
|
|
3576
3837
|
"-p",
|
|
@@ -3615,7 +3876,7 @@ function parseCursorLine(line) {
|
|
|
3615
3876
|
function runCursor(o, onEvent, spawnImpl = nodeSpawn5) {
|
|
3616
3877
|
const [file = "cursor-agent", ...pre] = o.bin ?? ["cursor-agent"];
|
|
3617
3878
|
const copies = (o.images ?? []).map((img, n) => {
|
|
3618
|
-
const to =
|
|
3879
|
+
const to = join10(o.cwd, `image-${n + 1}${extname(img.path)}`);
|
|
3619
3880
|
copyFileSync(img.path, to);
|
|
3620
3881
|
return to;
|
|
3621
3882
|
});
|
|
@@ -3701,7 +3962,7 @@ function createChat(deps) {
|
|
|
3701
3962
|
const now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
3702
3963
|
const today = deps.today ?? (() => now().slice(0, 10));
|
|
3703
3964
|
const uuid = deps.uuid ?? randomUUID2;
|
|
3704
|
-
const dayFile =
|
|
3965
|
+
const dayFile = join11(deps.home, "chat.json");
|
|
3705
3966
|
let turns = [];
|
|
3706
3967
|
let turnsDay = "";
|
|
3707
3968
|
let busy = false;
|
|
@@ -3736,7 +3997,7 @@ function createChat(deps) {
|
|
|
3736
3997
|
const p = await deps.tmux.capture(s.tmuxSession, PANE_LINES);
|
|
3737
3998
|
return p === null ? null : p.split("\n");
|
|
3738
3999
|
};
|
|
3739
|
-
const contextDir =
|
|
4000
|
+
const contextDir = join11(deps.home, "chat", "context");
|
|
3740
4001
|
async function proposalFrom(raw, tool, qid) {
|
|
3741
4002
|
const block = parseProposalBlock(raw);
|
|
3742
4003
|
if (!block) {
|
|
@@ -3925,7 +4186,7 @@ function createChat(deps) {
|
|
|
3925
4186
|
if (mine.cancelled) return CANCELLED;
|
|
3926
4187
|
let answer = "";
|
|
3927
4188
|
let resultError = null;
|
|
3928
|
-
const handle = runTool(tool, { question: turn.question, cwd: ctx.dir, addDirs: ctx.repoPaths, sessionId, resume, env, bin: deps.bins?.[tool] ?? deps.bin, ...images.length > 0 ? { images } : {}, ...tool === "omp" ? { sessionDir:
|
|
4189
|
+
const handle = runTool(tool, { question: turn.question, cwd: ctx.dir, addDirs: ctx.repoPaths, sessionId, resume, env, bin: deps.bins?.[tool] ?? deps.bin, ...images.length > 0 ? { images } : {}, ...tool === "omp" ? { sessionDir: join11(deps.home, "chat", "omp", sessionId) } : {} }, (e) => {
|
|
3929
4190
|
if (e.type === "tool") {
|
|
3930
4191
|
const step = e.path ? stepFor(e.path, ctx) : null;
|
|
3931
4192
|
if (step && !turn.steps.some((s) => sameStep(s, step))) {
|
|
@@ -4136,7 +4397,7 @@ function createChat(deps) {
|
|
|
4136
4397
|
|
|
4137
4398
|
// src/chat/threads.ts
|
|
4138
4399
|
import { readFile as readFile4, rm as rm3 } from "fs/promises";
|
|
4139
|
-
import { join as
|
|
4400
|
+
import { join as join12 } from "path";
|
|
4140
4401
|
var SAFE_ID = /^[A-Za-z0-9_-]{1,128}$/;
|
|
4141
4402
|
var checkId = (id) => {
|
|
4142
4403
|
if (!SAFE_ID.test(id)) throw new Error("invalid device id");
|
|
@@ -4161,7 +4422,7 @@ function createChatThreads(deps) {
|
|
|
4161
4422
|
const devices = /* @__PURE__ */ new Map();
|
|
4162
4423
|
const emitters = /* @__PURE__ */ new Map();
|
|
4163
4424
|
const dropping = /* @__PURE__ */ new Map();
|
|
4164
|
-
const deviceHome = (id) =>
|
|
4425
|
+
const deviceHome = (id) => join12(deps.home, "mobile", "chat", id);
|
|
4165
4426
|
const device = (id) => {
|
|
4166
4427
|
checkId(id);
|
|
4167
4428
|
if (dropping.has(id)) throw new Error("device thread is being dropped");
|
|
@@ -4190,7 +4451,7 @@ function createChatThreads(deps) {
|
|
|
4190
4451
|
/** The PC's tool of the day file, whatever its day: `claude` without one. */
|
|
4191
4452
|
async deviceTool() {
|
|
4192
4453
|
try {
|
|
4193
|
-
const t = JSON.parse(await readFile4(
|
|
4454
|
+
const t = JSON.parse(await readFile4(join12(deps.home, "chat.json"), "utf8")).tool;
|
|
4194
4455
|
return t === "claude" || t === "codex" || t === "cursor" || t === "omp" ? t : "claude";
|
|
4195
4456
|
} catch {
|
|
4196
4457
|
return "claude";
|
|
@@ -4234,12 +4495,12 @@ function createChatThreads(deps) {
|
|
|
4234
4495
|
}
|
|
4235
4496
|
|
|
4236
4497
|
// src/daemon/machines.ts
|
|
4237
|
-
import { readFileSync as
|
|
4498
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, renameSync } from "fs";
|
|
4238
4499
|
var DEFAULT_MACHINES = [{ id: "local", label: "Local" }];
|
|
4239
4500
|
function readMachines(file = paths().machines) {
|
|
4240
4501
|
let raw;
|
|
4241
4502
|
try {
|
|
4242
|
-
raw =
|
|
4503
|
+
raw = readFileSync4(file, "utf8");
|
|
4243
4504
|
} catch {
|
|
4244
4505
|
return DEFAULT_MACHINES;
|
|
4245
4506
|
}
|
|
@@ -4252,16 +4513,16 @@ function readMachines(file = paths().machines) {
|
|
|
4252
4513
|
}
|
|
4253
4514
|
function writeMachines(list, file = paths().machines) {
|
|
4254
4515
|
const tmp = `${file}.tmp`;
|
|
4255
|
-
|
|
4516
|
+
writeFileSync2(tmp, JSON.stringify(list, null, 2) + "\n");
|
|
4256
4517
|
renameSync(tmp, file);
|
|
4257
4518
|
}
|
|
4258
4519
|
|
|
4259
4520
|
// src/daemon/projects.ts
|
|
4260
|
-
import { readFileSync as
|
|
4521
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, renameSync as renameSync2 } from "fs";
|
|
4261
4522
|
function readProjects(file = paths().projects) {
|
|
4262
4523
|
let raw;
|
|
4263
4524
|
try {
|
|
4264
|
-
raw =
|
|
4525
|
+
raw = readFileSync5(file, "utf8");
|
|
4265
4526
|
} catch {
|
|
4266
4527
|
return DEFAULT_PROJECTS;
|
|
4267
4528
|
}
|
|
@@ -4274,16 +4535,16 @@ function readProjects(file = paths().projects) {
|
|
|
4274
4535
|
}
|
|
4275
4536
|
function writeProjects(state, file = paths().projects) {
|
|
4276
4537
|
const tmp = `${file}.tmp`;
|
|
4277
|
-
|
|
4538
|
+
writeFileSync3(tmp, JSON.stringify(state, null, 2) + "\n");
|
|
4278
4539
|
renameSync2(tmp, file);
|
|
4279
4540
|
}
|
|
4280
4541
|
|
|
4281
4542
|
// src/daemon/island.ts
|
|
4282
|
-
import { readFileSync as
|
|
4543
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, renameSync as renameSync3 } from "fs";
|
|
4283
4544
|
function readIsland(file = paths().island) {
|
|
4284
4545
|
let raw;
|
|
4285
4546
|
try {
|
|
4286
|
-
raw =
|
|
4547
|
+
raw = readFileSync6(file, "utf8");
|
|
4287
4548
|
} catch {
|
|
4288
4549
|
return null;
|
|
4289
4550
|
}
|
|
@@ -4296,13 +4557,13 @@ function readIsland(file = paths().island) {
|
|
|
4296
4557
|
}
|
|
4297
4558
|
function writeIsland(prefs, file = paths().island) {
|
|
4298
4559
|
const tmp = `${file}.tmp`;
|
|
4299
|
-
|
|
4560
|
+
writeFileSync4(tmp, JSON.stringify(prefs, null, 2) + "\n");
|
|
4300
4561
|
renameSync3(tmp, file);
|
|
4301
4562
|
}
|
|
4302
4563
|
function readContextPrefs(file) {
|
|
4303
4564
|
let raw;
|
|
4304
4565
|
try {
|
|
4305
|
-
raw =
|
|
4566
|
+
raw = readFileSync6(file, "utf8");
|
|
4306
4567
|
} catch {
|
|
4307
4568
|
return { near: CONTEXT_NEAR };
|
|
4308
4569
|
}
|
|
@@ -4315,7 +4576,7 @@ function readContextPrefs(file) {
|
|
|
4315
4576
|
}
|
|
4316
4577
|
function writeContextPrefs(prefs, file) {
|
|
4317
4578
|
const tmp = `${file}.tmp`;
|
|
4318
|
-
|
|
4579
|
+
writeFileSync4(tmp, JSON.stringify(prefs, null, 2) + "\n");
|
|
4319
4580
|
renameSync3(tmp, file);
|
|
4320
4581
|
}
|
|
4321
4582
|
|
|
@@ -4392,7 +4653,7 @@ async function readAt(request, path) {
|
|
|
4392
4653
|
import { execFile as execFile4 } from "child_process";
|
|
4393
4654
|
import { readFile as readFile5 } from "fs/promises";
|
|
4394
4655
|
import { homedir as homedir5 } from "os";
|
|
4395
|
-
import { join as
|
|
4656
|
+
import { join as join13 } from "path";
|
|
4396
4657
|
var CLAUDE_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
4397
4658
|
var CLAUDE_MODELS = [
|
|
4398
4659
|
{ id: "fable", label: "Fable", description: "The most capable" },
|
|
@@ -4464,7 +4725,7 @@ function createModels(deps = {}) {
|
|
|
4464
4725
|
const e = await env();
|
|
4465
4726
|
const home = e.HOME ?? homedir5();
|
|
4466
4727
|
if (tool === "claude") {
|
|
4467
|
-
const settings = await readText(
|
|
4728
|
+
const settings = await readText(join13(e.CLAUDE_CONFIG_DIR ?? join13(home, ".claude"), "settings.json"));
|
|
4468
4729
|
let model;
|
|
4469
4730
|
try {
|
|
4470
4731
|
model = settings ? JSON.parse(settings).model : void 0;
|
|
@@ -4480,7 +4741,7 @@ function createModels(deps = {}) {
|
|
|
4480
4741
|
if (r.code === null) throw new Error("not installed");
|
|
4481
4742
|
if (r.code !== 0) throw new Error(r.stderr.trim().split("\n").pop() || `exit ${r.code}`);
|
|
4482
4743
|
if (tool === "cursor") return parseCursorModels(r.stdout);
|
|
4483
|
-
return parseCodexModels(r.stdout, codexConfig(await readText(
|
|
4744
|
+
return parseCodexModels(r.stdout, codexConfig(await readText(join13(e.CODEX_HOME ?? join13(home, ".codex"), "config.toml"))));
|
|
4484
4745
|
}
|
|
4485
4746
|
return {
|
|
4486
4747
|
list(tool) {
|
|
@@ -4608,146 +4869,6 @@ function cardOf(threads, qid) {
|
|
|
4608
4869
|
} };
|
|
4609
4870
|
}
|
|
4610
4871
|
|
|
4611
|
-
// src/daemon/handover.ts
|
|
4612
|
-
import { mkdirSync, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
4613
|
-
import { join as join13 } from "path";
|
|
4614
|
-
var BRIEF_MS = 10 * 6e4;
|
|
4615
|
-
var PROMPT_BRIEF_MAX = 6e3;
|
|
4616
|
-
var quote2 = (s, max) => s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
4617
|
-
function createHandovers(d) {
|
|
4618
|
-
const now = d.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
4619
|
-
const briefMs = d.briefMs ?? BRIEF_MS;
|
|
4620
|
-
const waiting2 = /* @__PURE__ */ new Map();
|
|
4621
|
-
const file = (id) => join13(d.dir, `${id}.md`);
|
|
4622
|
-
const mark = (id, h) => {
|
|
4623
|
-
const cur = d.store.get(id);
|
|
4624
|
-
if (cur) d.store.upsert({ ...cur, handover: { ...h, at: now() } });
|
|
4625
|
-
};
|
|
4626
|
-
const openTasks = (id) => d.tasks.all().filter((t) => t.owner === id && !taskClosed(t));
|
|
4627
|
-
const fallback = (s) => {
|
|
4628
|
-
const held = openTasks(s.id);
|
|
4629
|
-
return [
|
|
4630
|
-
`Bilan \xE9crit par Ledge : ${s.name} n'a pas \xE9crit le sien.`,
|
|
4631
|
-
...s.lastReply?.text ? ["", "Sa derni\xE8re r\xE9ponse :", s.lastReply.text] : [],
|
|
4632
|
-
...held.length ? ["", "Ses t\xE2ches en cours :", ...held.map((t) => `- ${t.id} \xAB ${t.title} \xBB (confi\xE9e par ${t.giverName})`)] : []
|
|
4633
|
-
].join("\n");
|
|
4634
|
-
};
|
|
4635
|
-
async function relieve(id, brief, auto) {
|
|
4636
|
-
const s = d.store.get(id);
|
|
4637
|
-
if (!s) return;
|
|
4638
|
-
try {
|
|
4639
|
-
mkdirSync(d.dir, { recursive: true });
|
|
4640
|
-
writeFileSync4(file(id), `${brief.trim()}
|
|
4641
|
-
`);
|
|
4642
|
-
} catch (e) {
|
|
4643
|
-
log(`brief of ${id} not kept: ${e.message}`);
|
|
4644
|
-
}
|
|
4645
|
-
mark(id, { state: "launching", ...auto ? { auto: true } : {} });
|
|
4646
|
-
const team = s.team ? d.teams.get(s.team) : void 0;
|
|
4647
|
-
const live3 = team && team.state !== "dissolved" ? team : void 0;
|
|
4648
|
-
const seat = s.role ? ` (@${s.role}${live3 ? `, \xE9quipe ${live3.name}` : ""})` : "";
|
|
4649
|
-
const prompt = [
|
|
4650
|
-
`Tu prends la rel\xE8ve de ${s.name}${seat}, dont le contexte \xE9tait plein. Voici son bilan :`,
|
|
4651
|
-
"",
|
|
4652
|
-
quote2(brief.trim(), PROMPT_BRIEF_MAX),
|
|
4653
|
-
"",
|
|
4654
|
-
"Ses t\xE2ches en cours te sont pass\xE9es par Ledge : elles t'arrivent une \xE0 une. Relis le bilan complet avec `ledge handover show`."
|
|
4655
|
-
].join("\n");
|
|
4656
|
-
let relief;
|
|
4657
|
-
try {
|
|
4658
|
-
relief = await d.launch({
|
|
4659
|
-
tool: s.tool,
|
|
4660
|
-
cwd: s.cwd,
|
|
4661
|
-
prompt,
|
|
4662
|
-
...s.model ? { model: s.model } : {},
|
|
4663
|
-
...s.effort ? { effort: s.effort } : {},
|
|
4664
|
-
...s.role ? { role: s.role } : {},
|
|
4665
|
-
...s.role && live3 ? { team: live3.id } : {}
|
|
4666
|
-
});
|
|
4667
|
-
} catch (e) {
|
|
4668
|
-
const reason = e.message;
|
|
4669
|
-
log(`relief of ${s.name} (${id}) not launched: ${reason}`);
|
|
4670
|
-
mark(id, { state: "failed", reason, ...auto ? { auto: true } : {} });
|
|
4671
|
-
return;
|
|
4672
|
-
}
|
|
4673
|
-
const at = now();
|
|
4674
|
-
const fresh = d.store.get(relief.id) ?? relief;
|
|
4675
|
-
d.store.upsert({ ...fresh, relieves: { id, name: s.name, at } });
|
|
4676
|
-
for (const t of openTasks(id)) {
|
|
4677
|
-
const r = d.tasks.run({ type: "task.cmd", rid: `handover-${t.id}`, by: id, op: "pass", task: t.id, to: relief.id, text: `rel\xE8ve de ${s.name}` });
|
|
4678
|
-
if (r.reason) log(`task ${t.id} not passed to ${relief.name}: ${r.reason}`);
|
|
4679
|
-
}
|
|
4680
|
-
if (s.team) d.store.patch(id, { team: void 0 });
|
|
4681
|
-
const cur = d.store.get(id);
|
|
4682
|
-
if (cur && cur.state !== "exited") await d.close(cur).catch((e) => log(`relieved ${id} not closed: ${e.message}`));
|
|
4683
|
-
mark(id, { state: "done", to: relief.id, toName: relief.name, ...auto ? { auto: true } : {} });
|
|
4684
|
-
log(`${s.name} (${id}) relieved by ${relief.name} (${relief.id})`);
|
|
4685
|
-
}
|
|
4686
|
-
const finish = (id, brief) => {
|
|
4687
|
-
const w = waiting2.get(id);
|
|
4688
|
-
if (!w) return;
|
|
4689
|
-
clearTimeout(w.timer);
|
|
4690
|
-
waiting2.delete(id);
|
|
4691
|
-
const s = d.store.get(id);
|
|
4692
|
-
if (!s) return;
|
|
4693
|
-
void relieve(id, brief ?? fallback(s), w.auto).catch((e) => {
|
|
4694
|
-
log(`relief of ${id} failed: ${e.message}`);
|
|
4695
|
-
mark(id, { state: "failed", reason: e.message });
|
|
4696
|
-
});
|
|
4697
|
-
};
|
|
4698
|
-
const unsubscribe = d.store.onChange((c) => {
|
|
4699
|
-
if (c.type === "removed") {
|
|
4700
|
-
const w = waiting2.get(c.id);
|
|
4701
|
-
if (w) {
|
|
4702
|
-
clearTimeout(w.timer);
|
|
4703
|
-
waiting2.delete(c.id);
|
|
4704
|
-
}
|
|
4705
|
-
return;
|
|
4706
|
-
}
|
|
4707
|
-
if (c.type === "updated" && c.session.state === "exited" && waiting2.has(c.session.id)) finish(c.session.id, void 0);
|
|
4708
|
-
});
|
|
4709
|
-
return {
|
|
4710
|
-
start(id, o = {}) {
|
|
4711
|
-
const s = d.store.get(id);
|
|
4712
|
-
if (!s) return "session inconnue";
|
|
4713
|
-
if (s.source !== "launcher") return `${s.name} n'a pas \xE9t\xE9 lanc\xE9e par Ledge`;
|
|
4714
|
-
if (s.state === "exited" || isArchived(s)) return `${s.name} est termin\xE9e`;
|
|
4715
|
-
if (s.tool === "omp" || !s.tmuxSession) return `${s.name} n'a pas de terminal o\xF9 demander son bilan`;
|
|
4716
|
-
if (waiting2.has(id) || s.handover?.state === "briefing" || s.handover?.state === "launching") return `la rel\xE8ve de ${s.name} est d\xE9j\xE0 en cours`;
|
|
4717
|
-
const share = s.context ? ` (${Math.round(s.context.used / s.context.window * 100)} %)` : "";
|
|
4718
|
-
const asked = d.mailbox.notify(id, `<ledge-handover>Ton contexte est presque plein${share} : une nouvelle session va prendre ta place. \xC9cris ton bilan de rel\xE8ve en une seule commande : ledge handover "<bilan>" \u2014 ce qui est fait, ce qui est en cours, la suite, les pi\xE8ges, les fichiers cl\xE9s. Ne commence rien d'autre ensuite.</ledge-handover>`, `handover:${id}:${now()}`);
|
|
4719
|
-
if (!asked) return `impossible d'\xE9crire \xE0 ${s.name}`;
|
|
4720
|
-
const auto = o.auto === true;
|
|
4721
|
-
waiting2.set(id, { timer: setTimeout(() => finish(id, void 0), briefMs), auto });
|
|
4722
|
-
mark(id, { state: "briefing", ...auto ? { auto: true } : {} });
|
|
4723
|
-
log(`relief of ${s.name} (${id}) asked${auto ? " by Ledge" : ""}`);
|
|
4724
|
-
return null;
|
|
4725
|
-
},
|
|
4726
|
-
brief(by, text) {
|
|
4727
|
-
if (!waiting2.has(by)) return "aucune rel\xE8ve ne t'attend : Ledge ne t'a pas demand\xE9 de bilan";
|
|
4728
|
-
const t = text.trim();
|
|
4729
|
-
if (!t) return "le bilan est vide";
|
|
4730
|
-
finish(by, t.slice(0, BRIEF_MAX));
|
|
4731
|
-
return null;
|
|
4732
|
-
},
|
|
4733
|
-
show(by) {
|
|
4734
|
-
const s = d.store.get(by);
|
|
4735
|
-
if (!s) return { reason: "session inconnue" };
|
|
4736
|
-
const id = s.relieves?.id ?? by;
|
|
4737
|
-
try {
|
|
4738
|
-
return { text: readFileSync6(file(id), "utf8") };
|
|
4739
|
-
} catch {
|
|
4740
|
-
return { reason: s.relieves ? `le bilan de ${s.relieves.name} est introuvable` : "aucun bilan : tu ne prends la rel\xE8ve de personne" };
|
|
4741
|
-
}
|
|
4742
|
-
},
|
|
4743
|
-
close() {
|
|
4744
|
-
unsubscribe();
|
|
4745
|
-
for (const w of waiting2.values()) clearTimeout(w.timer);
|
|
4746
|
-
waiting2.clear();
|
|
4747
|
-
}
|
|
4748
|
-
};
|
|
4749
|
-
}
|
|
4750
|
-
|
|
4751
4872
|
// src/daemon/context.ts
|
|
4752
4873
|
import { open as open3 } from "fs/promises";
|
|
4753
4874
|
var TAIL_BYTES = 512 * 1024;
|
|
@@ -7209,7 +7330,7 @@ async function startServer(opts) {
|
|
|
7209
7330
|
...role ? { role: { id: role.id, label: role.label } } : {},
|
|
7210
7331
|
...team ? { team: team.id } : {},
|
|
7211
7332
|
...autonomy ? { autonomy } : {},
|
|
7212
|
-
taken: { names: live3.filter((s) => s.state !== "exited").map((s) => s.name), ids: live3.map((s) => s.id) }
|
|
7333
|
+
taken: { names: [...live3.filter((s) => s.state !== "exited").map((s) => s.name), ...heldNames(live3)], ids: live3.map((s) => s.id) }
|
|
7213
7334
|
});
|
|
7214
7335
|
const session = { ...spawned, ...req.model ? { model: req.model } : {}, ...req.effort ? { effort: req.effort } : {} };
|
|
7215
7336
|
store.upsert(session);
|