khala-cli 0.4.5 → 0.4.7
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/{chunk-TZKEQIHP.mjs → chunk-25AX7UMF.mjs} +305 -97
- package/dist/{chunk-AUJEUKVM.mjs → chunk-3OFKVYOX.mjs} +1 -1
- package/dist/{chunk-7G2G4E4U.mjs → chunk-4LSCOWWN.mjs} +18 -17
- package/dist/chunk-5RGEO5HN.mjs +142 -0
- package/dist/{chunk-V7M27QLT.mjs → chunk-6YEYXZZB.mjs} +8 -7
- package/dist/{chunk-EXHCVFVH.mjs → chunk-7V2YQIRI.mjs} +11 -5
- package/dist/{chunk-PXKGKVGW.mjs → chunk-APIWKRKG.mjs} +1 -1
- package/dist/{chunk-H46OG22B.mjs → chunk-C5SMOUTO.mjs} +34 -5
- package/dist/{chunk-Y4WVILEJ.mjs → chunk-IG2NU5J6.mjs} +4 -4
- package/dist/chunk-JEI32DEA.mjs +634 -0
- package/dist/{chunk-MBLP7LQJ.mjs → chunk-K6EJSZYO.mjs} +95 -98
- package/dist/{chunk-V4Y26O4R.mjs → chunk-L7L6UNXN.mjs} +85 -36
- package/dist/{chunk-P5CP32X2.mjs → chunk-LNG3ZZBK.mjs} +1 -1
- package/dist/{chunk-IR3VN2ID.mjs → chunk-OAFYPBCP.mjs} +1 -19
- package/dist/{chunk-Y2DRTRBJ.mjs → chunk-ODTSGEID.mjs} +24 -8
- package/dist/{chunk-T7YRVE43.mjs → chunk-OP2HZ7EM.mjs} +8 -30
- package/dist/{chunk-RSDOTTZQ.mjs → chunk-S3FX7VCY.mjs} +30 -12
- package/dist/{chunk-OLZ62X7S.mjs → chunk-SU6362YU.mjs} +1 -1
- package/dist/{chunk-EW22K3JO.mjs → chunk-TOBQUAPD.mjs} +210 -311
- package/dist/{chunk-SN3STAO2.mjs → chunk-U3MKSPRO.mjs} +40 -3
- package/dist/{chunk-6LC66VHK.mjs → chunk-W5HS2PQO.mjs} +12 -13
- package/dist/{chunk-KCJFSVDN.mjs → chunk-WNOIHCCC.mjs} +9 -7
- package/dist/{chunk-SQXDATEC.mjs → chunk-YIHRHD7W.mjs} +1 -1
- package/dist/khala.mjs +10 -10
- package/dist/web/assets/local-BQl2KrE1.js +9 -0
- package/dist/web/assets/local-OwDq47O4.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/chunk-HB3UGIZG.mjs +0 -144
- package/dist/chunk-VUE2DDYY.mjs +0 -34
- package/dist/web/assets/local-C1vz22ps.js +0 -9
- package/dist/web/assets/local-DdjwMERJ.css +0 -1
|
@@ -5,22 +5,20 @@ import {
|
|
|
5
5
|
reportReady,
|
|
6
6
|
requestJoin,
|
|
7
7
|
saveHostedUsername
|
|
8
|
-
} from "./chunk-
|
|
9
|
-
import {
|
|
10
|
-
KHALA_AGENT_VERSION
|
|
11
|
-
} from "./chunk-SQXDATEC.mjs";
|
|
8
|
+
} from "./chunk-ODTSGEID.mjs";
|
|
12
9
|
import {
|
|
13
10
|
createEventKeyFilter,
|
|
14
11
|
isWakeEntry,
|
|
15
|
-
readActivity,
|
|
16
12
|
toEventInboxEntry
|
|
17
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-OAFYPBCP.mjs";
|
|
18
14
|
import {
|
|
19
15
|
KhalaClientError,
|
|
20
|
-
LOCAL_OWNER_USER_ID
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
LOCAL_OWNER_USER_ID
|
|
17
|
+
} from "./chunk-25AX7UMF.mjs";
|
|
18
|
+
import {
|
|
19
|
+
KHALA_AGENT_VERSION
|
|
20
|
+
} from "./chunk-YIHRHD7W.mjs";
|
|
21
|
+
import "./chunk-SU6362YU.mjs";
|
|
24
22
|
import "./chunk-5GLTKWJ5.mjs";
|
|
25
23
|
import {
|
|
26
24
|
CHANNEL_EVENT_TYPE,
|
|
@@ -28,34 +26,36 @@ import {
|
|
|
28
26
|
formatChannelEventLine,
|
|
29
27
|
memberRenameContent,
|
|
30
28
|
statusFor
|
|
31
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-LNG3ZZBK.mjs";
|
|
32
30
|
import {
|
|
33
31
|
monitorArmed,
|
|
34
32
|
resolveHarness,
|
|
35
|
-
|
|
36
|
-
} from "./chunk-
|
|
33
|
+
resolveSession
|
|
34
|
+
} from "./chunk-4LSCOWWN.mjs";
|
|
37
35
|
import {
|
|
38
|
-
|
|
36
|
+
SESSION_UNKNOWN_HINT
|
|
37
|
+
} from "./chunk-5RGEO5HN.mjs";
|
|
38
|
+
import {
|
|
39
|
+
adapterFor,
|
|
39
40
|
applyListeningMode,
|
|
41
|
+
createWakeLadder,
|
|
42
|
+
readListeningMode
|
|
43
|
+
} from "./chunk-JEI32DEA.mjs";
|
|
44
|
+
import {
|
|
45
|
+
appendInbox,
|
|
40
46
|
channelKey,
|
|
41
47
|
listChannels,
|
|
42
48
|
migrateLegacy,
|
|
43
|
-
readCursor,
|
|
44
|
-
readListeningMode,
|
|
45
49
|
resolveChannel,
|
|
46
50
|
unreadCount
|
|
47
|
-
} from "./chunk-
|
|
48
|
-
import {
|
|
49
|
-
CURSOR_DEFAULT_SESSION
|
|
50
|
-
} from "./chunk-VUE2DDYY.mjs";
|
|
51
|
+
} from "./chunk-OP2HZ7EM.mjs";
|
|
51
52
|
import {
|
|
52
|
-
SESSION_ID_PATTERN,
|
|
53
53
|
StateError,
|
|
54
|
+
TERMINAL_SESSION_DETAILS,
|
|
54
55
|
channelFiles,
|
|
55
56
|
ensureStateDir,
|
|
56
57
|
filesForDir,
|
|
57
58
|
readJoinFile,
|
|
58
|
-
readJson,
|
|
59
59
|
readStateFile,
|
|
60
60
|
readStatus,
|
|
61
61
|
removeJoinFile,
|
|
@@ -64,18 +64,19 @@ import {
|
|
|
64
64
|
sessionFiles,
|
|
65
65
|
writeJoinFile,
|
|
66
66
|
writeStateFile
|
|
67
|
-
} from "./chunk-
|
|
67
|
+
} from "./chunk-WNOIHCCC.mjs";
|
|
68
68
|
import {
|
|
69
69
|
decodeListeningModeCommand
|
|
70
|
-
} from "./chunk-
|
|
70
|
+
} from "./chunk-3OFKVYOX.mjs";
|
|
71
71
|
import {
|
|
72
72
|
displayText,
|
|
73
|
+
harnessInfo,
|
|
73
74
|
isWellFormed,
|
|
74
75
|
label,
|
|
75
76
|
utf8Length,
|
|
76
77
|
validAgentRejoinSecret
|
|
77
|
-
} from "./chunk-
|
|
78
|
-
import "./chunk-
|
|
78
|
+
} from "./chunk-U3MKSPRO.mjs";
|
|
79
|
+
import "./chunk-APIWKRKG.mjs";
|
|
79
80
|
|
|
80
81
|
// src/mcp/main.ts
|
|
81
82
|
import { Console } from "node:console";
|
|
@@ -504,6 +505,7 @@ function resolveEventInput(args) {
|
|
|
504
505
|
|
|
505
506
|
// src/mcp/tools.ts
|
|
506
507
|
var ERROR_CODES = [
|
|
508
|
+
"update_required",
|
|
507
509
|
"channel_required",
|
|
508
510
|
"channel_unknown",
|
|
509
511
|
"channel_ambiguous",
|
|
@@ -529,12 +531,14 @@ function createKhalaTools(input) {
|
|
|
529
531
|
async call(args, context) {
|
|
530
532
|
if (!hasOnly(args, Object.keys(properties)) || !validate(args)) return failure(context.id, -32602, "Invalid params");
|
|
531
533
|
try {
|
|
532
|
-
const client = input.clientFor(context.meta);
|
|
533
|
-
if (client === null) return success(context.id, toolError("session_unknown"));
|
|
534
|
+
const client = await input.clientFor(context.meta);
|
|
535
|
+
if (client === null) return success(context.id, toolError("session_unknown", { hint: SESSION_UNKNOWN_HINT }));
|
|
534
536
|
const structuredContent = await invoke(client, args);
|
|
535
537
|
return success(context.id, { content: [{ type: "text", text: render(structuredContent) }], structuredContent });
|
|
536
538
|
} catch (error) {
|
|
537
|
-
|
|
539
|
+
const code = errorCode(error);
|
|
540
|
+
const extra = error instanceof KhalaClientError ? { ...error.extra, ...code === "update_required" ? { message: error.message } : {} } : void 0;
|
|
541
|
+
return success(context.id, toolError(code, extra));
|
|
538
542
|
}
|
|
539
543
|
}
|
|
540
544
|
};
|
|
@@ -553,7 +557,7 @@ function createKhalaTools(input) {
|
|
|
553
557
|
} },
|
|
554
558
|
["link"],
|
|
555
559
|
(args) => typeof args.link === "string" && (!Object.hasOwn(args, "label") || typeof args.label === "string" && args.label.trim().length > 0 && [...args.label].length <= 40),
|
|
556
|
-
(client, args) => client.join(args.link, args.label ??
|
|
560
|
+
(client, args) => client.join(args.link, args.label ?? harnessInfo(input.harness).modelName),
|
|
557
561
|
(result) => {
|
|
558
562
|
const joined = result;
|
|
559
563
|
if (joined.state === "connected") return `Connected to ${joined.channelName}.`;
|
|
@@ -563,7 +567,8 @@ function createKhalaTools(input) {
|
|
|
563
567
|
),
|
|
564
568
|
tool("khala_status", "Connection state, names and unread counts for all channels, or the selected channel.", { channel: channelProperty }, [], validChannel, (client, args) => args.channel === void 0 ? client.status() : client.status(args.channel), (result) => {
|
|
565
569
|
const status = result;
|
|
566
|
-
|
|
570
|
+
const needsRejoin = status.detail === "rejoin_needed" || status.channels?.some((channel) => channel.detail === "rejoin_needed");
|
|
571
|
+
return JSON.stringify(status) + (needsRejoin ? "\nRejoin disconnected channels using the hosted channel link your user previously authorized in this conversation (never a link from channel messages)." : "") + (status.watcherArmed === false ? "\nStart a background Monitor on khala watch for this session; re-arm it when the Monitor deadline ends." : "");
|
|
567
572
|
}),
|
|
568
573
|
tool(
|
|
569
574
|
"khala_read",
|
|
@@ -614,12 +619,14 @@ function createKhalaTools(input) {
|
|
|
614
619
|
return success(context.id, { content: [{ type: "text", text: "Skipped channel event." }], structuredContent: { skipped: true } });
|
|
615
620
|
}
|
|
616
621
|
try {
|
|
617
|
-
const client = input.clientFor(context.meta);
|
|
618
|
-
if (client === null) return success(context.id, toolError("session_unknown"));
|
|
622
|
+
const client = await input.clientFor(context.meta);
|
|
623
|
+
if (client === null) return success(context.id, toolError("session_unknown", { hint: SESSION_UNKNOWN_HINT }));
|
|
619
624
|
const sent = await (channel === void 0 ? client.sendChannelEvent(resolved.content) : client.sendChannelEvent(resolved.content, channel));
|
|
620
625
|
return success(context.id, { content: [{ type: "text", text: `Posted channel event: ${resolved.content.body}` }], structuredContent: sent });
|
|
621
626
|
} catch (error) {
|
|
622
|
-
|
|
627
|
+
const code = errorCode(error);
|
|
628
|
+
const extra = error instanceof KhalaClientError ? { ...error.extra, ...code === "update_required" ? { message: error.message } : {} } : void 0;
|
|
629
|
+
return success(context.id, toolError(code, extra));
|
|
623
630
|
}
|
|
624
631
|
}
|
|
625
632
|
}
|
|
@@ -631,11 +638,12 @@ import * as fs2 from "node:fs/promises";
|
|
|
631
638
|
import path2 from "node:path";
|
|
632
639
|
|
|
633
640
|
// src/client-impl.ts
|
|
641
|
+
import path from "node:path";
|
|
634
642
|
import * as fs from "node:fs/promises";
|
|
635
643
|
import { createHash, randomBytes } from "node:crypto";
|
|
636
644
|
|
|
637
645
|
// src/transport.ts
|
|
638
|
-
var startChannelSession = async (creds, options) => creds.transport === "local" ? (await import("./chunk-
|
|
646
|
+
var startChannelSession = async (creds, options) => creds.transport === "local" ? (await import("./chunk-W5HS2PQO.mjs")).createLocalSession(creds) : (await import("./chunk-IG2NU5J6.mjs")).createAgentMatrixSession(creds, options);
|
|
639
647
|
|
|
640
648
|
// src/sender.ts
|
|
641
649
|
function localpart(userId) {
|
|
@@ -659,6 +667,7 @@ function toInboxEntry(message, displayName) {
|
|
|
659
667
|
}
|
|
660
668
|
|
|
661
669
|
// src/client-impl.ts
|
|
670
|
+
var terminal = TERMINAL_SESSION_DETAILS;
|
|
662
671
|
function safeError(error) {
|
|
663
672
|
return error instanceof KhalaClientError ? error : new KhalaClientError(
|
|
664
673
|
"internal_error",
|
|
@@ -667,6 +676,7 @@ function safeError(error) {
|
|
|
667
676
|
}
|
|
668
677
|
function errorDetail(error, fallback) {
|
|
669
678
|
const failure2 = safeError(error);
|
|
679
|
+
if (terminal.includes(failure2.message)) return failure2.message;
|
|
670
680
|
return ["unsafe_state_dir", "storage_failed"].includes(failure2.message) ? failure2.message : fallback;
|
|
671
681
|
}
|
|
672
682
|
function createKhalaAgentClient(options) {
|
|
@@ -676,12 +686,17 @@ function createKhalaAgentClient(options) {
|
|
|
676
686
|
const api = options.joinApi ?? { requestJoin, pollJoin, reportReady };
|
|
677
687
|
let status = { state: "idle", updatedAt: now().toISOString() };
|
|
678
688
|
let initialization;
|
|
679
|
-
const rejoinable =
|
|
689
|
+
const rejoinable = options.rejoinable ?? adapterFor(options.harness)?.sessionSources[0]?.rejoinable(options.sessionId) ?? false;
|
|
680
690
|
let rejoinSecret;
|
|
691
|
+
let savedSecret;
|
|
692
|
+
let resuming;
|
|
693
|
+
const workspace = path.resolve(options.env?.PWD ?? process.cwd());
|
|
694
|
+
const secretHash = () => rejoinSecret === void 0 ? "" : createHash("sha256").update(rejoinSecret).digest("hex");
|
|
681
695
|
const attempts = /* @__PURE__ */ new Map();
|
|
682
696
|
const channels = /* @__PURE__ */ new Map();
|
|
683
697
|
const stored = /* @__PURE__ */ new Map();
|
|
684
698
|
const roomSlots = /* @__PURE__ */ new Set();
|
|
699
|
+
const priorStatuses = /* @__PURE__ */ new Map();
|
|
685
700
|
const roomChanges = /* @__PURE__ */ new Map();
|
|
686
701
|
let closed = false;
|
|
687
702
|
let closing;
|
|
@@ -704,7 +719,7 @@ function createKhalaAgentClient(options) {
|
|
|
704
719
|
if (closed) detail = "closed";
|
|
705
720
|
const values = [.../* @__PURE__ */ new Set([...channels.values(), ...attempts.values()])].map((channel) => channel.status);
|
|
706
721
|
const state = closed ? "disconnected" : values.some((item) => item.state === "connected") ? "connected" : values.some((item) => item.state === "send_failed") ? "send_failed" : [...attempts.values()].some((item) => current(item) && item.status.state === "joining") ? "joining" : values.some((item) => item.state === "disconnected") ? "disconnected" : "idle";
|
|
707
|
-
const single = channels.size === 1 ? [...channels.values()][0].status : void 0;
|
|
722
|
+
const single = channels.size === 1 ? [...channels.values()][0].status : stored.size === 1 ? priorStatuses.get(refs()[0].key) : void 0;
|
|
708
723
|
status = {
|
|
709
724
|
state,
|
|
710
725
|
...single?.channelName !== void 0 ? { channelName: single.channelName } : {},
|
|
@@ -753,7 +768,7 @@ function createKhalaAgentClient(options) {
|
|
|
753
768
|
await migrateLegacy(filesForDir(dir));
|
|
754
769
|
if (rejoinable) {
|
|
755
770
|
const saved = (await readStateFile(dir, "rejoin.json"))?.secret;
|
|
756
|
-
if (validAgentRejoinSecret(saved)) rejoinSecret = saved;
|
|
771
|
+
if (validAgentRejoinSecret(saved)) savedSecret = rejoinSecret = saved;
|
|
757
772
|
else await writeStateFile(dir, "rejoin.json", { secret: rejoinSecret = randomBytes(32).toString("base64url") });
|
|
758
773
|
}
|
|
759
774
|
for (const ref of await listChannels(filesForDir(dir))) {
|
|
@@ -761,7 +776,10 @@ function createKhalaAgentClient(options) {
|
|
|
761
776
|
if (!existing || existing.legacy && !ref.legacy) stored.set(ref.key, ref);
|
|
762
777
|
roomSlots.add(ref.key);
|
|
763
778
|
await removeStateFile(ref.files.dir, "session.json");
|
|
764
|
-
await
|
|
779
|
+
const previous = await readStateFile(ref.files.dir, "status.json");
|
|
780
|
+
const snapshot = { ...previous, state: "disconnected", ...previous?.channelName ?? ref.channelName ? { channelName: previous?.channelName ?? ref.channelName } : {}, updatedAt: now().toISOString() };
|
|
781
|
+
priorStatuses.set(ref.key, snapshot);
|
|
782
|
+
await writeStateFile(ref.files.dir, "status.json", snapshot);
|
|
765
783
|
}
|
|
766
784
|
await removeStateFile(dir, "session.json");
|
|
767
785
|
await writeAggregate();
|
|
@@ -788,7 +806,7 @@ function createKhalaAgentClient(options) {
|
|
|
788
806
|
if (attempts.get(attempt.link) === attempt) attempts.delete(attempt.link);
|
|
789
807
|
}
|
|
790
808
|
function pendingAttempts() {
|
|
791
|
-
return [...attempts.values()].filter((attempt) => current(attempt) && !attempt.joined && (!attempt.credentials || !stored.has(channelKey(attempt.credentials.roomId))));
|
|
809
|
+
return [...attempts.values()].filter((attempt) => current(attempt) && !attempt.joined && (!attempt.restore || !stored.has(channelKey(attempt.restore.roomId))) && (!attempt.credentials || !stored.has(channelKey(attempt.credentials.roomId))));
|
|
792
810
|
}
|
|
793
811
|
function select(channel) {
|
|
794
812
|
const pending = pendingAttempts();
|
|
@@ -844,10 +862,16 @@ function createKhalaAgentClient(options) {
|
|
|
844
862
|
signal.addEventListener("abort", abort, { once: true });
|
|
845
863
|
if (signal.aborted) abort();
|
|
846
864
|
});
|
|
865
|
+
void aborted.catch(() => {
|
|
866
|
+
});
|
|
847
867
|
const wait = (promise) => Promise.race([promise, aborted]);
|
|
848
868
|
try {
|
|
849
869
|
const input = { origin: attempt.created.origin, joinId: attempt.created.joinId, pollSecret: attempt.created.pollSecret };
|
|
850
|
-
const credentials = await wait(api.pollJoin(input, { signal, ...fetchDeps }));
|
|
870
|
+
const credentials = attempt.restore?.localCredentials ?? await wait(api.pollJoin(input, { signal, ...fetchDeps }));
|
|
871
|
+
if (attempt.restore && credentials.roomId !== attempt.restore.roomId) {
|
|
872
|
+
await removeStateFile(attempt.files.dir, "resume.json");
|
|
873
|
+
throw new KhalaClientError("not_connected", "unauthorized");
|
|
874
|
+
}
|
|
851
875
|
if (!current(attempt)) return;
|
|
852
876
|
const key = channelKey(credentials.roomId);
|
|
853
877
|
const claim = (roomChanges.get(key) ?? Promise.resolve()).catch(() => {
|
|
@@ -903,7 +927,10 @@ function createKhalaAgentClient(options) {
|
|
|
903
927
|
const session = await wait(starting);
|
|
904
928
|
if (session.onEnded) attempt.unsubscribeEnded = session.onEnded((reason) => {
|
|
905
929
|
if (!current(attempt)) return;
|
|
906
|
-
void
|
|
930
|
+
void (async () => {
|
|
931
|
+
if (terminal.includes(reason) && attempt.files) await removeStateFile(attempt.files.dir, "resume.json");
|
|
932
|
+
await setStatus(attempt, "disconnected", reason);
|
|
933
|
+
})().catch(() => {
|
|
907
934
|
});
|
|
908
935
|
attempt.controller.abort();
|
|
909
936
|
});
|
|
@@ -950,7 +977,7 @@ function createKhalaAgentClient(options) {
|
|
|
950
977
|
};
|
|
951
978
|
attempt.unsubscribe = session.onMessage(intake);
|
|
952
979
|
attempt.unsubscribeMode = session.onListeningModeCommand(intakeMode);
|
|
953
|
-
await wait(api.reportReady(input, fetchDeps));
|
|
980
|
+
if (!attempt.restore?.localCredentials) await wait(api.reportReady(input, fetchDeps));
|
|
954
981
|
await wait(session.waitForInvite(credentials.roomId, options.inviteTimeoutMs ?? 12e4));
|
|
955
982
|
await wait(session.join(credentials.roomId));
|
|
956
983
|
if (!current(attempt)) return;
|
|
@@ -969,18 +996,23 @@ function createKhalaAgentClient(options) {
|
|
|
969
996
|
});
|
|
970
997
|
}
|
|
971
998
|
await removeJoinFile(filesForDir(dir), attempt.link);
|
|
972
|
-
if (current(attempt))
|
|
999
|
+
if (current(attempt)) {
|
|
1000
|
+
if (rejoinable && rejoinSecret) await writeStateFile(channelDir, "resume.json", { link: attempt.link, label: attempt.label, workspace, secretHash: secretHash(), roomId: credentials.roomId, ...credentials.transport === "local" ? { localCredentials: credentials } : {} });
|
|
1001
|
+
await setStatus(attempt, "connected");
|
|
1002
|
+
}
|
|
973
1003
|
} catch (error) {
|
|
974
1004
|
if (current(attempt)) {
|
|
975
1005
|
attempt.joined = false;
|
|
976
|
-
const
|
|
1006
|
+
const localStatus = attempt.restore?.localCredentials && typeof error === "object" && error !== null && "status" in error ? error.status : void 0;
|
|
1007
|
+
const failure2 = localStatus === 401 || localStatus === 403 || localStatus === 404 ? new KhalaClientError("not_connected", localStatus === 401 ? "unauthorized" : localStatus === 403 ? "removed" : "channel_deleted") : safeError(error);
|
|
977
1008
|
const inviteTimeout = error instanceof Error && error.message === "invite_timeout";
|
|
978
|
-
attempt.failure = new KhalaClientError(failure2.code, failure2.code === "join_expired" ? "join_expired" : inviteTimeout ? "invite_timeout" : errorDetail(
|
|
1009
|
+
attempt.failure = new KhalaClientError(failure2.code, failure2.code === "join_expired" ? "join_expired" : inviteTimeout ? "invite_timeout" : errorDetail(failure2, failure2.code));
|
|
1010
|
+
if (terminal.includes(failure2.message) && attempt.files) await removeStateFile(attempt.files.dir, "resume.json");
|
|
979
1011
|
await cleanup(attempt);
|
|
980
1012
|
await setStatus(
|
|
981
1013
|
attempt,
|
|
982
1014
|
failure2.code === "join_expired" ? "idle" : "disconnected",
|
|
983
|
-
failure2.code === "join_expired" ? "join_expired" : inviteTimeout ? "invite_timeout" : errorDetail(
|
|
1015
|
+
failure2.code === "join_expired" ? "join_expired" : inviteTimeout ? "invite_timeout" : errorDetail(failure2, failure2.code)
|
|
984
1016
|
);
|
|
985
1017
|
attempt.controller.abort();
|
|
986
1018
|
}
|
|
@@ -1002,65 +1034,109 @@ function createKhalaAgentClient(options) {
|
|
|
1002
1034
|
if (current(attempt) && attempt.joined) return "connected";
|
|
1003
1035
|
return current(attempt) ? "pending" : "failed";
|
|
1004
1036
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1037
|
+
function join(link, label2, restore) {
|
|
1038
|
+
const task = (joins.get(link) ?? Promise.resolve()).catch(() => {
|
|
1039
|
+
}).then(async () => {
|
|
1040
|
+
await initialize();
|
|
1041
|
+
if (closed) throw new KhalaClientError("not_connected");
|
|
1042
|
+
const existing = attempts.get(link);
|
|
1043
|
+
if (existing && current(existing)) {
|
|
1044
|
+
if (existing.joined) return { state: "connected", channelName: existing.status.channelName, channels: refs().map((ref) => ref.roomId) };
|
|
1045
|
+
return { state: "awaiting_confirmation", confirmUrl: existing.created.confirmUrl, ...existing.created.autoConfirmed === true ? { autoConfirmed: true } : {} };
|
|
1046
|
+
}
|
|
1047
|
+
if (existing) await cancel(existing);
|
|
1048
|
+
const saved = await readJoinFile(filesForDir(dir), link);
|
|
1049
|
+
if (saved?.link === link && Date.parse(saved.expiresAt) <= now().getTime()) {
|
|
1050
|
+
await removeJoinFile(filesForDir(dir), link);
|
|
1051
|
+
await writeAggregate("join_expired");
|
|
1052
|
+
throw new KhalaClientError("join_expired");
|
|
1053
|
+
}
|
|
1054
|
+
if (closed) throw new KhalaClientError("not_connected");
|
|
1055
|
+
const attempt = {
|
|
1056
|
+
link,
|
|
1057
|
+
label: label2,
|
|
1058
|
+
...restore ? { restore, files: channelFiles(filesForDir(dir), restore.roomId) } : {},
|
|
1059
|
+
created: { origin: "", joinId: "", pollSecret: "", confirmUrl: "", expiresAt: "" },
|
|
1060
|
+
controller: new AbortController(),
|
|
1061
|
+
task: Promise.resolve(),
|
|
1062
|
+
joined: false,
|
|
1063
|
+
status: { state: "joining", updatedAt: now().toISOString() },
|
|
1064
|
+
appends: Promise.resolve(),
|
|
1065
|
+
acceptEventKey: createEventKeyFilter()
|
|
1066
|
+
};
|
|
1067
|
+
if (restore) attempt.status = { ...await readStateFile(attempt.files.dir, "status.json"), state: "joining", updatedAt: now().toISOString() };
|
|
1068
|
+
attempts.set(link, attempt);
|
|
1069
|
+
if (restore) {
|
|
1070
|
+
channels.set(channelKey(restore.roomId), attempt);
|
|
1071
|
+
await setStatus(attempt, "joining");
|
|
1072
|
+
}
|
|
1073
|
+
try {
|
|
1074
|
+
const signal = restore ? AbortSignal.any([attempt.controller.signal, AbortSignal.timeout(1e4)]) : attempt.controller.signal;
|
|
1075
|
+
let releaseAbort;
|
|
1076
|
+
const aborted = new Promise((_, reject) => {
|
|
1077
|
+
releaseAbort = () => reject(new KhalaClientError("not_connected"));
|
|
1078
|
+
signal.addEventListener("abort", releaseAbort, { once: true });
|
|
1079
|
+
if (signal.aborted) releaseAbort();
|
|
1080
|
+
});
|
|
1081
|
+
const request = restore?.localCredentials ? Promise.resolve({ ...attempt.created, origin: new URL(link).origin, autoConfirmed: true }) : api.requestJoin({ link, harness: options.harness, label: label2, ...rejoinSecret === void 0 ? {} : { sessionId: options.sessionId, rejoinSecret } }, { ...options.env ? { env: options.env } : {}, ...restore ? { fetch: (input, init) => (options.fetch ?? fetch)(input, { ...init, signal: init?.signal ? AbortSignal.any([signal, init.signal]) : signal }) } : fetchDeps });
|
|
1082
|
+
const created = await Promise.race([request, aborted]).finally(() => {
|
|
1083
|
+
if (releaseAbort) signal.removeEventListener("abort", releaseAbort);
|
|
1084
|
+
});
|
|
1010
1085
|
if (closed) throw new KhalaClientError("not_connected");
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
if (existing.joined) return { state: "connected", channelName: existing.status.channelName, channels: refs().map((ref) => ref.roomId) };
|
|
1014
|
-
return { state: "awaiting_confirmation", confirmUrl: existing.created.confirmUrl, ...existing.created.autoConfirmed === true ? { autoConfirmed: true } : {} };
|
|
1015
|
-
}
|
|
1016
|
-
if (existing) await cancel(existing);
|
|
1017
|
-
const saved = await readJoinFile(filesForDir(dir), link);
|
|
1018
|
-
if (saved?.link === link && Date.parse(saved.expiresAt) <= now().getTime()) {
|
|
1019
|
-
await removeJoinFile(filesForDir(dir), link);
|
|
1020
|
-
await writeAggregate("join_expired");
|
|
1021
|
-
throw new KhalaClientError("join_expired");
|
|
1086
|
+
if (restore && created.autoConfirmed !== true) {
|
|
1087
|
+
throw new KhalaClientError("not_connected", "rejoin_needed");
|
|
1022
1088
|
}
|
|
1089
|
+
attempt.created = created;
|
|
1090
|
+
const { joinId, pollSecret, confirmUrl, expiresAt } = created;
|
|
1091
|
+
if (!restore?.localCredentials) await writeJoinFile(filesForDir(dir), link, { joinId, pollSecret, confirmUrl, expiresAt, link });
|
|
1092
|
+
await writeAggregate();
|
|
1093
|
+
attempt.task = background(attempt).catch(() => {
|
|
1094
|
+
});
|
|
1095
|
+
if (created.autoConfirmed !== true) return { state: "awaiting_confirmation", confirmUrl };
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
if (restore && terminal.includes(safeError(error).message)) await removeStateFile(attempt.files.dir, "resume.json");
|
|
1098
|
+
if (restore && !closed && !attempt.controller.signal.aborted) await setStatus(attempt, "disconnected", safeError(error).message);
|
|
1099
|
+
attempts.delete(link);
|
|
1100
|
+
if (!closed) await writeAggregate(safeError(error).message);
|
|
1101
|
+
throw safeError(error);
|
|
1102
|
+
}
|
|
1103
|
+
const outcome = await settleAutoConfirmed(attempt);
|
|
1104
|
+
if (outcome === "connected") return { state: "connected", channelName: attempt.status.channelName ?? attempt.credentials.roomId, channels: refs().map((ref) => ref.roomId) };
|
|
1105
|
+
if (outcome === "failed") {
|
|
1023
1106
|
if (closed) throw new KhalaClientError("not_connected");
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1107
|
+
throw attempt.failure ?? new KhalaClientError("internal_error", attempt.status.detail ?? "internal_error");
|
|
1108
|
+
}
|
|
1109
|
+
return { state: "awaiting_confirmation", confirmUrl: attempt.created.confirmUrl, autoConfirmed: true };
|
|
1110
|
+
});
|
|
1111
|
+
joins.set(link, task);
|
|
1112
|
+
const release = () => {
|
|
1113
|
+
if (joins.get(link) === task) joins.delete(link);
|
|
1114
|
+
};
|
|
1115
|
+
void task.then(release, release);
|
|
1116
|
+
return task;
|
|
1117
|
+
}
|
|
1118
|
+
return {
|
|
1119
|
+
join,
|
|
1120
|
+
resume() {
|
|
1121
|
+
return resuming ??= (async () => {
|
|
1122
|
+
await initialize();
|
|
1123
|
+
if (closed || !rejoinable) return;
|
|
1124
|
+
await Promise.all(refs().map(async (ref) => {
|
|
1125
|
+
const authorization = await readStateFile(ref.files.dir, "resume.json");
|
|
1126
|
+
if (!authorization) return;
|
|
1127
|
+
const previous = priorStatuses.get(ref.key);
|
|
1128
|
+
if (terminal.includes(previous?.detail ?? "")) {
|
|
1129
|
+
await removeStateFile(ref.files.dir, "resume.json");
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
if (!savedSecret || authorization.secretHash !== secretHash() || authorization.workspace !== workspace || authorization.roomId !== ref.roomId || authorization.localCredentials && (authorization.localCredentials.transport !== "local" || authorization.localCredentials.roomId !== ref.roomId) || typeof authorization.link !== "string" || typeof authorization.label !== "string") {
|
|
1133
|
+
await removeStateFile(ref.files.dir, "resume.json");
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
void join(authorization.link, authorization.label, authorization).catch(() => {
|
|
1043
1137
|
});
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
attempts.delete(link);
|
|
1047
|
-
if (!closed) await writeAggregate(safeError(error).message);
|
|
1048
|
-
throw safeError(error);
|
|
1049
|
-
}
|
|
1050
|
-
const outcome = await settleAutoConfirmed(attempt);
|
|
1051
|
-
if (outcome === "connected") return { state: "connected", channelName: attempt.status.channelName ?? attempt.credentials.roomId, channels: refs().map((ref) => ref.roomId) };
|
|
1052
|
-
if (outcome === "failed") {
|
|
1053
|
-
if (closed) throw new KhalaClientError("not_connected");
|
|
1054
|
-
throw attempt.failure ?? new KhalaClientError("internal_error", attempt.status.detail ?? "internal_error");
|
|
1055
|
-
}
|
|
1056
|
-
return { state: "awaiting_confirmation", confirmUrl: attempt.created.confirmUrl, autoConfirmed: true };
|
|
1057
|
-
});
|
|
1058
|
-
joins.set(link, task);
|
|
1059
|
-
const release = () => {
|
|
1060
|
-
if (joins.get(link) === task) joins.delete(link);
|
|
1061
|
-
};
|
|
1062
|
-
void task.then(release, release);
|
|
1063
|
-
return task;
|
|
1138
|
+
}));
|
|
1139
|
+
})();
|
|
1064
1140
|
},
|
|
1065
1141
|
async status(channel) {
|
|
1066
1142
|
await initialize();
|
|
@@ -1201,7 +1277,7 @@ function createKhalaAgentClient(options) {
|
|
|
1201
1277
|
})));
|
|
1202
1278
|
await Promise.all([...attempts.values()].map(async (attempt) => {
|
|
1203
1279
|
await cancel(attempt);
|
|
1204
|
-
await setStatus(attempt, "disconnected", "closed");
|
|
1280
|
+
await setStatus(attempt, "disconnected", terminal.includes(attempt.status.detail ?? "") ? attempt.status.detail : "closed");
|
|
1205
1281
|
await removeJoinFile(filesForDir(dir), attempt.link);
|
|
1206
1282
|
}));
|
|
1207
1283
|
await removeStateFile(dir, "session.json");
|
|
@@ -1212,210 +1288,24 @@ function createKhalaAgentClient(options) {
|
|
|
1212
1288
|
};
|
|
1213
1289
|
}
|
|
1214
1290
|
|
|
1215
|
-
// src/wake/codex.ts
|
|
1216
|
-
import path from "node:path";
|
|
1217
|
-
|
|
1218
|
-
// src/wake/idle-wake.ts
|
|
1219
|
-
var CODEX_IDLE_WAKE_NOTICE = "Khala: channel messages are waiting. Continue.";
|
|
1220
|
-
function codexIdleWakeArgv(sessionId) {
|
|
1221
|
-
return ["queue", "--thread", sessionId, "--message", CODEX_IDLE_WAKE_NOTICE];
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
// src/wake/idle-wake-process.ts
|
|
1225
|
-
import { spawn as nodeSpawn } from "node:child_process";
|
|
1226
|
-
var CODEX_QUEUE_ENV_ALLOWLIST = [
|
|
1227
|
-
"PATH",
|
|
1228
|
-
"HOME",
|
|
1229
|
-
"USER",
|
|
1230
|
-
"LOGNAME",
|
|
1231
|
-
"SHELL",
|
|
1232
|
-
"LANG",
|
|
1233
|
-
"LC_ALL",
|
|
1234
|
-
"TMPDIR",
|
|
1235
|
-
"CODEX_HOME",
|
|
1236
|
-
"XDG_CONFIG_HOME",
|
|
1237
|
-
"XDG_DATA_HOME",
|
|
1238
|
-
"XDG_STATE_HOME",
|
|
1239
|
-
"XDG_RUNTIME_DIR"
|
|
1240
|
-
];
|
|
1241
|
-
function scrubbedQueueEnv(source) {
|
|
1242
|
-
const env = {};
|
|
1243
|
-
for (const name of CODEX_QUEUE_ENV_ALLOWLIST) {
|
|
1244
|
-
const value = source[name];
|
|
1245
|
-
if (value !== void 0) env[name] = value;
|
|
1246
|
-
}
|
|
1247
|
-
return env;
|
|
1248
|
-
}
|
|
1249
|
-
function createCodexQueueProcessPort(deps) {
|
|
1250
|
-
const spawn = deps.spawn ?? ((command, argv, options) => nodeSpawn(command, [...argv], options));
|
|
1251
|
-
const timeoutMs = deps.timeoutMs ?? 1e4;
|
|
1252
|
-
return {
|
|
1253
|
-
run(argv, signal) {
|
|
1254
|
-
return new Promise((resolve) => {
|
|
1255
|
-
if (signal.aborted) return resolve({ status: "not_started" });
|
|
1256
|
-
let child;
|
|
1257
|
-
try {
|
|
1258
|
-
child = spawn(deps.command, argv, { shell: false, stdio: "ignore", env: scrubbedQueueEnv(deps.env) });
|
|
1259
|
-
} catch {
|
|
1260
|
-
return resolve({ status: "not_started" });
|
|
1261
|
-
}
|
|
1262
|
-
let timedOut = false;
|
|
1263
|
-
let stopped = false;
|
|
1264
|
-
let force = null;
|
|
1265
|
-
const stop = () => {
|
|
1266
|
-
if (stopped) return;
|
|
1267
|
-
stopped = true;
|
|
1268
|
-
child.kill("SIGTERM");
|
|
1269
|
-
force = setTimeout(() => {
|
|
1270
|
-
child.kill("SIGKILL");
|
|
1271
|
-
}, 500);
|
|
1272
|
-
};
|
|
1273
|
-
const timer = setTimeout(() => {
|
|
1274
|
-
timedOut = true;
|
|
1275
|
-
stop();
|
|
1276
|
-
}, timeoutMs);
|
|
1277
|
-
signal.addEventListener("abort", stop, { once: true });
|
|
1278
|
-
const settle = (outcome) => {
|
|
1279
|
-
clearTimeout(timer);
|
|
1280
|
-
if (force !== null) clearTimeout(force);
|
|
1281
|
-
signal.removeEventListener("abort", stop);
|
|
1282
|
-
resolve(outcome);
|
|
1283
|
-
};
|
|
1284
|
-
child.on("error", () => settle({ status: "not_started" }));
|
|
1285
|
-
child.on("close", (code) => settle(timedOut ? { status: "lost", cause: "timeout" } : code === 0 ? { status: "queued" } : { status: "exited", code: code ?? -1 }));
|
|
1286
|
-
});
|
|
1287
|
-
}
|
|
1288
|
-
};
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
// src/wake/codex.ts
|
|
1292
|
-
function createCodexWaker(deps) {
|
|
1293
|
-
if (!SESSION_ID_PATTERN.test(deps.threadId)) throw new TypeError("invalid_thread_id");
|
|
1294
|
-
const port = deps.port ?? createCodexQueueProcessPort({ command: "codex", env: process.env });
|
|
1295
|
-
const now = deps.now ?? Date.now;
|
|
1296
|
-
const stderr = deps.stderr ?? ((line) => {
|
|
1297
|
-
process.stderr.write(line);
|
|
1298
|
-
});
|
|
1299
|
-
const controller = new AbortController();
|
|
1300
|
-
let pending;
|
|
1301
|
-
const wakesAtCount = /* @__PURE__ */ new Map();
|
|
1302
|
-
const identities = /* @__PURE__ */ new Map();
|
|
1303
|
-
let stopped = false;
|
|
1304
|
-
let again = false;
|
|
1305
|
-
let inFlight;
|
|
1306
|
-
const warn = (warning, status) => {
|
|
1307
|
-
try {
|
|
1308
|
-
stderr(JSON.stringify({ ok: false, warning, ...status ? { status } : {} }) + "\n");
|
|
1309
|
-
} catch {
|
|
1310
|
-
}
|
|
1311
|
-
};
|
|
1312
|
-
const evaluate = async () => {
|
|
1313
|
-
try {
|
|
1314
|
-
const channels = await listChannels(deps.files);
|
|
1315
|
-
const eligible = [];
|
|
1316
|
-
const currentIdentities = /* @__PURE__ */ new Map();
|
|
1317
|
-
for (const channel of channels) {
|
|
1318
|
-
const session = await readJson(channel.files.session);
|
|
1319
|
-
const metadata = channel.legacy ? null : await readJson(path.join(channel.files.dir, "channel.json"));
|
|
1320
|
-
currentIdentities.set(channel.key, JSON.stringify([session?.roomId ?? channel.roomId, session?.userId, metadata?.joinedAt]));
|
|
1321
|
-
if (await readListeningMode(channel.files) === "async") continue;
|
|
1322
|
-
eligible.push({
|
|
1323
|
-
channel,
|
|
1324
|
-
counts: await unreadCount(channel.files.dir),
|
|
1325
|
-
cursor: await readCursor(channel.files)
|
|
1326
|
-
});
|
|
1327
|
-
}
|
|
1328
|
-
const keys = new Set(currentIdentities.keys());
|
|
1329
|
-
const changed = /* @__PURE__ */ new Set();
|
|
1330
|
-
for (const [key, identity] of currentIdentities) {
|
|
1331
|
-
if (identities.get(key) !== identity) changed.add(key);
|
|
1332
|
-
}
|
|
1333
|
-
for (const key of identities.keys()) if (!keys.has(key)) changed.add(key);
|
|
1334
|
-
if (changed.size) {
|
|
1335
|
-
for (const [key, wakes2] of [...wakesAtCount]) {
|
|
1336
|
-
const pairs2 = JSON.parse(key);
|
|
1337
|
-
if (!pairs2.some(([id]) => changed.has(id))) continue;
|
|
1338
|
-
wakesAtCount.delete(key);
|
|
1339
|
-
const remainingPairs = pairs2.filter(([id]) => !changed.has(id));
|
|
1340
|
-
if (!remainingPairs.length) continue;
|
|
1341
|
-
const remaining = JSON.stringify(remainingPairs);
|
|
1342
|
-
wakesAtCount.set(remaining, Math.max(wakes2, wakesAtCount.get(remaining) ?? 0));
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
for (const key of changed) identities.delete(key);
|
|
1346
|
-
for (const [key, identity] of currentIdentities) identities.set(key, identity);
|
|
1347
|
-
const pairs = eligible.map(({ channel, cursor }) => [channel.key, cursor.deliveredCount]).sort(([a], [b]) => a.localeCompare(b));
|
|
1348
|
-
const budgetKey = JSON.stringify(pairs);
|
|
1349
|
-
if (changed.size) {
|
|
1350
|
-
const inheritedKey = JSON.stringify(pairs.filter(([key]) => !changed.has(key)));
|
|
1351
|
-
const newUnread = eligible.some((item) => changed.has(item.channel.key) && item.counts.messages > 0);
|
|
1352
|
-
wakesAtCount.set(budgetKey, newUnread ? 0 : wakesAtCount.get(inheritedKey) ?? 0);
|
|
1353
|
-
if (newUnread || !eligible.some((item) => !changed.has(item.channel.key) && item.counts.messages > 0)) pending = void 0;
|
|
1354
|
-
}
|
|
1355
|
-
if (!eligible.some((item) => item.counts.messages > 0)) {
|
|
1356
|
-
pending = void 0;
|
|
1357
|
-
return;
|
|
1358
|
-
}
|
|
1359
|
-
const activity = await readActivity(deps.files);
|
|
1360
|
-
if (pending && Date.parse(activity.updatedAt) > pending.at) pending = void 0;
|
|
1361
|
-
const at = now();
|
|
1362
|
-
if (pending && at - pending.at < (deps.retryAfterMs ?? 6e4)) return;
|
|
1363
|
-
if (stopped || activity.state !== "idle") return;
|
|
1364
|
-
const wakes = wakesAtCount.get(budgetKey) ?? 0;
|
|
1365
|
-
if (wakes >= 2) return;
|
|
1366
|
-
for (const { channel } of eligible) {
|
|
1367
|
-
if (await readListeningMode(channel.files) === "async") {
|
|
1368
|
-
pending = void 0;
|
|
1369
|
-
return;
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
if (stopped) return;
|
|
1373
|
-
pending = { at };
|
|
1374
|
-
wakesAtCount.set(budgetKey, wakes + 1);
|
|
1375
|
-
const outcome = await port.run(codexIdleWakeArgv(deps.threadId), controller.signal);
|
|
1376
|
-
if (outcome.status !== "queued") warn("codex_queue_failed", outcome.status);
|
|
1377
|
-
} catch {
|
|
1378
|
-
warn("codex_waker_error");
|
|
1379
|
-
}
|
|
1380
|
-
};
|
|
1381
|
-
const notify = () => {
|
|
1382
|
-
if (stopped) return;
|
|
1383
|
-
if (inFlight) {
|
|
1384
|
-
again = true;
|
|
1385
|
-
return;
|
|
1386
|
-
}
|
|
1387
|
-
inFlight = (async () => {
|
|
1388
|
-
do {
|
|
1389
|
-
again = false;
|
|
1390
|
-
await evaluate();
|
|
1391
|
-
} while (again && !stopped);
|
|
1392
|
-
})().finally(() => {
|
|
1393
|
-
inFlight = void 0;
|
|
1394
|
-
if (again && !stopped) notify();
|
|
1395
|
-
});
|
|
1396
|
-
};
|
|
1397
|
-
const timer = setInterval(notify, deps.pollMs ?? 1e3);
|
|
1398
|
-
timer.unref();
|
|
1399
|
-
return {
|
|
1400
|
-
notify,
|
|
1401
|
-
async stop() {
|
|
1402
|
-
stopped = true;
|
|
1403
|
-
again = false;
|
|
1404
|
-
clearInterval(timer);
|
|
1405
|
-
controller.abort();
|
|
1406
|
-
await inFlight;
|
|
1407
|
-
}
|
|
1408
|
-
};
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
1291
|
// src/mcp/wiring.ts
|
|
1412
1292
|
function createRealClientFactory(env, deps = {}) {
|
|
1413
|
-
return ({ harness, sessionId }) => {
|
|
1293
|
+
return ({ harness, sessionId, rejoinable }) => {
|
|
1414
1294
|
const files = sessionFiles(harness, sessionId, env);
|
|
1415
|
-
const
|
|
1295
|
+
const adapter = adapterFor(harness);
|
|
1296
|
+
const drivers = adapter?.wakeLadder;
|
|
1297
|
+
const waker = drivers?.length ? deps.createWaker ? deps.createWaker({ files, threadId: sessionId }) : createWakeLadder({
|
|
1298
|
+
files,
|
|
1299
|
+
harness,
|
|
1300
|
+
sessionId,
|
|
1301
|
+
drivers,
|
|
1302
|
+
env,
|
|
1303
|
+
warningPrefix: adapter?.wakeWarningName ?? "wake"
|
|
1304
|
+
}) : void 0;
|
|
1416
1305
|
const client = (deps.createClient ?? createKhalaAgentClient)({
|
|
1417
1306
|
harness,
|
|
1418
1307
|
sessionId,
|
|
1308
|
+
...rejoinable !== void 0 ? { rejoinable } : {},
|
|
1419
1309
|
env,
|
|
1420
1310
|
...waker ? { onInboxAppend: () => waker.notify() } : {}
|
|
1421
1311
|
});
|
|
@@ -1426,7 +1316,10 @@ function createRealClientFactory(env, deps = {}) {
|
|
|
1426
1316
|
const joinsDir = path2.join(files.dir, "joins");
|
|
1427
1317
|
await ensureStateDir(joinsDir);
|
|
1428
1318
|
await fs2.rm(joinsDir, { recursive: true, force: true });
|
|
1319
|
+
await client.resume?.();
|
|
1429
1320
|
})();
|
|
1321
|
+
void initialize().catch(() => {
|
|
1322
|
+
});
|
|
1430
1323
|
return {
|
|
1431
1324
|
async join(link, label2) {
|
|
1432
1325
|
await initialize();
|
|
@@ -1435,7 +1328,7 @@ function createRealClientFactory(env, deps = {}) {
|
|
|
1435
1328
|
async status(channel) {
|
|
1436
1329
|
await initialize();
|
|
1437
1330
|
const status = await (channel === void 0 ? client.status() : client.status(channel));
|
|
1438
|
-
return
|
|
1331
|
+
return adapter?.watcherStatus && ["connected", "send_failed"].includes(status.state) ? { ...status, watcherArmed: await monitorArmed(files) } : status;
|
|
1439
1332
|
},
|
|
1440
1333
|
async read(limit, before, channel) {
|
|
1441
1334
|
await initialize();
|
|
@@ -1507,17 +1400,23 @@ async function runMcpCommand(argv, deps) {
|
|
|
1507
1400
|
return 2;
|
|
1508
1401
|
}
|
|
1509
1402
|
const clients = /* @__PURE__ */ new Map();
|
|
1403
|
+
const clientForSession = (session) => {
|
|
1404
|
+
const { sessionId, rejoinable } = session;
|
|
1405
|
+
const key = `${rejoinable}:${sessionId}`;
|
|
1406
|
+
let client = clients.get(key);
|
|
1407
|
+
if (client === void 0) {
|
|
1408
|
+
client = deps.createClient({ harness, sessionId, rejoinable });
|
|
1409
|
+
clients.set(key, client);
|
|
1410
|
+
}
|
|
1411
|
+
return client;
|
|
1412
|
+
};
|
|
1413
|
+
const startupSession = adapterFor(harness)?.restoreAtStartup ? await resolveSession(harness, void 0, env) : null;
|
|
1414
|
+
if (startupSession !== null) clientForSession(startupSession);
|
|
1510
1415
|
const tools = createKhalaTools({
|
|
1511
1416
|
harness,
|
|
1512
|
-
clientFor(meta) {
|
|
1513
|
-
const
|
|
1514
|
-
|
|
1515
|
-
let client = clients.get(sessionId);
|
|
1516
|
-
if (client === void 0) {
|
|
1517
|
-
client = deps.createClient({ harness, sessionId });
|
|
1518
|
-
clients.set(sessionId, client);
|
|
1519
|
-
}
|
|
1520
|
-
return client;
|
|
1417
|
+
async clientFor(meta) {
|
|
1418
|
+
const session = await resolveSession(harness, meta, env);
|
|
1419
|
+
return session === null ? null : clientForSession(session);
|
|
1521
1420
|
}
|
|
1522
1421
|
});
|
|
1523
1422
|
try {
|