@paradow/ledge 0.16.0 → 0.17.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.
Files changed (32) hide show
  1. package/README.md +2 -0
  2. package/dist/cli/{attach-5H727FCW.js → attach-EYIX7FZN.js} +7 -7
  3. package/dist/cli/{bridge-LXMOQN6Q.js → bridge-6UHCD76N.js} +2 -2
  4. package/dist/cli/{chunk-MQICEVKR.js → chunk-36KJUV62.js} +6 -6
  5. package/dist/cli/chunk-5ZCHFGCV.js +115 -0
  6. package/dist/cli/{chunk-QJVODYJ5.js → chunk-AQ25HFBP.js} +40 -2
  7. package/dist/cli/{chunk-R5462SQM.js → chunk-ELLXOV4Q.js} +2 -1
  8. package/dist/cli/{chunk-BNKPXSUY.js → chunk-JQZHXVIZ.js} +1 -1
  9. package/dist/cli/{chunk-2H5FJH4Q.js → chunk-LAUNHDTX.js} +2 -2
  10. package/dist/cli/{chunk-DBSCO3HI.js → chunk-NC5YAEJB.js} +56 -7
  11. package/dist/cli/{chunk-S4ANPQGA.js → chunk-OVJL2QTN.js} +2 -2
  12. package/dist/cli/{chunk-TB47EHML.js → chunk-Q7YNLP66.js} +2 -2
  13. package/dist/cli/{chunk-Q5WWH7FG.js → chunk-QD27W6W5.js} +681 -630
  14. package/dist/cli/chunk-V2H7UD2N.js +6 -0
  15. package/dist/cli/{chunk-H36JD4E3.js → chunk-VE4LGNXX.js} +2 -2
  16. package/dist/cli/{chunk-DEXDXDO4.js → chunk-VVK4I6B5.js} +1 -1
  17. package/dist/cli/{daemon-HWAHX4RR.js → daemon-QEJLAIBT.js} +355 -40
  18. package/dist/cli/{hook-LY2YTK5U.js → hook-AE5HC6AS.js} +18 -15
  19. package/dist/cli/index.js +30 -11
  20. package/dist/cli/{launch-UQEFFPBN.js → launch-B4G5OFBV.js} +7 -7
  21. package/dist/cli/send-2OZ3YZRV.js +18 -0
  22. package/dist/cli/{setup-3N2EOJXS.js → setup-2VM4GCDJ.js} +4 -4
  23. package/dist/cli/{status-ZQSYXILE.js → status-3VJAO6UL.js} +3 -3
  24. package/dist/cli/task-PMKNYP6M.js +18 -0
  25. package/dist/cli/tui-DWA2EDC2.js +3561 -0
  26. package/dist/cli/{uninstall-W2OMKU3B.js → uninstall-AKUWE33D.js} +4 -4
  27. package/dist/cli/{version-5DA7G7HN.js → version-WOGCIZY3.js} +1 -1
  28. package/dist/cli/who-37TNDYVA.js +11 -0
  29. package/package.json +7 -1
  30. package/dist/cli/chunk-CBKKOJIG.js +0 -6
  31. package/dist/cli/send-EY63XQXI.js +0 -14
  32. package/dist/cli/who-VGIMGQNS.js +0 -11
@@ -0,0 +1,6 @@
1
+ // src/version.ts
2
+ var LEDGE_VERSION = true ? "0.17.0" : "0.0.0-dev";
3
+
4
+ export {
5
+ LEDGE_VERSION
6
+ };
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  NAMES,
3
3
  pickName
4
- } from "./chunk-Q5WWH7FG.js";
4
+ } from "./chunk-QD27W6W5.js";
5
5
  import {
6
6
  paths
7
- } from "./chunk-R5462SQM.js";
7
+ } from "./chunk-ELLXOV4Q.js";
8
8
 
9
9
  // src/daemon/transcript.ts
10
10
  import { appendFileSync, closeSync, fstatSync, openSync, readFileSync, readSync, statSync } from "fs";
@@ -4,7 +4,7 @@ import {
4
4
  SUMMARY_MAX,
5
5
  parseServerMessage,
6
6
  readDaemonToken
7
- } from "./chunk-Q5WWH7FG.js";
7
+ } from "./chunk-QD27W6W5.js";
8
8
 
9
9
  // src/client.ts
10
10
  import WebSocket from "ws";
@@ -17,18 +17,24 @@ import {
17
17
  dialogMismatch,
18
18
  flatten,
19
19
  formatLedgeMessage,
20
+ formatLedgeNotice,
21
+ formatLedgeTask,
20
22
  gitInfo,
21
23
  gitSummary,
24
+ inline,
22
25
  ledgeBody,
23
26
  ledgeMessages,
27
+ ledgeNotice,
24
28
  ledgePeer,
29
+ ledgeTask,
30
+ mailId,
25
31
  pendingTool,
26
32
  readPeers,
27
33
  reduce,
28
34
  repositoryId,
29
35
  shownCommand,
30
36
  stripNote
31
- } from "./chunk-DBSCO3HI.js";
37
+ } from "./chunk-NC5YAEJB.js";
32
38
  import {
33
39
  PARENT_AGENT_VARS,
34
40
  agentEnv,
@@ -39,7 +45,7 @@ import {
39
45
  labelTranscript,
40
46
  resumeLabel,
41
47
  spawnSession
42
- } from "./chunk-H36JD4E3.js";
48
+ } from "./chunk-VE4LGNXX.js";
43
49
  import {
44
50
  CHAT_HISTORY_BUDGET,
45
51
  DEFAULT_PROJECTS,
@@ -99,30 +105,31 @@ import {
99
105
  proposalRows,
100
106
  pushEndpointAllowed,
101
107
  requestAuthorized,
102
- selectDaemonProtocol
103
- } from "./chunk-Q5WWH7FG.js";
108
+ selectDaemonProtocol,
109
+ taskClosed
110
+ } from "./chunk-QD27W6W5.js";
104
111
  import {
105
112
  LEDGE_VERSION
106
- } from "./chunk-CBKKOJIG.js";
113
+ } from "./chunk-V2H7UD2N.js";
107
114
  import {
108
115
  SERVICE,
109
116
  inDaemonService,
110
117
  tmuxServerAtRisk
111
- } from "./chunk-S4ANPQGA.js";
118
+ } from "./chunk-OVJL2QTN.js";
112
119
  import {
113
120
  realTmux,
114
121
  runFile
115
- } from "./chunk-2H5FJH4Q.js";
122
+ } from "./chunk-LAUNHDTX.js";
116
123
  import {
117
124
  log
118
- } from "./chunk-BNKPXSUY.js";
125
+ } from "./chunk-JQZHXVIZ.js";
119
126
  import {
120
127
  ensureLedgeDir,
121
128
  paths
122
- } from "./chunk-R5462SQM.js";
129
+ } from "./chunk-ELLXOV4Q.js";
123
130
 
124
131
  // src/daemon/index.ts
125
- import { writeFile as writeFile7, rm as rm6 } from "fs/promises";
132
+ import { writeFile as writeFile8, rm as rm6 } from "fs/promises";
126
133
  import { join as join17 } from "path";
127
134
 
128
135
  // src/daemon/omp/owner.ts
@@ -594,6 +601,40 @@ var History = class {
594
601
  });
595
602
  return last;
596
603
  }
604
+ const failed = ledgeNotice(ev.text);
605
+ if (failed) {
606
+ this.turns.set(id, turn + 1);
607
+ const peer = this.peerOf(failed.to);
608
+ return this.put(id, {
609
+ kind: "notice",
610
+ id: `notice-${turn + 1}-0`,
611
+ at: ev.at,
612
+ turn: turn + 1,
613
+ notice: "delivery",
614
+ status: "refused",
615
+ count: 1,
616
+ recipients: [failed.to],
617
+ name: failed.to,
618
+ ...peer ? { peer } : {},
619
+ ...failed.reason ? { reason: failed.reason } : {},
620
+ text: failed.text
621
+ });
622
+ }
623
+ const task = ledgeTask(ev.text);
624
+ if (task) {
625
+ this.turns.set(id, turn + 1);
626
+ const peer = task.from ? this.peerOf(task.from) : void 0;
627
+ return this.put(id, {
628
+ kind: "notice",
629
+ id: `notice-${turn + 1}-0`,
630
+ at: ev.at,
631
+ turn: turn + 1,
632
+ notice: "other",
633
+ ...task.from ? { name: task.from } : {},
634
+ ...peer ? { peer } : {},
635
+ text: task.text
636
+ });
637
+ }
597
638
  this.turns.set(id, turn + 1);
598
639
  return this.put(id, { kind: "prompt", id: `prompt-${turn + 1}`, at: ev.at, turn: turn + 1, text: ev.text });
599
640
  }
@@ -725,15 +766,17 @@ var History = class {
725
766
  }
726
767
  /** Puts a received card, paired with the sender's sent one when the sender is a Ledge session: the last of its
727
768
  * messages to this session not read yet, the one whose text starts like the body if any (the hook may have cut
728
- * either), both read as Ledge types a body (a multi-line one arrives on one line). That sent one becomes read, in the sender's thread. */
769
+ * either), both read as Ledge types a body (a multi-line one arrives on one line). A message the mailbox typed names
770
+ * its id: the sent card of that id is the one, and its text replaces a pointer's. That sent one becomes read, in the sender's thread. */
729
771
  receive(id, recv, m) {
730
772
  const peer = (m.sender ? this.peerOf(m.sender) : void 0) ?? this.peerOf(m.from) ?? (m.name ? this.peerOf(m.name) : void 0);
731
773
  if (!peer) return this.put(id, recv);
732
774
  const head = (t) => ledgeBody(t).trim().slice(0, 200);
733
775
  const open3 = this.items(peer).filter((i) => i.kind === "sent" && i.peer === id && i.status !== "read");
734
- const sent = [...open3].reverse().find((i) => head(i.text) === head(recv.text)) ?? open3.at(-1);
776
+ const byId = m.id ? open3.find((i) => mailId(i.id) === m.id) : void 0;
777
+ const sent = byId ?? [...open3].reverse().find((i) => head(i.text) === head(recv.text)) ?? open3.at(-1);
735
778
  if (sent) this.put(peer, { ...sent, status: "read", received: recv.id });
736
- return this.put(id, { ...recv, peer, ...sent ? { sent: sent.id } : {} });
779
+ return this.put(id, { ...recv, ...byId ? { text: byId.text } : {}, peer, ...sent ? { sent: sent.id } : {} });
737
780
  }
738
781
  /** The turn (or the helper) is over: an action still running will not get its result from this side. It shows as
739
782
  * unknown, not as waiting forever; a result that arrives later still settles it (callFor finds it by id). */
@@ -4145,8 +4188,46 @@ function cardOf(threads, qid) {
4145
4188
  } };
4146
4189
  }
4147
4190
 
4191
+ // src/daemon/journal.ts
4192
+ import { readFileSync as readFileSync4 } from "fs";
4193
+ import { writeFile as writeFile7, rename as rename3 } from "fs/promises";
4194
+ function jsonJournal(path, name, read) {
4195
+ let pending;
4196
+ let writing = Promise.resolve();
4197
+ const drain = async () => {
4198
+ while (pending !== void 0) {
4199
+ const state = pending;
4200
+ pending = void 0;
4201
+ const tmp = `${path}.tmp`;
4202
+ try {
4203
+ await writeFile7(tmp, JSON.stringify(state));
4204
+ await rename3(tmp, path);
4205
+ } catch (e) {
4206
+ log(`${name} journal not written: ${e.message}`);
4207
+ }
4208
+ }
4209
+ };
4210
+ return {
4211
+ load() {
4212
+ try {
4213
+ return read(JSON.parse(readFileSync4(path, "utf8")));
4214
+ } catch {
4215
+ return void 0;
4216
+ }
4217
+ },
4218
+ save(state) {
4219
+ const idle = pending === void 0;
4220
+ pending = state;
4221
+ if (idle) writing = writing.then(drain);
4222
+ },
4223
+ flush: () => writing
4224
+ };
4225
+ }
4226
+
4148
4227
  // src/daemon/mailbox.ts
4149
4228
  var MAX_SEEN = 1e3;
4229
+ var BODY_MAX = 200;
4230
+ var BODY_TTL_MS = 7 * 24 * 3600 * 1e3;
4150
4231
  var PASSING = [
4151
4232
  /^a decision is already in progress$/,
4152
4233
  /^an? \w+ prompt takes accept or deny, not a reply$/,
@@ -4154,34 +4235,58 @@ var PASSING = [
4154
4235
  /^free-text answers are not supported yet\b/,
4155
4236
  /^the request changed, look again$/
4156
4237
  ];
4238
+ var GONE = "la session n'existe plus";
4239
+ var ENDED = "la session s'est termin\xE9e avant de le recevoir";
4240
+ var reachable = (s) => !!s && s.source === "launcher" && s.state !== "exited" && !isArchived(s) && (!!s.tmuxSession || backendOf(s) === "omp-rpc");
4157
4241
  function createMailbox(d) {
4158
4242
  const now = d.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
4159
4243
  const queue = [];
4160
4244
  const seen = /* @__PURE__ */ new Map();
4245
+ const bodies = /* @__PURE__ */ new Map();
4246
+ const save = () => {
4247
+ const oldest = Date.parse(now()) - BODY_TTL_MS;
4248
+ for (const [id, b] of bodies) if (Date.parse(b.at) < oldest || bodies.size > BODY_MAX) bodies.delete(id);
4249
+ d.journal?.save({ queue: [...queue], bodies: [...bodies.values()] });
4250
+ };
4161
4251
  const ready = (id) => free(d.store.get(id)) && !d.typing.has(id) && !d.closing?.has(id) && !d.relayQueued(id);
4252
+ function fail2(m, reason) {
4253
+ log(`${m.kind} ${m.rid} to ${m.target} failed: ${reason}`);
4254
+ if (m.kind === "notice") return;
4255
+ d.sent.mark(m.from, m.rid, "failed");
4256
+ const sender = d.store.get(m.from);
4257
+ if (!reachable(sender)) return;
4258
+ notice(sender, formatLedgeNotice({ to: m.to, text: m.text, reason }), `${m.rid}:notice`);
4259
+ }
4260
+ function notice(s, text, rid) {
4261
+ queue.push({ kind: "notice", id: mailId(rid), rid, from: "", sender: { name: "Ledge", tool: s.tool }, target: s.id, to: s.name, text });
4262
+ queueMicrotask(() => kick(s.id));
4263
+ }
4162
4264
  async function deliver(m) {
4163
4265
  const s = d.store.get(m.target);
4164
4266
  if (!s) {
4165
- d.sent.mark(m.from, m.rid, "failed");
4267
+ fail2(m, GONE);
4268
+ save();
4166
4269
  queueMicrotask(() => kick(m.target));
4167
4270
  return;
4168
4271
  }
4169
4272
  d.typing.add(m.target);
4170
4273
  try {
4171
4274
  let refused;
4275
+ 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 } : {} });
4172
4276
  try {
4173
- refused = await d.replyTo(m.target, formatLedgeMessage({ from: m.sender.name, tool: m.sender.tool, text: m.text, to: s.tool }));
4277
+ refused = await d.replyTo(m.target, text);
4174
4278
  } catch (e) {
4175
4279
  refused = e.message;
4176
4280
  }
4177
4281
  const live2 = d.store.get(m.target);
4178
4282
  if (refused && PASSING.some((re) => re.test(refused)) && live2 && live2.state !== "exited") {
4179
- log(`message ${m.rid} to ${m.target} waits: ${refused}`);
4283
+ log(`${m.kind} ${m.rid} to ${m.target} waits: ${refused}`);
4180
4284
  queue.unshift(m);
4181
4285
  return;
4182
4286
  }
4183
- if (refused) log(`message ${m.rid} to ${m.target} failed: ${refused}`);
4184
- d.sent.mark(m.from, m.rid, refused ? "failed" : "delivered");
4287
+ if (refused) fail2(m, refused);
4288
+ else if (m.kind === "message") d.sent.mark(m.from, m.rid, "delivered");
4289
+ save();
4185
4290
  if (refused) queueMicrotask(() => kick(m.target));
4186
4291
  } finally {
4187
4292
  d.typing.delete(m.target);
@@ -4192,18 +4297,22 @@ function createMailbox(d) {
4192
4297
  const i = queue.findIndex((m2) => m2.target === id);
4193
4298
  if (i < 0) return;
4194
4299
  const [m] = queue.splice(i, 1);
4195
- void deliver(m).catch((e) => log(`message ${m.rid} failed: ${e.message}`));
4300
+ void deliver(m).catch((e) => log(`${m.kind} ${m.rid} failed: ${e.message}`));
4301
+ }
4302
+ function failFor(id, reason) {
4303
+ let changed = false;
4304
+ for (let i = queue.length - 1; i >= 0; i--) if (queue[i].target === id) {
4305
+ const [m] = queue.splice(i, 1);
4306
+ fail2(m, reason);
4307
+ changed = true;
4308
+ }
4309
+ if (changed) save();
4196
4310
  }
4197
4311
  const unsubscribe = d.store.onChange((c) => {
4198
4312
  if (c.type === "recents") return;
4199
4313
  const id = c.type === "updated" ? c.session.id : c.id;
4200
- if (c.type === "removed" || c.session.state === "exited") {
4201
- for (let i = queue.length - 1; i >= 0; i--) if (queue[i].target === id) {
4202
- const [m] = queue.splice(i, 1);
4203
- d.sent.mark(m.from, m.rid, "failed");
4204
- }
4205
- return;
4206
- }
4314
+ if (c.type === "removed") return failFor(id, GONE);
4315
+ if (c.session.state === "exited") return failFor(id, ENDED);
4207
4316
  kick(id);
4208
4317
  });
4209
4318
  const target = (to) => {
@@ -4215,6 +4324,30 @@ function createMailbox(d) {
4215
4324
  if (seen.size > MAX_SEEN) seen.delete(seen.keys().next().value);
4216
4325
  return ack;
4217
4326
  };
4327
+ const idFor = (rid) => {
4328
+ for (const length of [6, 8, 10, 40]) {
4329
+ const id = mailId(rid, length);
4330
+ const held = bodies.get(id);
4331
+ if (!held || held.rid === rid) return id;
4332
+ }
4333
+ return mailId(rid, 40);
4334
+ };
4335
+ const loaded = d.journal?.load();
4336
+ if (loaded) {
4337
+ for (const b of loaded.bodies) bodies.set(b.id, b);
4338
+ const failed = [];
4339
+ for (const m of loaded.queue) {
4340
+ const t = d.store.get(m.target);
4341
+ if (t && t.state !== "exited") queue.push(m);
4342
+ else failed.push([m, t ? ENDED : GONE]);
4343
+ }
4344
+ for (const [m, reason] of failed) fail2(m, reason);
4345
+ if (failed.length) save();
4346
+ if (queue.length) log(`mailbox: ${queue.length} message(s) still waiting from the last run`);
4347
+ queueMicrotask(() => {
4348
+ for (const id of new Set(queue.map((m) => m.target))) kick(id);
4349
+ });
4350
+ }
4218
4351
  return {
4219
4352
  submit(m) {
4220
4353
  const first = seen.get(m.rid);
@@ -4226,17 +4359,188 @@ function createMailbox(d) {
4226
4359
  const sender = d.store.get(m.from);
4227
4360
  if (!sender) return refuse("session \xE9mettrice inconnue");
4228
4361
  if (t.source === "hook_only" || !t.tmuxSession && backendOf(t) !== "omp-rpc") return refuse(`${t.name} n'a pas \xE9t\xE9 lanc\xE9e par Ledge : impossible de lui \xE9crire`);
4229
- d.sent.record(m.from, { rid: m.rid, at: now(), to: t.name, peer: t.id, text: m.text, ...m.summary ? { summary: m.summary } : {} });
4362
+ const at = now();
4363
+ const id = idFor(m.rid);
4364
+ d.sent.record(m.from, { rid: m.rid, at, to: t.name, peer: t.id, text: m.text, ...m.summary ? { summary: m.summary } : {} });
4365
+ if (!inline(m.text)) bodies.set(id, { id, rid: m.rid, from: sender.id, fromName: sender.name, target: t.id, to: t.name, at, text: m.text });
4230
4366
  const outcome = ready(t.id) ? "accepted" : "queued";
4231
- queue.push({ rid: m.rid, from: m.from, sender: { name: sender.name, tool: sender.tool }, target: t.id, text: m.text });
4367
+ queue.push({ kind: "message", id, rid: m.rid, from: m.from, sender: { name: sender.name, tool: sender.tool }, target: t.id, to: t.name, text: m.text, ...m.summary ? { summary: m.summary } : {} });
4368
+ save();
4232
4369
  const ack = remember(m.rid, { type: "message.ack", rid: m.rid, outcome });
4233
4370
  if (outcome === "accepted") kick(t.id);
4234
4371
  return ack;
4235
4372
  },
4373
+ read({ id, by }) {
4374
+ const b = bodies.get(id);
4375
+ if (!b) return { type: "message.body", id, reason: `aucun message ${id} (seuls les longs messages des 7 derniers jours se relisent)` };
4376
+ if (by !== b.from && by !== b.target) return { type: "message.body", id, reason: `le message ${id} ne t'est pas adress\xE9` };
4377
+ return { type: "message.body", id, from: b.fromName, to: b.to, at: b.at, text: b.text };
4378
+ },
4379
+ notify(target2, text, rid) {
4380
+ const s = d.store.get(target2);
4381
+ if (!reachable(s)) return false;
4382
+ notice(s, text, rid);
4383
+ save();
4384
+ return true;
4385
+ },
4236
4386
  kick,
4237
4387
  close: unsubscribe
4238
4388
  };
4239
4389
  }
4390
+ function fileJournal(path) {
4391
+ return jsonJournal(path, "mailbox", (raw) => {
4392
+ const r = raw;
4393
+ return r ? { queue: Array.isArray(r.queue) ? r.queue : [], bodies: Array.isArray(r.bodies) ? r.bodies : [] } : void 0;
4394
+ });
4395
+ }
4396
+
4397
+ // src/daemon/tasks.ts
4398
+ import { randomBytes as randomBytes2 } from "crypto";
4399
+ var CLOSED_TTL_MS = 7 * 24 * 3600 * 1e3;
4400
+ var CLOSED_MAX = 300;
4401
+ var QUOTE = 200;
4402
+ var quote = (s) => {
4403
+ const flat = s.trim().replace(/\s+/g, " ");
4404
+ return flat.length > QUOTE ? `${flat.slice(0, QUOTE)}\u2026` : flat;
4405
+ };
4406
+ var reachable2 = (s) => !!s && s.source === "launcher" && s.state !== "exited" && !isArchived(s) && (!!s.tmuxSession || backendOf(s) === "omp-rpc");
4407
+ function createTasks(d) {
4408
+ const now = d.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
4409
+ const tasks = /* @__PURE__ */ new Map();
4410
+ for (const t of d.journal?.load() ?? []) tasks.set(t.id, t);
4411
+ const save = () => {
4412
+ const oldest = Date.parse(now()) - CLOSED_TTL_MS;
4413
+ const closed = [...tasks.values()].filter(taskClosed);
4414
+ for (const t of closed) if (Date.parse(t.updatedAt) < oldest) tasks.delete(t.id);
4415
+ const left = closed.filter((t) => tasks.has(t.id));
4416
+ for (const t of left.slice(0, Math.max(0, left.length - CLOSED_MAX))) tasks.delete(t.id);
4417
+ d.journal?.save([...tasks.values()]);
4418
+ };
4419
+ const newId = () => {
4420
+ for (; ; ) {
4421
+ const id = `t${randomBytes2(3).toString("hex")}`;
4422
+ if (!tasks.has(id)) return id;
4423
+ }
4424
+ };
4425
+ const target = (to) => {
4426
+ const name = bareName(to);
4427
+ return d.store.all().find((s) => s.name === name && s.state !== "exited" && !isArchived(s));
4428
+ };
4429
+ const put = (t, by, what) => {
4430
+ const at = now();
4431
+ const next2 = { ...t, updatedAt: at, log: [...t.log, { at, by, what }] };
4432
+ tasks.set(t.id, next2);
4433
+ save();
4434
+ return next2;
4435
+ };
4436
+ const tellOwner = (t, from, lead) => d.mailbox.notify(t.owner, formatLedgeTask({
4437
+ id: t.id,
4438
+ from,
4439
+ text: `${lead} \xAB ${t.title} \xBB. Lis-la : ledge task show ${t.id} ; prends-la : ledge task take ${t.id} ; \xE0 la fin : ledge task done ${t.id} "r\xE9sultat" (ou ledge task refuse ${t.id} "raison").`
4440
+ }), `${t.id}:${t.log.length}`);
4441
+ const tellGiver = (t, from, what) => d.mailbox.notify(t.giver, formatLedgeTask({
4442
+ id: t.id,
4443
+ from,
4444
+ text: `T\xE2che \xAB ${t.title} \xBB ${what} (ledge task show ${t.id})`
4445
+ }), `${t.id}:${t.log.length}`);
4446
+ const dropFor = (id) => {
4447
+ for (const t of [...tasks.values()]) {
4448
+ if (t.owner !== id || taskClosed(t)) continue;
4449
+ const dropped = put({ ...t, state: "dropped" }, "Ledge", `${t.ownerName} s'est termin\xE9e sans la finir`);
4450
+ log(`task ${t.id} dropped: ${t.ownerName} ended`);
4451
+ tellGiver(dropped, "Ledge", `abandonn\xE9e : ${t.ownerName} s'est termin\xE9e sans la finir`);
4452
+ }
4453
+ };
4454
+ for (const owner of new Set([...tasks.values()].filter((t) => !taskClosed(t)).map((t) => t.owner))) {
4455
+ const s = d.store.get(owner);
4456
+ if (!s || s.state === "exited") dropFor(owner);
4457
+ }
4458
+ const unsubscribe = d.store.onChange((c) => {
4459
+ if (c.type === "recents") return;
4460
+ if (c.type === "removed") return dropFor(c.id);
4461
+ if (c.session.state === "exited") dropFor(c.session.id);
4462
+ });
4463
+ function run2(cmd) {
4464
+ const refuse = (reason) => ({ type: "task.result", rid: cmd.rid, reason });
4465
+ const ok = (task) => ({ type: "task.result", rid: cmd.rid, task });
4466
+ const me = d.store.get(cmd.by);
4467
+ if (!me) return refuse("session inconnue");
4468
+ if (cmd.op === "list") {
4469
+ const mine = [...tasks.values()].filter((t2) => (t2.owner === me.id || t2.giver === me.id) && (cmd.all || !taskClosed(t2)));
4470
+ return { type: "task.result", rid: cmd.rid, tasks: mine.sort((a, b) => a.at.localeCompare(b.at)) };
4471
+ }
4472
+ if (cmd.op === "give") {
4473
+ if (!cmd.to || !cmd.title) return refuse("il faut un destinataire et un titre");
4474
+ const t2 = target(cmd.to);
4475
+ if (!t2) return refuse(`aucune session Ledge nomm\xE9e ${cmd.to}`);
4476
+ if (t2.id === me.id) return refuse("tu ne peux pas te confier une t\xE2che");
4477
+ if (!reachable2(t2)) return refuse(`${t2.name} n'a pas \xE9t\xE9 lanc\xE9e par Ledge : impossible de lui confier une t\xE2che`);
4478
+ const at = now();
4479
+ const task = {
4480
+ id: newId(),
4481
+ title: cmd.title.trim(),
4482
+ ...cmd.text?.trim() ? { text: cmd.text.trim() } : {},
4483
+ giver: me.id,
4484
+ giverName: me.name,
4485
+ owner: t2.id,
4486
+ ownerName: t2.name,
4487
+ state: "open",
4488
+ at,
4489
+ updatedAt: at,
4490
+ log: [{ at, by: me.name, what: `confi\xE9e \xE0 ${t2.name}` }]
4491
+ };
4492
+ tasks.set(task.id, task);
4493
+ save();
4494
+ tellOwner(task, me.name, `T\xE2che de ${me.name} :`);
4495
+ return ok(task);
4496
+ }
4497
+ if (!cmd.task) return refuse("il faut l'id de la t\xE2che");
4498
+ const t = tasks.get(cmd.task);
4499
+ if (!t) return refuse(`aucune t\xE2che ${cmd.task}`);
4500
+ const owner = t.owner === me.id;
4501
+ const giver = t.giver === me.id;
4502
+ if (cmd.op === "show") return owner || giver ? ok(t) : refuse(`la t\xE2che ${t.id} n'est ni \xE0 toi ni de toi`);
4503
+ if (taskClosed(t)) return refuse(`la t\xE2che ${t.id} est d\xE9j\xE0 close (${t.state})`);
4504
+ switch (cmd.op) {
4505
+ case "take":
4506
+ if (!owner) return refuse(`la t\xE2che ${t.id} est \xE0 ${t.ownerName}, pas \xE0 toi`);
4507
+ return ok(t.state === "taken" ? t : put({ ...t, state: "taken" }, me.name, "prise"));
4508
+ case "done": {
4509
+ if (!owner) return refuse(`seule la session qui la tient (${t.ownerName}) peut la terminer`);
4510
+ const result = cmd.text?.trim();
4511
+ const done = put({ ...t, state: "done", ...result ? { result } : {} }, me.name, result ? `termin\xE9e : ${result}` : "termin\xE9e");
4512
+ tellGiver(done, me.name, `termin\xE9e par ${me.name}${result ? ` : ${quote(result)}` : ""}`);
4513
+ return ok(done);
4514
+ }
4515
+ case "refuse": {
4516
+ if (!owner && !giver) return refuse(`la t\xE2che ${t.id} n'est ni \xE0 toi ni de toi`);
4517
+ const reason = cmd.text?.trim();
4518
+ if (!reason) return refuse("il faut une raison");
4519
+ const refused = put({ ...t, state: "refused", result: reason }, me.name, `${owner ? "refus\xE9e" : "annul\xE9e"} : ${reason}`);
4520
+ if (owner) tellGiver(refused, me.name, `refus\xE9e par ${me.name} : ${quote(reason)}`);
4521
+ if (giver && !owner) d.mailbox.notify(t.owner, formatLedgeTask({ id: t.id, from: me.name, text: `T\xE2che \xAB ${t.title} \xBB annul\xE9e par ${me.name} : ${quote(reason)}. Arr\xEAte d'y travailler.` }), `${t.id}:${refused.log.length}`);
4522
+ return ok(refused);
4523
+ }
4524
+ case "pass": {
4525
+ if (!owner) return refuse(`seule la session qui la tient (${t.ownerName}) peut la passer`);
4526
+ if (!cmd.to) return refuse("il faut le nom de la session \xE0 qui la passer");
4527
+ const next2 = target(cmd.to);
4528
+ if (!next2) return refuse(`aucune session Ledge nomm\xE9e ${cmd.to}`);
4529
+ if (next2.id === me.id) return refuse("elle est d\xE9j\xE0 \xE0 toi");
4530
+ if (!reachable2(next2)) return refuse(`${next2.name} n'a pas \xE9t\xE9 lanc\xE9e par Ledge : impossible de lui passer une t\xE2che`);
4531
+ const note = cmd.text?.trim();
4532
+ const passed = put({ ...t, owner: next2.id, ownerName: next2.name, state: "open" }, me.name, `pass\xE9e \xE0 ${next2.name}${note ? ` : ${note}` : ""}`);
4533
+ tellOwner(passed, me.name, `T\xE2che pass\xE9e par ${me.name}${note ? ` (${quote(note)})` : ""}, confi\xE9e par ${t.giverName} :`);
4534
+ return ok(passed);
4535
+ }
4536
+ }
4537
+ return refuse("op\xE9ration inconnue");
4538
+ }
4539
+ return { run: run2, close: unsubscribe };
4540
+ }
4541
+ function tasksJournal(path) {
4542
+ return jsonJournal(path, "tasks", (raw) => Array.isArray(raw) ? raw : void 0);
4543
+ }
4240
4544
 
4241
4545
  // src/chat/routing.ts
4242
4546
  import { randomUUID as randomUUID2 } from "crypto";
@@ -4947,7 +5251,7 @@ function capabilities() {
4947
5251
  }
4948
5252
 
4949
5253
  // src/mobile/challenges.ts
4950
- import { randomBytes as randomBytes2 } from "crypto";
5254
+ import { randomBytes as randomBytes3 } from "crypto";
4951
5255
  var CHALLENGE_TTL_MS = 6e4;
4952
5256
  var MAX_OUTSTANDING = 1e3;
4953
5257
  function createChallengeStore(opts = {}) {
@@ -4966,7 +5270,7 @@ function createChallengeStore(opts = {}) {
4966
5270
  const slot2 = slotOf(b);
4967
5271
  const previous = slots.get(slot2);
4968
5272
  if (previous !== void 0) pending.delete(previous);
4969
- const challenge = randomBytes2(32).toString("base64url");
5273
+ const challenge = randomBytes3(32).toString("base64url");
4970
5274
  pending.set(challenge, { binding: { deviceId: b.deviceId, purpose: b.purpose, subject: b.subject }, issuedAt: now(), slot: slot2 });
4971
5275
  slots.set(slot2, challenge);
4972
5276
  while (pending.size > max) {
@@ -5586,13 +5890,13 @@ function createPhoneGateway(deps) {
5586
5890
  }
5587
5891
 
5588
5892
  // src/mobile/push/encrypt.ts
5589
- import { createCipheriv, createECDH, hkdfSync, randomBytes as randomBytes3 } from "crypto";
5893
+ import { createCipheriv, createECDH, hkdfSync, randomBytes as randomBytes4 } from "crypto";
5590
5894
  var RS = 4096;
5591
5895
  var expand = (ikm, salt, info, len) => Buffer.from(hkdfSync("sha256", ikm, salt, Buffer.from(info, "latin1"), len));
5592
5896
  function encryptPush(plain, sub, opts = {}) {
5593
5897
  const uaPublic = Buffer.from(sub.p256dh, "base64url");
5594
5898
  const authSecret = Buffer.from(sub.auth, "base64url");
5595
- const salt = Buffer.from(opts.salt ?? randomBytes3(16));
5899
+ const salt = Buffer.from(opts.salt ?? randomBytes4(16));
5596
5900
  if (salt.length !== 16) throw new Error("salt must be 16 octets");
5597
5901
  if (uaPublic.length !== 65 || uaPublic[0] !== 4) throw new Error("p256dh is not an uncompressed P-256 point");
5598
5902
  const eph = createECDH("prime256v1");
@@ -5628,7 +5932,7 @@ function encryptPush(plain, sub, opts = {}) {
5628
5932
  // src/mobile/push/triggers.ts
5629
5933
  var TAG_MAX = 32;
5630
5934
  var TITLE_MAX = 80;
5631
- var BODY_MAX = 120;
5935
+ var BODY_MAX2 = 120;
5632
5936
  var GENERIC_REASONS = /* @__PURE__ */ new Set(["Allow the requested shell command?"]);
5633
5937
  var safeTag = (s) => s.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, TAG_MAX);
5634
5938
  function cut(s, n) {
@@ -5652,11 +5956,11 @@ function pushFor(before, after, all) {
5652
5956
  const p = after.pending;
5653
5957
  if (p && (!before?.pending || pendingKey(p) !== pendingKey(before.pending))) {
5654
5958
  const url = `/#/request/${encodeURIComponent(after.id)}/${encodeURIComponent(pendingKey(p))}`;
5655
- if (p.kind === "question") return { v: 1, kind: "question", title: cut(`${after.name} a une question`, TITLE_MAX), body: cut(p.text.trim(), BODY_MAX), url, badge, tag };
5656
- return { v: 1, kind: "request", title: cut(`${after.name} attend votre accord`, TITLE_MAX), body: cut(requestBody(after), BODY_MAX), url, badge, tag };
5959
+ if (p.kind === "question") return { v: 1, kind: "question", title: cut(`${after.name} a une question`, TITLE_MAX), body: cut(p.text.trim(), BODY_MAX2), url, badge, tag };
5960
+ return { v: 1, kind: "request", title: cut(`${after.name} attend votre accord`, TITLE_MAX), body: cut(requestBody(after), BODY_MAX2), url, badge, tag };
5657
5961
  }
5658
5962
  if (before?.state === "working" && after.state === "idle" && !p) {
5659
- return { v: 1, kind: "finished", title: cut(`${after.name} a termin\xE9`, TITLE_MAX), body: cut((after.lastReply?.text ?? "").trim(), BODY_MAX), url: `/#/thread/${encodeURIComponent(after.id)}`, badge, tag };
5963
+ return { v: 1, kind: "finished", title: cut(`${after.name} a termin\xE9`, TITLE_MAX), body: cut((after.lastReply?.text ?? "").trim(), BODY_MAX2), url: `/#/thread/${encodeURIComponent(after.id)}`, badge, tag };
5660
5964
  }
5661
5965
  return null;
5662
5966
  }
@@ -5906,12 +6210,14 @@ async function startServer(opts) {
5906
6210
  typing,
5907
6211
  closing,
5908
6212
  now,
6213
+ ...opts.mail ? { journal: opts.mail } : {},
5909
6214
  relayQueued: (id) => cardThreads.queuedRelays().some((q) => q.relay.id === id),
5910
6215
  sent: { record: (id, m) => {
5911
6216
  history?.recordSent(id, m);
5912
6217
  }, mark: (id, rid, status) => history?.markSent(id, rid, status) }
5913
6218
  });
5914
6219
  const relay = createRelay({ threads: cardThreads, store, replyTo: replyText, now, typing, closing, next: (id) => mailbox.kick(id) });
6220
+ const tasks = createTasks({ store, mailbox, now, ...opts.tasks ? { journal: opts.tasks } : {} });
5915
6221
  const previews = /* @__PURE__ */ new Map();
5916
6222
  const titles = /* @__PURE__ */ new Map();
5917
6223
  const labelled = /* @__PURE__ */ new Map();
@@ -6583,6 +6889,10 @@ async function startServer(opts) {
6583
6889
  // An agent's message to another session: acked at once, typed when the target is free.
6584
6890
  case "session.message":
6585
6891
  return reply(mailbox.submit(msg));
6892
+ case "message.read":
6893
+ return reply(mailbox.read(msg));
6894
+ case "task.cmd":
6895
+ return reply(tasks.run(msg));
6586
6896
  // A chat card's instruction: a refusal reaches the requester as an `error`, the card's state everyone through chat.history.
6587
6897
  case "session.relay":
6588
6898
  return relay.request(msg.qid);
@@ -6777,6 +7087,7 @@ async function startServer(opts) {
6777
7087
  gateway?.stop();
6778
7088
  relay.close();
6779
7089
  mailbox.close();
7090
+ tasks.close();
6780
7091
  void omp.closeAll();
6781
7092
  for (const c of wss.clients) c.terminate();
6782
7093
  for (const socket of sockets) socket.destroy();
@@ -6878,7 +7189,7 @@ ${output}` } : { code: r.code, output };
6878
7189
  }
6879
7190
 
6880
7191
  // src/mobile/files.ts
6881
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, renameSync as renameSync4, chmodSync, unlinkSync } from "fs";
7192
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync as renameSync4, chmodSync, unlinkSync } from "fs";
6882
7193
  import { z } from "zod";
6883
7194
 
6884
7195
  // ../seal/src/errors.ts
@@ -7162,7 +7473,7 @@ function writePrivate(file, content) {
7162
7473
  }
7163
7474
  function readText(file, name) {
7164
7475
  try {
7165
- return readFileSync4(file, "utf8");
7476
+ return readFileSync5(file, "utf8");
7166
7477
  } catch (e) {
7167
7478
  if (e.code === "ENOENT") return null;
7168
7479
  throw new Error(`${name} cannot be read (${e.code ?? "error"})`);
@@ -8288,10 +8599,12 @@ async function startDaemon(opts = {}) {
8288
8599
  log(`mobile access not available: ${e.message}`);
8289
8600
  mobileUnavailable = e.message.slice(0, 200);
8290
8601
  }
8291
- const server = await startServer({ store, tmux, port: opts.port, daemon, updater, history, mobile, mobileUnavailable });
8602
+ const mail = fileJournal(paths().mail);
8603
+ const taskJournal = tasksJournal(paths().tasks);
8604
+ const server = await startServer({ store, tmux, port: opts.port, daemon, updater, history, mobile, mobileUnavailable, mail, tasks: taskJournal });
8292
8605
  ompLive = () => server.omp.liveCount();
8293
8606
  const poller = startPoller({ store, tmux, broadcastPreview: server.preview, labelExited: server.labelExited });
8294
- await writeFile7(paths().pid, String(process.pid));
8607
+ await writeFile8(paths().pid, String(process.pid));
8295
8608
  log(`daemon started on ${server.port} with ${store.all().length} sessions`);
8296
8609
  let stopping = null;
8297
8610
  const stop = () => stopping ??= (async () => {
@@ -8302,6 +8615,8 @@ async function startDaemon(opts = {}) {
8302
8615
  await mobile?.stop();
8303
8616
  await store.save();
8304
8617
  await history.flush();
8618
+ await mail.flush();
8619
+ await taskJournal.flush();
8305
8620
  await rm6(paths().pid, { force: true });
8306
8621
  log("daemon stopped");
8307
8622
  })();