@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.
@@ -34,7 +34,7 @@ import {
34
34
  shownCommand,
35
35
  teammates,
36
36
  withImages
37
- } from "./chunk-XR42PIDL.js";
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 (t.owner !== id || taskClosed(t)) continue;
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 owner of new Set([...tasks.values()].filter((t) => !taskClosed(t)).map((t) => t.owner))) {
296
- const s = d.store.get(owner);
297
- if (!s || s.state === "exited") dropFor(owner);
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") return dropFor(c.id);
302
- if (c.session.state === "exited") dropFor(c.session.id);
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
- const done = put({ ...t, state: "done", ...result ? { result } : {} }, me.name, result ? `termin\xE9e : ${result}` : "termin\xE9e", sides("done", ownerOf(t), giverOf(t)));
369
- tellGiver(done, me.name, `termin\xE9e par ${me.name}${result ? ` : ${quote(result)}` : ""}`);
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: unsubscribe };
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
- queue.push({ kind: "notice", id: mailId(rid), rid, from: "", sender: { name: "Ledge", tool: s.tool }, target: s.id, to: s.name, text });
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(m) {
1649
- const s = d.store.get(m.target);
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(m.target));
1894
+ queueMicrotask(() => kick(target2));
1654
1895
  return;
1655
1896
  }
1656
- d.typing.add(m.target);
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(m.target, text);
1902
+ refused = await d.replyTo(target2, text);
1662
1903
  } catch (e) {
1663
1904
  refused = e.message;
1664
1905
  }
1665
- const live3 = d.store.get(m.target);
1906
+ const live3 = d.store.get(target2);
1666
1907
  if (refused && PASSING.some((re) => re.test(refused)) && live3 && live3.state !== "exited") {
1667
- log(`${m.kind} ${m.rid} to ${m.target} waits: ${refused}`);
1668
- queue.unshift(m);
1908
+ log(`${batch.length} item(s) to ${target2} wait: ${refused}`);
1909
+ queue.unshift(...batch);
1669
1910
  return;
1670
1911
  }
1671
- if (refused) fail2(m, refused);
1672
- else if (m.kind === "message") d.sent.mark(m.from, m.rid, "delivered");
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(m.target));
1920
+ if (refused) queueMicrotask(() => kick(target2));
1675
1921
  } finally {
1676
- d.typing.delete(m.target);
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((m2) => m2.target === id);
1927
+ const i = queue.findIndex((m) => m.target === id);
1682
1928
  if (i < 0) return;
1683
- const [m] = queue.splice(i, 1);
1684
- void deliver(m).catch((e) => log(`${m.kind} ${m.rid} failed: ${e.message}`));
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
- return d.store.all().find((s) => s.name === name && s.state !== "exited" && !isArchived(s));
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 join3 } from "path";
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 = join3(deps.dir, id);
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 join4, resolve } from "path";
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(join4(dotGit, "HEAD"), MAX_HEAD_BYTES)).trim();
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(join4(path, ".git"))).isDirectory();
3120
+ dotDir = (await stat2(join5(path, ".git"))).isDirectory();
2860
3121
  } catch {
2861
3122
  }
2862
- if (dotDir) return { path: raw, exists: true, branch: await headBranch(join4(path, ".git")) };
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 join5, resolve as resolve2, sep } from "path";
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 = join5(dir, ".git");
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(join5(gitDir, "commondir"))).trim());
3204
+ gitDir = resolve2(gitDir, (await readFile6(join6(gitDir, "commondir"))).trim());
2944
3205
  } catch {
2945
3206
  }
2946
3207
  }
2947
- const url = originUrl(await readFile6(join5(gitDir, "config")));
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(join5(dir, ".git")),
3228
+ dotIsFile ? inspectRepos([dir], { home }).then((r) => r[0]?.branch ?? null) : headBranch(join6(dir, ".git")),
2968
3229
  remoteOf(dir, dotIsFile, readFile6),
2969
- manifest ? readFile6(join5(dir, manifest)).then((text) => manifestName(manifest, text), () => void 0) : void 0
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 = join5(d.path, name);
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 join6, resolve as resolve3 } from "path";
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(join6(path, d.name))).isDirectory() ? d.name : null;
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 = join6(dir, ".git");
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(join6(path, name), git)));
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(join6(at.path, clean));
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(join6(at.path, clean), home, requestId);
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 join10, resolve as resolve5 } from "path";
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 join7, resolve as resolve4 } from "path";
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(join7(dir, f), pane.join("\n"));
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(join7(dir, f), s.lastReply.text);
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(join7(dir, f), g);
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(join7(dir, "sessions.md"), lines.join("\n"));
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(join7(dir, "folders.md"), [
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(join7(dir, "projects.md"), [
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(join7(dir, "roles.md"), roleLines.join("\n"));
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(join7(dir, "teams.md"), teamLines.join("\n"));
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(join7(dir, "tasks.md"), [
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 join8 } from "path";
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(join8(ctx.dir, file), parts.flatMap((s) => s.lines).join("\n"));
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(join8(ctx.dir, "forge.md"), [...index, ""].join("\n"));
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 readFileSync2 } from "fs";
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: readFileSync2(i.path).toString("base64") } }))
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 join9 } from "path";
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 = join9(o.cwd, `image-${n + 1}${extname(img.path)}`);
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 = join10(deps.home, "chat.json");
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 = join10(deps.home, "chat", "context");
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: join10(deps.home, "chat", "omp", sessionId) } : {} }, (e) => {
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 join11 } from "path";
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) => join11(deps.home, "mobile", "chat", 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(join11(deps.home, "chat.json"), "utf8")).tool;
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 readFileSync3, writeFileSync, renameSync } from "fs";
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 = readFileSync3(file, "utf8");
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
- writeFileSync(tmp, JSON.stringify(list, null, 2) + "\n");
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 readFileSync4, writeFileSync as writeFileSync2, renameSync as renameSync2 } from "fs";
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 = readFileSync4(file, "utf8");
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
- writeFileSync2(tmp, JSON.stringify(state, null, 2) + "\n");
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 readFileSync5, writeFileSync as writeFileSync3, renameSync as renameSync3 } from "fs";
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 = readFileSync5(file, "utf8");
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
- writeFileSync3(tmp, JSON.stringify(prefs, null, 2) + "\n");
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 = readFileSync5(file, "utf8");
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
- writeFileSync3(tmp, JSON.stringify(prefs, null, 2) + "\n");
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 join12 } from "path";
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(join12(e.CLAUDE_CONFIG_DIR ?? join12(home, ".claude"), "settings.json"));
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(join12(e.CODEX_HOME ?? join12(home, ".codex"), "config.toml"))));
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);