@paradow/ledge 0.6.0 → 0.8.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-FDMNVXYR.js → attach-KTZOG7E5.js} +4 -4
- package/dist/cli/{bridge-AFAK3FU5.js → bridge-QKQW7UD6.js} +1 -1
- package/dist/cli/chunk-3KKTSYHP.js +6 -0
- package/dist/cli/{chunk-Z4YH2DLP.js → chunk-6ZKNEJND.js} +1 -1
- package/dist/cli/{chunk-QJG7M6BJ.js → chunk-7AKUPFMS.js} +3 -3
- package/dist/cli/{chunk-AEV7ODMZ.js → chunk-KJWMK65Y.js} +31 -5
- package/dist/cli/{chunk-VK2IBMW7.js → chunk-Y5TCZTFL.js} +38 -2
- package/dist/cli/{chunk-HYB7ZZN4.js → chunk-ZPANTZKC.js} +3 -3
- package/dist/cli/{daemon-SKI3UKOJ.js → daemon-SRNTDIZW.js} +244 -22
- package/dist/cli/{hook-CT5OKGHQ.js → hook-BBVUY4RK.js} +10 -8
- package/dist/cli/index.js +7 -7
- package/dist/cli/{launch-LVMFX3KF.js → launch-RWT4EWWI.js} +4 -4
- package/dist/cli/{status-TACDR6DZ.js → status-L2GROM4J.js} +2 -2
- package/dist/cli/{version-3CHJ23IV.js → version-M6IZZGUV.js} +1 -1
- package/package.json +1 -1
- package/dist/cli/chunk-FFVZIXKG.js +0 -6
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
attachEnv
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-7AKUPFMS.js";
|
|
4
|
+
import "./chunk-ZPANTZKC.js";
|
|
5
5
|
import {
|
|
6
6
|
realTmux
|
|
7
7
|
} from "./chunk-3TNPEAMR.js";
|
|
8
8
|
import "./chunk-L6IGH4LF.js";
|
|
9
9
|
import {
|
|
10
10
|
connect
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-6ZKNEJND.js";
|
|
12
12
|
import {
|
|
13
13
|
PROTOCOL_PORT
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-KJWMK65Y.js";
|
|
15
15
|
|
|
16
16
|
// src/cli/attach.ts
|
|
17
17
|
import { spawn } from "child_process";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
exitFile,
|
|
3
3
|
spawnSession
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-ZPANTZKC.js";
|
|
5
5
|
import {
|
|
6
6
|
realTmux
|
|
7
7
|
} from "./chunk-3TNPEAMR.js";
|
|
@@ -11,10 +11,10 @@ import {
|
|
|
11
11
|
} from "./chunk-L6IGH4LF.js";
|
|
12
12
|
import {
|
|
13
13
|
connect
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-6ZKNEJND.js";
|
|
15
15
|
import {
|
|
16
16
|
PROTOCOL_PORT
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-KJWMK65Y.js";
|
|
18
18
|
|
|
19
19
|
// src/cli/launch.ts
|
|
20
20
|
import { spawn } from "child_process";
|
|
@@ -30,7 +30,9 @@ var HelperSchema = z.object({
|
|
|
30
30
|
id: z.string(),
|
|
31
31
|
kind: z.string(),
|
|
32
32
|
task: z.string(),
|
|
33
|
-
state: SessionStateSchema
|
|
33
|
+
state: SessionStateSchema,
|
|
34
|
+
/** When this helper finished; retained across daemon restarts until its five-minute expiry. */
|
|
35
|
+
finishedAt: z.string().datetime().optional()
|
|
34
36
|
});
|
|
35
37
|
var TopicSourceSchema = z.enum(["user", "pane", "prompt"]);
|
|
36
38
|
var SessionSchema = z.object({
|
|
@@ -59,6 +61,8 @@ var SessionSchema = z.object({
|
|
|
59
61
|
pending: PendingSchema.optional(),
|
|
60
62
|
helpers: z.array(HelperSchema).default([]),
|
|
61
63
|
lastReply: ReplySchema.optional(),
|
|
64
|
+
/** The last message this session sent to another agent: its tool_use_id, the address, the peer's Ledge session id, a one-line summary. */
|
|
65
|
+
lastMessage: z.object({ id: z.string(), to: z.string(), peer: z.string().optional(), summary: z.string(), at: z.string().datetime() }).optional(),
|
|
62
66
|
startedAt: z.string().datetime(),
|
|
63
67
|
lastActivityAt: z.string().datetime(),
|
|
64
68
|
lastHookAt: z.string().datetime().optional(),
|
|
@@ -69,8 +73,15 @@ var SessionSchema = z.object({
|
|
|
69
73
|
exitCode: z.number().int().nullable().optional(),
|
|
70
74
|
exitedAt: z.string().datetime().optional(),
|
|
71
75
|
lastError: z.string().optional(),
|
|
72
|
-
source: z.enum(["launcher", "hook_only"])
|
|
76
|
+
source: z.enum(["launcher", "hook_only"]),
|
|
77
|
+
/** When the session was put in the archive (session management): set, it leaves the live list and waits to be restored, resumed or deleted. */
|
|
78
|
+
archivedAt: z.string().datetime().optional(),
|
|
79
|
+
/** When the session was last taken out of the archive: the poller's retirement clock restarts from it, not from `exitedAt`. */
|
|
80
|
+
restoredAt: z.string().datetime().optional()
|
|
73
81
|
});
|
|
82
|
+
function isArchived(s) {
|
|
83
|
+
return s.archivedAt !== void 0;
|
|
84
|
+
}
|
|
74
85
|
|
|
75
86
|
// ../protocol/src/chat.ts
|
|
76
87
|
import { z as z2 } from "zod";
|
|
@@ -324,7 +335,11 @@ var ThreadItemSchema = z11.discriminatedUnion("kind", [
|
|
|
324
335
|
/** `unknown`: answered in the terminal, the daemon only saw the prompt go away; `always`: allowed and not asked again. */
|
|
325
336
|
z11.object({ kind: z11.literal("permission"), ...base, tool: z11.string(), text: z11.string(), decision: z11.enum(["accept", "deny", "always", "unknown"]).optional() }),
|
|
326
337
|
z11.object({ kind: z11.literal("reply"), ...base, text: z11.string() }),
|
|
327
|
-
z11.object({ kind: z11.literal("helper"), ...base, task: z11.string(), agentType: z11.string().optional(), state: z11.enum(["working", "exited"]) })
|
|
338
|
+
z11.object({ kind: z11.literal("helper"), ...base, task: z11.string(), agentType: z11.string().optional(), state: z11.enum(["working", "exited"]) }),
|
|
339
|
+
/** A message this session sent to another agent; `id` is the tool_use_id, `to` the address it used, `peer` the Ledge session id, `received` the paired item's id in the peer's thread. */
|
|
340
|
+
z11.object({ kind: z11.literal("sent"), ...base, to: z11.string(), peer: z11.string().optional(), summary: z11.string().optional(), text: z11.string(), status: z11.enum(["sending", "delivered", "read", "failed"]), received: z11.string().optional() }),
|
|
341
|
+
/** A message another agent sent to this session; `from` is its from-name (else its address), `sent` the paired item's id in the peer's thread. */
|
|
342
|
+
z11.object({ kind: z11.literal("received"), ...base, from: z11.string(), peer: z11.string().optional(), text: z11.string(), sent: z11.string().optional() })
|
|
328
343
|
]);
|
|
329
344
|
|
|
330
345
|
// ../protocol/src/decisions.ts
|
|
@@ -388,11 +403,13 @@ var SessionEventSchema = z13.object({
|
|
|
388
403
|
/** The session's transcript, or the helper's on SubagentStop; the daemon reads its tail for the agent's messages. */
|
|
389
404
|
transcriptPath: z13.string().optional(),
|
|
390
405
|
/** Decided: what was chosen when the daemon knows it. */
|
|
391
|
-
decision: DecisionSchema.optional()
|
|
406
|
+
decision: DecisionSchema.optional(),
|
|
407
|
+
/** The agent's own session id when the hook names it, so the daemon can resume that conversation. */
|
|
408
|
+
agentSessionId: z13.string().optional()
|
|
392
409
|
});
|
|
393
410
|
var RecentSchema = z13.object({ cwd: z13.string().min(1), project: z13.string().min(1), lastAt: z13.string().datetime() });
|
|
394
411
|
var SEMVER = /^\d+\.\d+\.\d+$/;
|
|
395
|
-
var DaemonInfoSchema = z13.object({ version: z13.string(), install: z13.enum(["npm", "git", "unknown"]), systemd: z13.boolean(), manual: z13.string().optional(), routing: z13.boolean().optional() });
|
|
412
|
+
var DaemonInfoSchema = z13.object({ version: z13.string(), install: z13.enum(["npm", "git", "unknown"]), systemd: z13.boolean(), manual: z13.string().optional(), routing: z13.boolean().optional(), archive: z13.boolean().optional() });
|
|
396
413
|
var UpdateFailureSchema = z13.enum(["git-install", "no-systemd", "sessions-at-risk", "npm-failed", "busy"]);
|
|
397
414
|
var ServerMessageSchema = z13.discriminatedUnion("type", [
|
|
398
415
|
z13.object({ type: z13.literal("chat.routed"), qid: z13.string(), result: RouteResultSchema }),
|
|
@@ -505,6 +522,14 @@ var ClientMessageSchema = z13.discriminatedUnion("type", [
|
|
|
505
522
|
z13.object({ type: z13.literal("session.interrupt"), id: z13.string() }),
|
|
506
523
|
/** Removes a finished (exited) or read-only (hook_only) session from the list; refused for a live launcher session. */
|
|
507
524
|
z13.object({ type: z13.literal("session.dismiss"), id: z13.string() }),
|
|
525
|
+
/** Puts a session in the archive: it leaves the live list but is kept. */
|
|
526
|
+
z13.object({ type: z13.literal("session.archive"), id: z13.string() }),
|
|
527
|
+
/** Takes an archived session back out of the archive, as it was. */
|
|
528
|
+
z13.object({ type: z13.literal("session.restore"), id: z13.string() }),
|
|
529
|
+
/** Starts the agent again on an archived session's conversation. */
|
|
530
|
+
z13.object({ type: z13.literal("session.resume"), id: z13.string() }),
|
|
531
|
+
/** Deletes a session for good, archived or not. */
|
|
532
|
+
z13.object({ type: z13.literal("session.delete"), id: z13.string() }),
|
|
508
533
|
/** The user names the session's topic from the overlay (500 characters at most, the hook's prompt cap); an empty topic hands back to the automatic sources. */
|
|
509
534
|
z13.object({ type: z13.literal("session.rename"), id: z13.string(), topic: z13.string().max(500) }),
|
|
510
535
|
z13.object({ type: z13.literal("session.thread"), id: z13.string() }),
|
|
@@ -673,6 +698,7 @@ function sessionTitle(s) {
|
|
|
673
698
|
|
|
674
699
|
export {
|
|
675
700
|
SessionSchema,
|
|
701
|
+
isArchived,
|
|
676
702
|
NOTE_NOT_TYPED,
|
|
677
703
|
offers,
|
|
678
704
|
pendingKey,
|
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
import {
|
|
2
2
|
cleanTopic,
|
|
3
3
|
sessionTitle
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-KJWMK65Y.js";
|
|
5
|
+
|
|
6
|
+
// src/daemon/crossSession.ts
|
|
7
|
+
var FIRST_LINE = /^Another Claude session sent a message:\s*\n/;
|
|
8
|
+
var BLOCK = /<cross-session-message\b([^>]*)>([\s\S]*?)(?:<\/cross-session-message>|(?=\n[ \t]*<cross-session-message\b)|$)/g;
|
|
9
|
+
var ATTR = /([\w-]+)="([^"]*)"/g;
|
|
10
|
+
function crossSession(text) {
|
|
11
|
+
const body = text.trimStart().replace(FIRST_LINE, "");
|
|
12
|
+
if (!body.startsWith("<cross-session-message")) return void 0;
|
|
13
|
+
const out = [];
|
|
14
|
+
for (const m of body.matchAll(BLOCK)) {
|
|
15
|
+
const attrs = new Map([...m[1].matchAll(ATTR)].map((a) => [a[1], a[2]]));
|
|
16
|
+
const from = attrs.get("from");
|
|
17
|
+
const name = attrs.get("from-name");
|
|
18
|
+
if (from) out.push({ from, ...name ? { name } : {}, text: m[2].trim() });
|
|
19
|
+
}
|
|
20
|
+
return out.length ? out : void 0;
|
|
21
|
+
}
|
|
5
22
|
|
|
6
23
|
// src/daemon/state.ts
|
|
7
24
|
var HOOK_PRIORITY_MS = 2e3;
|
|
@@ -134,6 +151,24 @@ function withTopic(s, text, source) {
|
|
|
134
151
|
return { ...next, title: sessionTitle(next) };
|
|
135
152
|
}
|
|
136
153
|
function reduce(s, ev) {
|
|
154
|
+
const next = reduceEvent(s, ev);
|
|
155
|
+
if (next === s) return s;
|
|
156
|
+
let changed = false;
|
|
157
|
+
const helpers = next.helpers.map((h) => {
|
|
158
|
+
if (h.state === "exited" || next.state === "exited") {
|
|
159
|
+
if (h.state === "exited" && h.finishedAt) return h;
|
|
160
|
+
changed = true;
|
|
161
|
+
return { ...h, state: "exited", finishedAt: h.finishedAt ?? next.exitedAt ?? ev.at };
|
|
162
|
+
}
|
|
163
|
+
if (!h.finishedAt) return h;
|
|
164
|
+
changed = true;
|
|
165
|
+
const { finishedAt: _gone, ...active } = h;
|
|
166
|
+
return active;
|
|
167
|
+
});
|
|
168
|
+
return changed ? { ...next, helpers } : next;
|
|
169
|
+
}
|
|
170
|
+
function reduceEvent(s, ev) {
|
|
171
|
+
if (ev.agentSessionId && !ev.helperId && ev.agentSessionId !== s.agentSessionId) s = { ...s, agentSessionId: ev.agentSessionId };
|
|
137
172
|
if (s.state === "exited") return s;
|
|
138
173
|
s = endCursorHelpers(s, ev);
|
|
139
174
|
const at = ev.at;
|
|
@@ -200,7 +235,7 @@ function reduce(s, ev) {
|
|
|
200
235
|
case "PreToolUse":
|
|
201
236
|
case "PermissionRequest":
|
|
202
237
|
case "Notification": {
|
|
203
|
-
if (ev.name === "UserPromptSubmit" && ev.text && !s.topic) s = withTopic(s, ev.text, "prompt");
|
|
238
|
+
if (ev.name === "UserPromptSubmit" && ev.text && !s.topic && !crossSession(ev.text)) s = withTopic(s, ev.text, "prompt");
|
|
204
239
|
const incoming = pendingFor(ev);
|
|
205
240
|
if (incoming) {
|
|
206
241
|
if (ev.name === "Notification" && s.pending?.source === "hook") return { ...s, lastHookAt: at, lastActivityAt: at };
|
|
@@ -218,6 +253,7 @@ function reduce(s, ev) {
|
|
|
218
253
|
}
|
|
219
254
|
|
|
220
255
|
export {
|
|
256
|
+
crossSession,
|
|
221
257
|
QUESTION_TOOLS,
|
|
222
258
|
reduce
|
|
223
259
|
};
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
} from "./chunk-L6IGH4LF.js";
|
|
4
4
|
import {
|
|
5
5
|
pickName
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-KJWMK65Y.js";
|
|
7
7
|
|
|
8
8
|
// src/daemon/transcript.ts
|
|
9
9
|
import { closeSync, fstatSync, openSync, readSync } from "fs";
|
|
@@ -120,8 +120,8 @@ function agentEnv(base, extra) {
|
|
|
120
120
|
async function spawnSession(opts) {
|
|
121
121
|
const now = opts.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
122
122
|
const takenIds = new Set(opts.taken.ids);
|
|
123
|
-
let id;
|
|
124
|
-
do
|
|
123
|
+
let id = opts.id;
|
|
124
|
+
if (id === void 0) do
|
|
125
125
|
id = randomBytes(2).toString("hex");
|
|
126
126
|
while (takenIds.has(id));
|
|
127
127
|
const name = pickName(opts.taken.names, opts.name);
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
QUESTION_TOOLS,
|
|
3
|
+
crossSession,
|
|
3
4
|
reduce
|
|
4
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-Y5TCZTFL.js";
|
|
5
6
|
import {
|
|
6
7
|
agentMessages,
|
|
7
8
|
claudePlan,
|
|
8
9
|
spawnSession
|
|
9
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-ZPANTZKC.js";
|
|
10
11
|
import {
|
|
11
12
|
LEDGE_VERSION
|
|
12
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-3KKTSYHP.js";
|
|
13
14
|
import {
|
|
14
15
|
SERVICE,
|
|
15
16
|
inDaemonService,
|
|
@@ -41,6 +42,7 @@ import {
|
|
|
41
42
|
SessionSchema,
|
|
42
43
|
SshAliasSchema,
|
|
43
44
|
extractProposal,
|
|
45
|
+
isArchived,
|
|
44
46
|
offers,
|
|
45
47
|
parseClientMessage,
|
|
46
48
|
parseProjectBlock,
|
|
@@ -50,7 +52,7 @@ import {
|
|
|
50
52
|
pendingKey,
|
|
51
53
|
pickName,
|
|
52
54
|
projectOf
|
|
53
|
-
} from "./chunk-
|
|
55
|
+
} from "./chunk-KJWMK65Y.js";
|
|
54
56
|
|
|
55
57
|
// src/daemon/index.ts
|
|
56
58
|
import { writeFile as writeFile5, rm as rm5 } from "fs/promises";
|
|
@@ -59,6 +61,7 @@ import { writeFile as writeFile5, rm as rm5 } from "fs/promises";
|
|
|
59
61
|
import { readFile, writeFile, rename, mkdir } from "fs/promises";
|
|
60
62
|
import { dirname } from "path";
|
|
61
63
|
var MAX_RECENTS = 12;
|
|
64
|
+
var ARCHIVE_MAX = 200;
|
|
62
65
|
var Store = class {
|
|
63
66
|
sessions = /* @__PURE__ */ new Map();
|
|
64
67
|
recentList = [];
|
|
@@ -67,33 +70,49 @@ var Store = class {
|
|
|
67
70
|
timer = null;
|
|
68
71
|
saving = Promise.resolve();
|
|
69
72
|
journalPath;
|
|
73
|
+
/** Set by an unclean load and saved with the journal: a later clean-looking load must not be trusted for cleanups. */
|
|
74
|
+
incomplete = false;
|
|
70
75
|
constructor(opts = {}) {
|
|
71
76
|
this.journalPath = opts.journalPath ?? paths().journal;
|
|
72
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Reads the journal. `clean` is false when it was missing, unreadable, or an entry was dropped by the schema, or when
|
|
80
|
+
* an earlier load was so (the mark is saved with the journal): what the store holds may then not be every session,
|
|
81
|
+
* and nothing may be cleaned up on the strength of it (the threads of the sessions it misses).
|
|
82
|
+
*/
|
|
73
83
|
async load() {
|
|
74
84
|
let raw;
|
|
75
85
|
try {
|
|
76
86
|
raw = await readFile(this.journalPath, "utf8");
|
|
77
87
|
} catch {
|
|
78
|
-
return;
|
|
88
|
+
return { clean: false, reason: "no journal" };
|
|
79
89
|
}
|
|
80
|
-
|
|
90
|
+
const bad = async (reason) => {
|
|
91
|
+
this.incomplete = true;
|
|
92
|
+
await writeFile(`${this.journalPath}.incomplete`, raw, { flag: "wx" }).catch(() => void 0);
|
|
93
|
+
return { clean: false, reason };
|
|
94
|
+
};
|
|
95
|
+
if (!raw.trim()) return bad("empty journal");
|
|
81
96
|
let parsed;
|
|
82
97
|
try {
|
|
83
98
|
parsed = JSON.parse(raw);
|
|
84
99
|
} catch (e) {
|
|
85
100
|
log(`journal unreadable, starting empty: ${e.message}`);
|
|
86
|
-
return;
|
|
101
|
+
return bad("unreadable journal");
|
|
87
102
|
}
|
|
88
103
|
const list = parsed?.sessions;
|
|
89
104
|
if (!Array.isArray(list)) {
|
|
90
105
|
log("journal unreadable, starting empty: no sessions array");
|
|
91
|
-
return;
|
|
106
|
+
return bad("no sessions array");
|
|
92
107
|
}
|
|
108
|
+
let dropped = 0;
|
|
93
109
|
for (const item of list) {
|
|
94
110
|
const r = SessionSchema.safeParse(item);
|
|
95
111
|
if (r.success) this.sessions.set(r.data.id, r.data);
|
|
96
|
-
else
|
|
112
|
+
else {
|
|
113
|
+
dropped++;
|
|
114
|
+
log(`journal entry dropped: ${r.error.issues[0]?.message ?? "invalid"}`);
|
|
115
|
+
}
|
|
97
116
|
}
|
|
98
117
|
const recents = parsed?.recents;
|
|
99
118
|
if (Array.isArray(recents)) {
|
|
@@ -103,6 +122,12 @@ var Store = class {
|
|
|
103
122
|
}
|
|
104
123
|
this.recentList = this.recentList.slice(0, MAX_RECENTS);
|
|
105
124
|
}
|
|
125
|
+
if (dropped) return bad(`${dropped} journal ${dropped === 1 ? "entry" : "entries"} dropped`);
|
|
126
|
+
if (parsed.incomplete === true) {
|
|
127
|
+
this.incomplete = true;
|
|
128
|
+
return { clean: false, reason: "an earlier load was incomplete" };
|
|
129
|
+
}
|
|
130
|
+
return { clean: true };
|
|
106
131
|
}
|
|
107
132
|
all() {
|
|
108
133
|
return [...this.sessions.values()];
|
|
@@ -130,14 +155,33 @@ var Store = class {
|
|
|
130
155
|
if (Object.keys(fields).every((k) => cur[k] === fields[k])) return;
|
|
131
156
|
this.upsert({ ...cur, ...fields });
|
|
132
157
|
}
|
|
158
|
+
/** Sets the archive date (kept if already set), then drops the oldest archives past `ARCHIVE_MAX`. */
|
|
159
|
+
archive(id, at) {
|
|
160
|
+
const cur = this.sessions.get(id);
|
|
161
|
+
if (!cur) return void 0;
|
|
162
|
+
const next = cur.archivedAt === void 0 ? { ...cur, archivedAt: at } : cur;
|
|
163
|
+
if (next !== cur) this.upsert(next);
|
|
164
|
+
const archived = this.all().filter((s) => s.archivedAt !== void 0).sort((a, b) => a.archivedAt.localeCompare(b.archivedAt));
|
|
165
|
+
for (const old of archived.slice(0, Math.max(0, archived.length - ARCHIVE_MAX))) this.remove(old.id);
|
|
166
|
+
return this.sessions.get(id);
|
|
167
|
+
}
|
|
168
|
+
/** Takes the session out of the archive; `at` (a restore by the user) restarts the retirement clock from then. */
|
|
169
|
+
unarchive(id, at) {
|
|
170
|
+
const cur = this.sessions.get(id);
|
|
171
|
+
if (!cur) return void 0;
|
|
172
|
+
if (cur.archivedAt === void 0) return cur;
|
|
173
|
+
const { archivedAt: _gone, ...rest } = cur;
|
|
174
|
+
const next = at !== void 0 ? { ...rest, restoredAt: at } : rest;
|
|
175
|
+
this.upsert(next);
|
|
176
|
+
return next;
|
|
177
|
+
}
|
|
133
178
|
/** Every event applied to a known session, before the reducer decides whether it changes anything. */
|
|
134
179
|
onEvent(listener) {
|
|
135
180
|
this.eventListeners.add(listener);
|
|
136
181
|
return () => this.eventListeners.delete(listener);
|
|
137
182
|
}
|
|
138
183
|
apply(id, event) {
|
|
139
|
-
|
|
140
|
-
if (!cur) return void 0;
|
|
184
|
+
if (!this.sessions.has(id)) return void 0;
|
|
141
185
|
for (const l of this.eventListeners) {
|
|
142
186
|
try {
|
|
143
187
|
l(id, event);
|
|
@@ -145,6 +189,8 @@ var Store = class {
|
|
|
145
189
|
log(`event listener failed: ${e.message}`);
|
|
146
190
|
}
|
|
147
191
|
}
|
|
192
|
+
const cur = this.sessions.get(id);
|
|
193
|
+
if (!cur) return void 0;
|
|
148
194
|
const next = reduce(cur, event);
|
|
149
195
|
if (next === cur) return cur;
|
|
150
196
|
this.sessions.set(id, next);
|
|
@@ -171,7 +217,7 @@ var Store = class {
|
|
|
171
217
|
async write() {
|
|
172
218
|
await mkdir(dirname(this.journalPath), { recursive: true });
|
|
173
219
|
const tmp = `${this.journalPath}.tmp`;
|
|
174
|
-
await writeFile(tmp, JSON.stringify({ savedAt: (/* @__PURE__ */ new Date()).toISOString(), sessions: this.all(), recents: this.recentList }, null, 2));
|
|
220
|
+
await writeFile(tmp, JSON.stringify({ savedAt: (/* @__PURE__ */ new Date()).toISOString(), ...this.incomplete ? { incomplete: true } : {}, sessions: this.all(), recents: this.recentList }, null, 2));
|
|
175
221
|
await rename(tmp, this.journalPath);
|
|
176
222
|
}
|
|
177
223
|
emit(c) {
|
|
@@ -282,8 +328,11 @@ var History = class {
|
|
|
282
328
|
listeners = /* @__PURE__ */ new Set();
|
|
283
329
|
writes = Promise.resolve();
|
|
284
330
|
dir;
|
|
331
|
+
/** The Ledge session a SendMessage address or a sender's name is, when it is one (peers.ts). */
|
|
332
|
+
peerOf;
|
|
285
333
|
constructor(opts = {}) {
|
|
286
334
|
this.dir = opts.dir ?? paths().history;
|
|
335
|
+
this.peerOf = opts.peerOf ?? (() => void 0);
|
|
287
336
|
}
|
|
288
337
|
items(id) {
|
|
289
338
|
return this.threads.get(id) ?? [];
|
|
@@ -355,6 +404,15 @@ var History = class {
|
|
|
355
404
|
if (helper?.kind === "helper") this.settle(id, (i) => i.helperId === back.helperId);
|
|
356
405
|
return back.report ? this.put(id, { kind: "reply", id: `report-${back.helperId}`, at: ev.at, turn: helper?.turn ?? turn, helperId: back.helperId, text: back.report }) : void 0;
|
|
357
406
|
}
|
|
407
|
+
const messages = crossSession(ev.text);
|
|
408
|
+
if (messages) {
|
|
409
|
+
this.turns.set(id, turn + 1);
|
|
410
|
+
let last;
|
|
411
|
+
messages.forEach((m, i) => {
|
|
412
|
+
last = this.receive(id, { kind: "received", id: `recv-${turn + 1}-${i}`, at: ev.at, turn: turn + 1, from: m.name ?? m.from, text: m.text }, m);
|
|
413
|
+
});
|
|
414
|
+
return last;
|
|
415
|
+
}
|
|
358
416
|
this.turns.set(id, turn + 1);
|
|
359
417
|
return this.put(id, { kind: "prompt", id: `prompt-${turn + 1}`, at: ev.at, turn: turn + 1, text: ev.text });
|
|
360
418
|
}
|
|
@@ -362,6 +420,7 @@ var History = class {
|
|
|
362
420
|
if (!ev.tool) return void 0;
|
|
363
421
|
const existing = ev.toolUseId && this.items(id).find((i) => i.id === ev.toolUseId);
|
|
364
422
|
if (existing) return existing;
|
|
423
|
+
if (ev.tool === "SendMessage") return this.put(id, this.sentOf(ev, common, "sending"));
|
|
365
424
|
const input = inputText(ev.toolInput);
|
|
366
425
|
return this.put(id, { kind: "tool", id: ev.toolUseId ?? `tool-${ev.at}-${this.items(id).length}`, ...common, tool: ev.tool, label: ev.text ?? ev.tool, ...input ? { input } : {}, status: "running", files: filesOf(ev.tool, ev.toolInput) });
|
|
367
426
|
}
|
|
@@ -369,6 +428,11 @@ var History = class {
|
|
|
369
428
|
case "PostToolUseFailure": {
|
|
370
429
|
if (!ev.tool) return void 0;
|
|
371
430
|
const failed = ev.name === "PostToolUseFailure";
|
|
431
|
+
if (ev.tool === "SendMessage") {
|
|
432
|
+
const sent = this.items(id).find((i) => i.kind === "sent" && i.id === ev.toolUseId);
|
|
433
|
+
if (sent?.status === "read") return sent;
|
|
434
|
+
return this.put(id, sent ? { ...sent, status: failed ? "failed" : "delivered" } : this.sentOf(ev, common, failed ? "failed" : "delivered"));
|
|
435
|
+
}
|
|
372
436
|
const row = this.callFor(id, ev);
|
|
373
437
|
const base = row ?? { kind: "tool", id: ev.toolUseId ?? `tool-${ev.at}-${this.items(id).length}`, ...common, tool: ev.tool, label: ev.text ?? ev.tool, status: "running", files: filesOf(ev.tool, ev.toolInput) };
|
|
374
438
|
const error = failed ? ev.error ?? (ev.interrupt ? "interrupted" : "failed") : void 0;
|
|
@@ -419,7 +483,7 @@ var History = class {
|
|
|
419
483
|
if (!opts.force && now - (this.synced.get(key) ?? 0) < SYNC_MS) return [];
|
|
420
484
|
this.synced.set(key, now);
|
|
421
485
|
const items = this.items(id);
|
|
422
|
-
const since = opts.helperId ? items.find((i) => i.id === opts.helperId)?.at : [...items].reverse().find((i) => i.kind === "prompt")?.at;
|
|
486
|
+
const since = opts.helperId ? items.find((i) => i.id === opts.helperId)?.at : [...items].reverse().find((i) => i.kind === "prompt" || i.kind === "received")?.at;
|
|
423
487
|
if (!since) return [];
|
|
424
488
|
const have = new Set(items.map((i) => i.id));
|
|
425
489
|
const turn = this.turns.get(id) ?? 0;
|
|
@@ -430,6 +494,25 @@ var History = class {
|
|
|
430
494
|
}
|
|
431
495
|
return added;
|
|
432
496
|
}
|
|
497
|
+
/** A SendMessage's card, from its input: `to` the address used, `peer` the Ledge session that address is. */
|
|
498
|
+
sentOf(ev, common, status) {
|
|
499
|
+
const input = ev.toolInput ?? {};
|
|
500
|
+
const to = typeof input.to === "string" ? input.to : "";
|
|
501
|
+
const peer = to ? this.peerOf(to) : void 0;
|
|
502
|
+
return { kind: "sent", id: ev.toolUseId ?? `sent-${ev.at}`, ...common, to, ...peer ? { peer } : {}, ...typeof input.summary === "string" && input.summary ? { summary: input.summary } : {}, text: typeof input.message === "string" ? input.message : "", status };
|
|
503
|
+
}
|
|
504
|
+
/** Puts a received card, paired with the sender's sent one when the sender is a Ledge session: the last of its
|
|
505
|
+
* messages to this session not read yet, the one whose text starts like the body if any (the hook may have cut
|
|
506
|
+
* either). That sent one becomes read, in the sender's thread. */
|
|
507
|
+
receive(id, recv, m) {
|
|
508
|
+
const peer = this.peerOf(m.from) ?? (m.name ? this.peerOf(m.name) : void 0);
|
|
509
|
+
if (!peer) return this.put(id, recv);
|
|
510
|
+
const head = (t) => t.trim().slice(0, 200);
|
|
511
|
+
const open3 = this.items(peer).filter((i) => i.kind === "sent" && i.peer === id && i.status !== "read");
|
|
512
|
+
const sent = [...open3].reverse().find((i) => head(i.text) === head(recv.text)) ?? open3.at(-1);
|
|
513
|
+
if (sent) this.put(peer, { ...sent, status: "read", received: recv.id });
|
|
514
|
+
return this.put(id, { ...recv, peer, ...sent ? { sent: sent.id } : {} });
|
|
515
|
+
}
|
|
433
516
|
/** The turn (or the helper) is over: an action still running will not get its result from this side. It shows as
|
|
434
517
|
* unknown, not as waiting forever; a result that arrives later still settles it (callFor finds it by id). */
|
|
435
518
|
settle(id, mine) {
|
|
@@ -1258,6 +1341,7 @@ async function gitSummary(cwd, exec = execGit) {
|
|
|
1258
1341
|
// src/daemon/poller.ts
|
|
1259
1342
|
var DETECT_LINES = 60;
|
|
1260
1343
|
var REST_MS = 2e3;
|
|
1344
|
+
var HELPER_RETENTION_MS = 5 * 60 * 1e3;
|
|
1261
1345
|
function startPoller(opts) {
|
|
1262
1346
|
const { store, tmux } = opts;
|
|
1263
1347
|
const adapterFor2 = opts.adapterFor ?? adapterFor;
|
|
@@ -1289,7 +1373,9 @@ function startPoller(opts) {
|
|
|
1289
1373
|
const deadCheck = t - lastDeadCheck >= deadCheckMs;
|
|
1290
1374
|
if (deadCheck) lastDeadCheck = t;
|
|
1291
1375
|
for (const id of previews.keys()) if (!store.get(id)) previews.delete(id);
|
|
1292
|
-
for (const
|
|
1376
|
+
for (const snapshot of store.all()) {
|
|
1377
|
+
const s = store.get(snapshot.id);
|
|
1378
|
+
if (!s) continue;
|
|
1293
1379
|
try {
|
|
1294
1380
|
await pollOne(s, t, deadCheck);
|
|
1295
1381
|
} catch (e) {
|
|
@@ -1301,9 +1387,22 @@ function startPoller(opts) {
|
|
|
1301
1387
|
}
|
|
1302
1388
|
}
|
|
1303
1389
|
async function pollOne(s, t, deadCheck) {
|
|
1390
|
+
const helpers = s.helpers.flatMap((h2) => {
|
|
1391
|
+
if (s.pending?.helperId === h2.id || h2.state !== "exited" && s.state !== "exited") return [h2];
|
|
1392
|
+
const finishedAt = h2.finishedAt ?? s.exitedAt ?? s.lastActivityAt;
|
|
1393
|
+
if (t - Date.parse(finishedAt) >= HELPER_RETENTION_MS) return [];
|
|
1394
|
+
return h2.finishedAt && h2.state === "exited" ? [h2] : [{ ...h2, state: "exited", finishedAt }];
|
|
1395
|
+
});
|
|
1396
|
+
if (helpers.length !== s.helpers.length || helpers.some((h2, i) => h2 !== s.helpers[i])) {
|
|
1397
|
+
s = { ...s, helpers };
|
|
1398
|
+
store.upsert(s);
|
|
1399
|
+
}
|
|
1304
1400
|
if (s.state === "exited") {
|
|
1305
1401
|
forget(s.id);
|
|
1306
|
-
if (s.
|
|
1402
|
+
if (!s.archivedAt && s.exitedAt) {
|
|
1403
|
+
const since = Math.max(Date.parse(s.exitedAt), s.restoredAt ? Date.parse(s.restoredAt) : -Infinity);
|
|
1404
|
+
if (t - since > retireMs) store.archive(s.id, now());
|
|
1405
|
+
}
|
|
1307
1406
|
return;
|
|
1308
1407
|
}
|
|
1309
1408
|
if ((deadCheck || !gitSeen.has(s.id)) && s.cwd) {
|
|
@@ -2127,7 +2226,7 @@ function createChat(deps) {
|
|
|
2127
2226
|
}
|
|
2128
2227
|
}
|
|
2129
2228
|
async function run2(turn, emit, fresh, mine, tool) {
|
|
2130
|
-
const ctx = await writeContext(contextDir, { sessions: deps.store.all(), recents: deps.store.recents(), projects: deps.projects?.(), now: now(), capture, git: (cwd) => gitSummary(cwd) });
|
|
2229
|
+
const ctx = await writeContext(contextDir, { sessions: deps.store.all().filter((s) => !isArchived(s)), recents: deps.store.recents(), projects: deps.projects?.(), now: now(), capture, git: (cwd) => gitSummary(cwd) });
|
|
2131
2230
|
if (mine.cancelled) return CANCELLED;
|
|
2132
2231
|
const { sessionId, resume } = await daySession(fresh, tool);
|
|
2133
2232
|
const label2 = TOOL_LABEL[tool];
|
|
@@ -2606,6 +2705,11 @@ function serveSsh(ws, alias, opts = {}) {
|
|
|
2606
2705
|
|
|
2607
2706
|
// src/daemon/server.ts
|
|
2608
2707
|
var CONTINUE_ARGS = { claude: ["--continue"], codex: ["resume", "--last"], cursor: ["--continue"] };
|
|
2708
|
+
var RESUME_ARGS = { claude: (id) => ["--resume", id], codex: (id) => ["resume", id], cursor: (id) => ["--resume", id] };
|
|
2709
|
+
function resumeKey(s) {
|
|
2710
|
+
if (s.agentSessionId) return s.agentSessionId;
|
|
2711
|
+
return s.tool === "claude" && s.transcriptPath ? basename4(s.transcriptPath, ".jsonl") : void 0;
|
|
2712
|
+
}
|
|
2609
2713
|
var MULTI_DIR_ARGS = { claude: (dirs) => ["--add-dir", ...dirs] };
|
|
2610
2714
|
function launchArgs(msg) {
|
|
2611
2715
|
const prompt = msg.prompt?.trim();
|
|
@@ -2617,6 +2721,7 @@ var NoteNotTyped = class extends Error {
|
|
|
2617
2721
|
super("denied, but the note could not be typed");
|
|
2618
2722
|
}
|
|
2619
2723
|
};
|
|
2724
|
+
var DELETED_TTL_MS = 5 * 60 * 1e3;
|
|
2620
2725
|
var ALLOWED_ORIGINS = [
|
|
2621
2726
|
"tauri://localhost",
|
|
2622
2727
|
"http://tauri.localhost",
|
|
@@ -2680,7 +2785,13 @@ async function startServer(opts) {
|
|
|
2680
2785
|
};
|
|
2681
2786
|
const history = opts.history;
|
|
2682
2787
|
const unsubscribeHistory = history ? [
|
|
2683
|
-
history.onItem((id, item) =>
|
|
2788
|
+
history.onItem((id, item) => {
|
|
2789
|
+
broadcast({ type: "session.thread.item", id, item });
|
|
2790
|
+
const cur = store.get(id)?.lastMessage;
|
|
2791
|
+
if (item.kind === "sent" && item.status !== "read" && item.status !== "failed" && cur?.id !== item.id && (!cur || item.at >= cur.at)) {
|
|
2792
|
+
store.patch(id, { lastMessage: { id: item.id, to: item.to, ...item.peer ? { peer: item.peer } : {}, summary: item.summary ?? item.text.split("\n")[0].slice(0, 120), at: item.at } });
|
|
2793
|
+
}
|
|
2794
|
+
}),
|
|
2684
2795
|
store.onEvent((id, ev) => history.record(id, ev)),
|
|
2685
2796
|
store.onChange((c) => {
|
|
2686
2797
|
if (c.type === "removed") history.forget(c.id);
|
|
@@ -2702,7 +2813,7 @@ async function startServer(opts) {
|
|
|
2702
2813
|
broadcast(c.type === "updated" ? { type: "session.updated", session: c.session } : { type: "session.removed", id: c.id });
|
|
2703
2814
|
});
|
|
2704
2815
|
const sendSnapshot = (ws) => {
|
|
2705
|
-
send(ws, { type: "snapshot", sessions: store.all(), recents: store.recents(), launcher, ...opts.daemon ? { daemon: { ...opts.daemon, routing: true } } : {} });
|
|
2816
|
+
send(ws, { type: "snapshot", sessions: store.all(), recents: store.recents(), launcher, ...opts.daemon ? { daemon: { ...opts.daemon, routing: true, archive: true } } : {} });
|
|
2706
2817
|
send(ws, machinesMsg());
|
|
2707
2818
|
send(ws, projectsMsg());
|
|
2708
2819
|
send(ws, islandMsg());
|
|
@@ -2791,6 +2902,11 @@ async function startServer(opts) {
|
|
|
2791
2902
|
const NOT_TAKEN = "the terminal did not take it";
|
|
2792
2903
|
const FREE_TEXT = "free-text answers are not supported yet, answer in the terminal";
|
|
2793
2904
|
const NOT_OFFERED = "this answer is not offered for this request, answer in the terminal";
|
|
2905
|
+
async function endSession(s) {
|
|
2906
|
+
if (s.source !== "launcher" || s.state === "exited") return;
|
|
2907
|
+
if (s.tmuxSession) await tmux.killSession(s.tmuxSession).catch((e) => log(`kill of ${s.tmuxSession} failed: ${e.message}`));
|
|
2908
|
+
store.apply(s.id, { name: "PaneGone", at: now(), source: "daemon" });
|
|
2909
|
+
}
|
|
2794
2910
|
async function replyTo(id, text) {
|
|
2795
2911
|
const s = store.get(id);
|
|
2796
2912
|
if (!s) return "unknown session";
|
|
@@ -2803,6 +2919,19 @@ async function startServer(opts) {
|
|
|
2803
2919
|
await injectAndVerify(id, (pane) => adapter.replyPlan(text, pending, pane || void 0), !!pending);
|
|
2804
2920
|
return null;
|
|
2805
2921
|
}
|
|
2922
|
+
const deleted = /* @__PURE__ */ new Map();
|
|
2923
|
+
function markDeleted(id) {
|
|
2924
|
+
const t = Date.now();
|
|
2925
|
+
for (const [k, at] of deleted) if (t - at > DELETED_TTL_MS) deleted.delete(k);
|
|
2926
|
+
deleted.set(id, t);
|
|
2927
|
+
}
|
|
2928
|
+
function recentlyDeleted(id) {
|
|
2929
|
+
const at = deleted.get(id);
|
|
2930
|
+
if (at === void 0) return false;
|
|
2931
|
+
if (Date.now() - at <= DELETED_TTL_MS) return true;
|
|
2932
|
+
deleted.delete(id);
|
|
2933
|
+
return false;
|
|
2934
|
+
}
|
|
2806
2935
|
const chatLaunches = /* @__PURE__ */ new Set();
|
|
2807
2936
|
async function launch(req) {
|
|
2808
2937
|
if (req.dirs?.length && !MULTI_DIR_ARGS[req.tool]) throw new Error(`launch: ${req.tool} takes one folder`);
|
|
@@ -2917,6 +3046,7 @@ async function startServer(opts) {
|
|
|
2917
3046
|
}
|
|
2918
3047
|
case "session.event": {
|
|
2919
3048
|
const ev = msg.event;
|
|
3049
|
+
if (recentlyDeleted(msg.id)) return;
|
|
2920
3050
|
const ownTurn = !ev.helperId && (ev.name === "PreToolUse" || ev.name === "PermissionRequest" || ev.name === "Stop");
|
|
2921
3051
|
const helperEnd = ev.name === "SubagentStop" && !!ev.helperId;
|
|
2922
3052
|
if (history && ev.transcriptPath && (ownTurn || helperEnd)) {
|
|
@@ -2926,7 +3056,11 @@ async function startServer(opts) {
|
|
|
2926
3056
|
log(`transcript not read for ${msg.id}: ${e.message}`);
|
|
2927
3057
|
}
|
|
2928
3058
|
}
|
|
3059
|
+
const terminal = ev.name === "SessionEnd" || ev.name === "PaneGone";
|
|
3060
|
+
const stored = store.get(msg.id);
|
|
3061
|
+
if (stored?.archivedAt && stored.state !== "exited" && !terminal) store.unarchive(msg.id);
|
|
2929
3062
|
const before = store.get(msg.id);
|
|
3063
|
+
if (!before && terminal) return;
|
|
2930
3064
|
if (!before) {
|
|
2931
3065
|
const taken = store.all().map((s) => s.name);
|
|
2932
3066
|
const cwd = msg.cwd ?? "";
|
|
@@ -2988,6 +3122,54 @@ async function startServer(opts) {
|
|
|
2988
3122
|
store.remove(msg.id);
|
|
2989
3123
|
return;
|
|
2990
3124
|
}
|
|
3125
|
+
case "session.archive": {
|
|
3126
|
+
const s = store.get(msg.id);
|
|
3127
|
+
if (!s) return refuse("unknown session", msg.id);
|
|
3128
|
+
await endSession(s);
|
|
3129
|
+
store.archive(msg.id, now());
|
|
3130
|
+
return;
|
|
3131
|
+
}
|
|
3132
|
+
case "session.restore": {
|
|
3133
|
+
if (!store.get(msg.id)) return refuse("unknown session", msg.id);
|
|
3134
|
+
store.unarchive(msg.id, now());
|
|
3135
|
+
return;
|
|
3136
|
+
}
|
|
3137
|
+
case "session.resume": {
|
|
3138
|
+
const s = store.get(msg.id);
|
|
3139
|
+
if (!s) return refuse("unknown session", msg.id);
|
|
3140
|
+
if (s.state !== "exited") return refuse("the session is still running", msg.id);
|
|
3141
|
+
const key = resumeKey(s);
|
|
3142
|
+
if (!key) return refuse("no conversation to resume", msg.id);
|
|
3143
|
+
const { archivedAt: _a, exitedAt: _e, exitCode: _c, ...rest } = s;
|
|
3144
|
+
store.upsert({ ...rest, state: "starting", source: "launcher", agentSessionId: key });
|
|
3145
|
+
try {
|
|
3146
|
+
const cwd = await resolveFolder(s.cwd, opts.home);
|
|
3147
|
+
const env = await agentEnv();
|
|
3148
|
+
const spawned = await spawnSession({ tool: s.tool, id: s.id, name: s.name, args: RESUME_ARGS[s.tool](key), cwd, tmux, now, env, taken: { names: store.all().filter((o) => o.id !== s.id && o.state !== "exited").map((o) => o.name), ids: [] } });
|
|
3149
|
+
const cur = store.get(s.id);
|
|
3150
|
+
if (!cur || cur.archivedAt !== void 0) {
|
|
3151
|
+
if (spawned.tmuxSession) await tmux.killSession(spawned.tmuxSession).catch((e) => log(`kill of ${spawned.tmuxSession} failed: ${e.message}`));
|
|
3152
|
+
log(`resume of ${s.id} dropped: the session was ${cur ? "archived" : "deleted"} meanwhile`);
|
|
3153
|
+
return;
|
|
3154
|
+
}
|
|
3155
|
+
const { topic, topicSource, repo, repoPath, branch, worktree, repoId, project, transcriptPath } = s;
|
|
3156
|
+
store.upsert({ ...spawned, agentSessionId: key, ...transcriptPath ? { transcriptPath } : {}, ...topic ? { topic } : {}, ...topicSource ? { topicSource } : {}, ...repo ? { repo } : {}, ...repoPath ? { repoPath } : {}, ...branch ? { branch } : {}, ...worktree !== void 0 ? { worktree } : {}, ...repoId ? { repoId } : {}, project });
|
|
3157
|
+
log(`resumed ${spawned.name} (${spawned.tool}) in ${spawned.cwd} as ${spawned.id}`);
|
|
3158
|
+
} catch (e) {
|
|
3159
|
+
const cur = store.get(s.id);
|
|
3160
|
+
if (cur && cur.archivedAt === void 0 && cur.state === "starting") store.upsert(s);
|
|
3161
|
+
return refuse(e.message, msg.id);
|
|
3162
|
+
}
|
|
3163
|
+
return;
|
|
3164
|
+
}
|
|
3165
|
+
case "session.delete": {
|
|
3166
|
+
const s = store.get(msg.id);
|
|
3167
|
+
if (!s) return refuse("unknown session", msg.id);
|
|
3168
|
+
if (s.source === "launcher" || s.state === "exited") markDeleted(msg.id);
|
|
3169
|
+
await endSession(s);
|
|
3170
|
+
store.remove(msg.id);
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
2991
3173
|
// A chat card's instruction: a refusal reaches the requester as an `error`, the card's state everyone through chat.history.
|
|
2992
3174
|
case "session.relay":
|
|
2993
3175
|
return relay.request(msg.qid);
|
|
@@ -3166,11 +3348,49 @@ ${output}` } : { code: r.code, output };
|
|
|
3166
3348
|
};
|
|
3167
3349
|
}
|
|
3168
3350
|
|
|
3351
|
+
// src/daemon/peers.ts
|
|
3352
|
+
import { readdirSync, readFileSync as readFileSync4 } from "fs";
|
|
3353
|
+
import { homedir as homedir6 } from "os";
|
|
3354
|
+
import { basename as basename5, join as join10 } from "path";
|
|
3355
|
+
var defaultDir = () => join10(process.env.CLAUDE_CONFIG_DIR ?? join10(homedir6(), ".claude"), "sessions");
|
|
3356
|
+
function parsePeer(path) {
|
|
3357
|
+
try {
|
|
3358
|
+
const j = JSON.parse(readFileSync4(path, "utf8"));
|
|
3359
|
+
if (typeof j.pid !== "number" || typeof j.sessionId !== "string" || typeof j.name !== "string" || typeof j.messagingSocketPath !== "string") return void 0;
|
|
3360
|
+
return { pid: j.pid, sessionId: j.sessionId, name: j.name, address: `uds:${j.messagingSocketPath}` };
|
|
3361
|
+
} catch {
|
|
3362
|
+
return void 0;
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
function readPeers(dir = defaultDir()) {
|
|
3366
|
+
let names;
|
|
3367
|
+
try {
|
|
3368
|
+
names = readdirSync(dir);
|
|
3369
|
+
} catch {
|
|
3370
|
+
return [];
|
|
3371
|
+
}
|
|
3372
|
+
return names.filter((n) => n.endsWith(".json")).flatMap((n) => parsePeer(join10(dir, n)) ?? []);
|
|
3373
|
+
}
|
|
3374
|
+
function peerFor(target, peers, sessions, dir = defaultDir()) {
|
|
3375
|
+
const name = target.replace(/ \[[^\]]*\]$/, "");
|
|
3376
|
+
const found = peers.filter((p) => p.name === name || p.address === name);
|
|
3377
|
+
if (!found.length && name.startsWith("uds:") && name.endsWith(".sock")) {
|
|
3378
|
+
const pid = basename5(name, ".sock");
|
|
3379
|
+
const direct = /^\d+$/.test(pid) ? parsePeer(join10(dir, `${pid}.json`)) : void 0;
|
|
3380
|
+
if (direct) found.push(direct);
|
|
3381
|
+
}
|
|
3382
|
+
for (const { sessionId: id } of found) {
|
|
3383
|
+
const s = sessions.find((s2) => s2.agentSessionId === id || s2.transcriptPath?.endsWith(`/${id}.jsonl`));
|
|
3384
|
+
if (s) return s;
|
|
3385
|
+
}
|
|
3386
|
+
return void 0;
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3169
3389
|
// src/daemon/index.ts
|
|
3170
3390
|
async function startDaemon(opts = {}) {
|
|
3171
3391
|
await ensureLedgeDir();
|
|
3172
3392
|
const store = new Store();
|
|
3173
|
-
await store.load();
|
|
3393
|
+
const loaded = await store.load();
|
|
3174
3394
|
const tmux = opts.tmux ?? realTmux(opts.tmuxSocket ?? "ledge");
|
|
3175
3395
|
let live2 = null;
|
|
3176
3396
|
try {
|
|
@@ -3179,17 +3399,19 @@ async function startDaemon(opts = {}) {
|
|
|
3179
3399
|
log(`startup prune skipped, tmux unavailable: ${e.message}`);
|
|
3180
3400
|
}
|
|
3181
3401
|
if (live2) for (const s of store.all()) {
|
|
3182
|
-
if (s.tmuxSession && !live2.has(s.tmuxSession)) store.
|
|
3402
|
+
if (s.tmuxSession && !live2.has(s.tmuxSession)) store.apply(s.id, { name: "PaneGone", at: (/* @__PURE__ */ new Date()).toISOString(), source: "daemon" });
|
|
3183
3403
|
}
|
|
3184
3404
|
const daemon = await daemonInfo();
|
|
3185
3405
|
const updater = createDaemonUpdater(realUpdaterDeps({ info: daemon, save: () => store.save() }));
|
|
3186
|
-
const history = new History();
|
|
3406
|
+
const history = new History({ peerOf: (t) => peerFor(t, readPeers(), store.all())?.id });
|
|
3187
3407
|
try {
|
|
3188
3408
|
await history.load();
|
|
3189
3409
|
} catch (e) {
|
|
3190
3410
|
log(`history not loaded: ${e.message}`);
|
|
3191
3411
|
}
|
|
3192
|
-
|
|
3412
|
+
if (loaded.clean) {
|
|
3413
|
+
for (const id of history.ids()) if (!store.get(id)) history.forget(id);
|
|
3414
|
+
} else log(`history cleanup skipped: ${loaded.reason}`);
|
|
3193
3415
|
const server = await startServer({ store, tmux, port: opts.port, daemon, updater, history });
|
|
3194
3416
|
const poller = startPoller({ store, tmux, broadcastPreview: server.preview });
|
|
3195
3417
|
await writeFile5(paths().pid, String(process.pid));
|
|
@@ -1,33 +1,34 @@
|
|
|
1
1
|
import {
|
|
2
2
|
QUESTION_TOOLS
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-Y5TCZTFL.js";
|
|
4
4
|
import {
|
|
5
5
|
CODEX_ENV_VAR,
|
|
6
6
|
CODEX_HOOK_VAR,
|
|
7
7
|
codexPlan
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-ZPANTZKC.js";
|
|
9
9
|
import {
|
|
10
10
|
log
|
|
11
11
|
} from "./chunk-L6IGH4LF.js";
|
|
12
12
|
import {
|
|
13
13
|
connect
|
|
14
|
-
} from "./chunk-
|
|
15
|
-
import "./chunk-
|
|
14
|
+
} from "./chunk-6ZKNEJND.js";
|
|
15
|
+
import "./chunk-KJWMK65Y.js";
|
|
16
16
|
|
|
17
17
|
// src/cli/hook.ts
|
|
18
18
|
import { readFileSync, openSync, fstatSync, readSync, closeSync } from "fs";
|
|
19
19
|
import { basename, join } from "path";
|
|
20
20
|
var str = (v) => typeof v === "string" ? v : void 0;
|
|
21
21
|
var PROMPT_MAX = 500;
|
|
22
|
-
var
|
|
22
|
+
var fromAgent = (t) => t.startsWith('<agent-message from="') || t.startsWith("<cross-session-message ");
|
|
23
23
|
var promptText = (p) => {
|
|
24
24
|
const t = str(p.prompt)?.trim();
|
|
25
|
-
return t ? { text: t.slice(0,
|
|
25
|
+
return t ? { text: t.slice(0, fromAgent(t) ? INPUT_MAX : PROMPT_MAX) } : {};
|
|
26
26
|
};
|
|
27
27
|
function describeTool(tool, input) {
|
|
28
28
|
const i = input && typeof input === "object" ? input : {};
|
|
29
29
|
if (tool === "Bash") return str(i.command) ?? "";
|
|
30
30
|
if (tool === "Edit" || tool === "Write" || tool === "MultiEdit" || tool === "Read") return str(i.file_path) ?? "";
|
|
31
|
+
if (tool === "SendMessage") return str(i.summary) || `\u2192 ${str(i.to) ?? ""}`;
|
|
31
32
|
if (tool === "ExitPlanMode") return str(i.plan)?.split("\n")[0] ?? "plan";
|
|
32
33
|
if (tool === "apply_patch") {
|
|
33
34
|
const files = [...(str(i.command) ?? "").matchAll(/^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm)].map((m) => m[1].trim());
|
|
@@ -102,7 +103,8 @@ function mapHook(agent, eventName, payload, at) {
|
|
|
102
103
|
const event = mapEvent(eventName, p, at, launched);
|
|
103
104
|
if (!event) return null;
|
|
104
105
|
const task = agent === "claude" && event.helperId ? helperDescription(str(p.transcript_path), event.helperId) : void 0;
|
|
105
|
-
|
|
106
|
+
const agentSessionId = !event.helperId ? str(p.session_id) : void 0;
|
|
107
|
+
return { id, event: { ...event, ...task ? { task } : {}, ...agentSessionId ? { agentSessionId } : {} }, agent, ...cwd ? { cwd } : {} };
|
|
106
108
|
}
|
|
107
109
|
function helperDescription(transcriptPath, agentId) {
|
|
108
110
|
if (!transcriptPath?.endsWith(".jsonl") || !/^[\w-]+$/.test(agentId)) return void 0;
|
|
@@ -246,7 +248,7 @@ function mapCursorHook(eventName, payload, at, env = process.env) {
|
|
|
246
248
|
const roots = Array.isArray(p.workspace_roots) ? p.workspace_roots : [];
|
|
247
249
|
const cwd = str(roots[0]) || void 0;
|
|
248
250
|
const event = helper ? mapCursorHelperEvent(eventName, p, at, sessionId) : mapCursorEvent(eventName, p, at, !!env.LEDGE_SESSION, transcript);
|
|
249
|
-
return event ? { id, event, agent: "cursor", ...cwd ? { cwd } : {} } : null;
|
|
251
|
+
return event ? { id, event: helper || event.helperId ? event : { ...event, agentSessionId: sessionId }, agent: "cursor", ...cwd ? { cwd } : {} } : null;
|
|
250
252
|
}
|
|
251
253
|
function mapCursorHelperEvent(eventName, p, at, helperId) {
|
|
252
254
|
const base = { at, source: "hook", helperId };
|
package/dist/cli/index.js
CHANGED
|
@@ -16,7 +16,7 @@ async function hookMain(args) {
|
|
|
16
16
|
try {
|
|
17
17
|
const chunks = [];
|
|
18
18
|
for await (const c of process.stdin) chunks.push(c);
|
|
19
|
-
const { runHook } = await import("./hook-
|
|
19
|
+
const { runHook } = await import("./hook-BBVUY4RK.js");
|
|
20
20
|
await runHook(event, Buffer.concat(chunks).toString("utf8"), { tool });
|
|
21
21
|
} catch {
|
|
22
22
|
}
|
|
@@ -26,7 +26,7 @@ async function main(argv) {
|
|
|
26
26
|
const [cmd, ...rest] = argv;
|
|
27
27
|
switch (cmd) {
|
|
28
28
|
case "daemon": {
|
|
29
|
-
const { startDaemon } = await import("./daemon-
|
|
29
|
+
const { startDaemon } = await import("./daemon-SRNTDIZW.js");
|
|
30
30
|
await startDaemon();
|
|
31
31
|
await new Promise(() => {
|
|
32
32
|
});
|
|
@@ -34,7 +34,7 @@ async function main(argv) {
|
|
|
34
34
|
}
|
|
35
35
|
case "--version":
|
|
36
36
|
case "version": {
|
|
37
|
-
const { LEDGE_VERSION } = await import("./version-
|
|
37
|
+
const { LEDGE_VERSION } = await import("./version-M6IZZGUV.js");
|
|
38
38
|
console.log(LEDGE_VERSION);
|
|
39
39
|
return 0;
|
|
40
40
|
}
|
|
@@ -50,23 +50,23 @@ async function main(argv) {
|
|
|
50
50
|
return r.ok ? 0 : 1;
|
|
51
51
|
}
|
|
52
52
|
case "status": {
|
|
53
|
-
const { printStatus } = await import("./status-
|
|
53
|
+
const { printStatus } = await import("./status-L2GROM4J.js");
|
|
54
54
|
console.log(await printStatus());
|
|
55
55
|
return 0;
|
|
56
56
|
}
|
|
57
57
|
case "bridge": {
|
|
58
|
-
const { bridge } = await import("./bridge-
|
|
58
|
+
const { bridge } = await import("./bridge-QKQW7UD6.js");
|
|
59
59
|
await bridge();
|
|
60
60
|
return 0;
|
|
61
61
|
}
|
|
62
62
|
case "claude":
|
|
63
63
|
case "codex":
|
|
64
64
|
case "cursor": {
|
|
65
|
-
const { launch } = await import("./launch-
|
|
65
|
+
const { launch } = await import("./launch-RWT4EWWI.js");
|
|
66
66
|
return launch(cmd, rest);
|
|
67
67
|
}
|
|
68
68
|
case "attach": {
|
|
69
|
-
const { attach } = await import("./attach-
|
|
69
|
+
const { attach } = await import("./attach-KTZOG7E5.js");
|
|
70
70
|
return attach(rest[0] ?? "");
|
|
71
71
|
}
|
|
72
72
|
default:
|
|
@@ -5,15 +5,15 @@ import {
|
|
|
5
5
|
ensureDaemon,
|
|
6
6
|
launch,
|
|
7
7
|
parseLaunchArgs
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-7AKUPFMS.js";
|
|
9
9
|
import {
|
|
10
10
|
agentEnv,
|
|
11
11
|
exitFile
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-ZPANTZKC.js";
|
|
13
13
|
import "./chunk-3TNPEAMR.js";
|
|
14
14
|
import "./chunk-L6IGH4LF.js";
|
|
15
|
-
import "./chunk-
|
|
16
|
-
import "./chunk-
|
|
15
|
+
import "./chunk-6ZKNEJND.js";
|
|
16
|
+
import "./chunk-KJWMK65Y.js";
|
|
17
17
|
export {
|
|
18
18
|
DaemonError,
|
|
19
19
|
agentEnv,
|
package/package.json
CHANGED