khala-cli 0.4.5 → 0.4.6
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-2U4E7AM7.mjs} +18 -3
- package/dist/{chunk-EXHCVFVH.mjs → chunk-2XTYRBYX.mjs} +10 -4
- package/dist/{chunk-MBLP7LQJ.mjs → chunk-4X35OUAN.mjs} +11 -11
- package/dist/{chunk-PXKGKVGW.mjs → chunk-55KBLEX5.mjs} +1 -1
- package/dist/{chunk-RSDOTTZQ.mjs → chunk-6E3UCO22.mjs} +32 -11
- package/dist/{chunk-7G2G4E4U.mjs → chunk-AWBKVTLV.mjs} +15 -13
- package/dist/{chunk-V7M27QLT.mjs → chunk-B2MOLCJM.mjs} +7 -5
- package/dist/{chunk-IR3VN2ID.mjs → chunk-CWHGNVZV.mjs} +0 -18
- package/dist/chunk-DPW43YNC.mjs +36 -0
- package/dist/{chunk-V4Y26O4R.mjs → chunk-FH2CZX5T.mjs} +14 -12
- package/dist/{chunk-HB3UGIZG.mjs → chunk-IULQYQJZ.mjs} +3 -3
- package/dist/{chunk-Y4WVILEJ.mjs → chunk-JG5J3SHW.mjs} +1 -1
- package/dist/{chunk-Y2DRTRBJ.mjs → chunk-OECJ45NM.mjs} +2 -2
- package/dist/{chunk-KCJFSVDN.mjs → chunk-OW3DSTEZ.mjs} +4 -2
- package/dist/{chunk-OLZ62X7S.mjs → chunk-PFDUXY3N.mjs} +1 -1
- package/dist/{chunk-H46OG22B.mjs → chunk-PY2ZNVNY.mjs} +1 -1
- package/dist/{chunk-SQXDATEC.mjs → chunk-RRBIVI77.mjs} +1 -1
- package/dist/{chunk-6LC66VHK.mjs → chunk-SZIEG3RS.mjs} +6 -5
- package/dist/chunk-WHONG7UF.mjs +267 -0
- package/dist/{chunk-T7YRVE43.mjs → chunk-XUJIJ4KB.mjs} +8 -30
- package/dist/{chunk-EW22K3JO.mjs → chunk-Z4EDYB6Y.mjs} +176 -293
- package/dist/chunk-ZHMGB3PP.mjs +82 -0
- package/dist/khala.mjs +10 -10
- package/dist/web/assets/local-K8EcTVxW.js +9 -0
- package/dist/web/assets/local-R2y0Gl4T.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- 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,21 @@ import {
|
|
|
5
5
|
reportReady,
|
|
6
6
|
requestJoin,
|
|
7
7
|
saveHostedUsername
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-OECJ45NM.mjs";
|
|
9
9
|
import {
|
|
10
10
|
KHALA_AGENT_VERSION
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-RRBIVI77.mjs";
|
|
12
12
|
import {
|
|
13
13
|
createEventKeyFilter,
|
|
14
14
|
isWakeEntry,
|
|
15
|
-
readActivity,
|
|
16
15
|
toEventInboxEntry
|
|
17
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-CWHGNVZV.mjs";
|
|
18
17
|
import {
|
|
19
18
|
KhalaClientError,
|
|
20
19
|
LOCAL_OWNER_USER_ID,
|
|
21
20
|
MODEL_NAMES
|
|
22
|
-
} from "./chunk-
|
|
23
|
-
import "./chunk-
|
|
21
|
+
} from "./chunk-2U4E7AM7.mjs";
|
|
22
|
+
import "./chunk-PFDUXY3N.mjs";
|
|
24
23
|
import "./chunk-5GLTKWJ5.mjs";
|
|
25
24
|
import {
|
|
26
25
|
CHANNEL_EVENT_TYPE,
|
|
@@ -33,29 +32,30 @@ import {
|
|
|
33
32
|
monitorArmed,
|
|
34
33
|
resolveHarness,
|
|
35
34
|
resolveSessionId
|
|
36
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-AWBKVTLV.mjs";
|
|
36
|
+
import {
|
|
37
|
+
adapterFor
|
|
38
|
+
} from "./chunk-WHONG7UF.mjs";
|
|
37
39
|
import {
|
|
38
|
-
appendInbox,
|
|
39
40
|
applyListeningMode,
|
|
41
|
+
readListeningMode
|
|
42
|
+
} from "./chunk-ZHMGB3PP.mjs";
|
|
43
|
+
import {
|
|
44
|
+
appendInbox,
|
|
40
45
|
channelKey,
|
|
41
46
|
listChannels,
|
|
42
47
|
migrateLegacy,
|
|
43
|
-
readCursor,
|
|
44
|
-
readListeningMode,
|
|
45
48
|
resolveChannel,
|
|
46
49
|
unreadCount
|
|
47
|
-
} from "./chunk-
|
|
48
|
-
import
|
|
49
|
-
CURSOR_DEFAULT_SESSION
|
|
50
|
-
} from "./chunk-VUE2DDYY.mjs";
|
|
50
|
+
} from "./chunk-XUJIJ4KB.mjs";
|
|
51
|
+
import "./chunk-DPW43YNC.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,7 +64,7 @@ import {
|
|
|
64
64
|
sessionFiles,
|
|
65
65
|
writeJoinFile,
|
|
66
66
|
writeStateFile
|
|
67
|
-
} from "./chunk-
|
|
67
|
+
} from "./chunk-OW3DSTEZ.mjs";
|
|
68
68
|
import {
|
|
69
69
|
decodeListeningModeCommand
|
|
70
70
|
} from "./chunk-AUJEUKVM.mjs";
|
|
@@ -75,7 +75,7 @@ import {
|
|
|
75
75
|
utf8Length,
|
|
76
76
|
validAgentRejoinSecret
|
|
77
77
|
} from "./chunk-SN3STAO2.mjs";
|
|
78
|
-
import "./chunk-
|
|
78
|
+
import "./chunk-55KBLEX5.mjs";
|
|
79
79
|
|
|
80
80
|
// src/mcp/main.ts
|
|
81
81
|
import { Console } from "node:console";
|
|
@@ -563,7 +563,8 @@ function createKhalaTools(input) {
|
|
|
563
563
|
),
|
|
564
564
|
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
565
|
const status = result;
|
|
566
|
-
|
|
566
|
+
const needsRejoin = status.detail === "rejoin_needed" || status.channels?.some((channel) => channel.detail === "rejoin_needed");
|
|
567
|
+
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
568
|
}),
|
|
568
569
|
tool(
|
|
569
570
|
"khala_read",
|
|
@@ -631,11 +632,12 @@ import * as fs2 from "node:fs/promises";
|
|
|
631
632
|
import path2 from "node:path";
|
|
632
633
|
|
|
633
634
|
// src/client-impl.ts
|
|
635
|
+
import path from "node:path";
|
|
634
636
|
import * as fs from "node:fs/promises";
|
|
635
637
|
import { createHash, randomBytes } from "node:crypto";
|
|
636
638
|
|
|
637
639
|
// src/transport.ts
|
|
638
|
-
var startChannelSession = async (creds, options) => creds.transport === "local" ? (await import("./chunk-
|
|
640
|
+
var startChannelSession = async (creds, options) => creds.transport === "local" ? (await import("./chunk-SZIEG3RS.mjs")).createLocalSession(creds) : (await import("./chunk-JG5J3SHW.mjs")).createAgentMatrixSession(creds, options);
|
|
639
641
|
|
|
640
642
|
// src/sender.ts
|
|
641
643
|
function localpart(userId) {
|
|
@@ -659,6 +661,7 @@ function toInboxEntry(message, displayName) {
|
|
|
659
661
|
}
|
|
660
662
|
|
|
661
663
|
// src/client-impl.ts
|
|
664
|
+
var terminal = TERMINAL_SESSION_DETAILS;
|
|
662
665
|
function safeError(error) {
|
|
663
666
|
return error instanceof KhalaClientError ? error : new KhalaClientError(
|
|
664
667
|
"internal_error",
|
|
@@ -667,6 +670,7 @@ function safeError(error) {
|
|
|
667
670
|
}
|
|
668
671
|
function errorDetail(error, fallback) {
|
|
669
672
|
const failure2 = safeError(error);
|
|
673
|
+
if (terminal.includes(failure2.message)) return failure2.message;
|
|
670
674
|
return ["unsafe_state_dir", "storage_failed"].includes(failure2.message) ? failure2.message : fallback;
|
|
671
675
|
}
|
|
672
676
|
function createKhalaAgentClient(options) {
|
|
@@ -676,12 +680,17 @@ function createKhalaAgentClient(options) {
|
|
|
676
680
|
const api = options.joinApi ?? { requestJoin, pollJoin, reportReady };
|
|
677
681
|
let status = { state: "idle", updatedAt: now().toISOString() };
|
|
678
682
|
let initialization;
|
|
679
|
-
const rejoinable =
|
|
683
|
+
const rejoinable = adapterFor(options.harness)?.rejoinable(options.sessionId) ?? false;
|
|
680
684
|
let rejoinSecret;
|
|
685
|
+
let savedSecret;
|
|
686
|
+
let resuming;
|
|
687
|
+
const workspace = path.resolve(options.env?.PWD ?? process.cwd());
|
|
688
|
+
const secretHash = () => rejoinSecret === void 0 ? "" : createHash("sha256").update(rejoinSecret).digest("hex");
|
|
681
689
|
const attempts = /* @__PURE__ */ new Map();
|
|
682
690
|
const channels = /* @__PURE__ */ new Map();
|
|
683
691
|
const stored = /* @__PURE__ */ new Map();
|
|
684
692
|
const roomSlots = /* @__PURE__ */ new Set();
|
|
693
|
+
const priorStatuses = /* @__PURE__ */ new Map();
|
|
685
694
|
const roomChanges = /* @__PURE__ */ new Map();
|
|
686
695
|
let closed = false;
|
|
687
696
|
let closing;
|
|
@@ -704,7 +713,7 @@ function createKhalaAgentClient(options) {
|
|
|
704
713
|
if (closed) detail = "closed";
|
|
705
714
|
const values = [.../* @__PURE__ */ new Set([...channels.values(), ...attempts.values()])].map((channel) => channel.status);
|
|
706
715
|
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;
|
|
716
|
+
const single = channels.size === 1 ? [...channels.values()][0].status : stored.size === 1 ? priorStatuses.get(refs()[0].key) : void 0;
|
|
708
717
|
status = {
|
|
709
718
|
state,
|
|
710
719
|
...single?.channelName !== void 0 ? { channelName: single.channelName } : {},
|
|
@@ -753,7 +762,7 @@ function createKhalaAgentClient(options) {
|
|
|
753
762
|
await migrateLegacy(filesForDir(dir));
|
|
754
763
|
if (rejoinable) {
|
|
755
764
|
const saved = (await readStateFile(dir, "rejoin.json"))?.secret;
|
|
756
|
-
if (validAgentRejoinSecret(saved)) rejoinSecret = saved;
|
|
765
|
+
if (validAgentRejoinSecret(saved)) savedSecret = rejoinSecret = saved;
|
|
757
766
|
else await writeStateFile(dir, "rejoin.json", { secret: rejoinSecret = randomBytes(32).toString("base64url") });
|
|
758
767
|
}
|
|
759
768
|
for (const ref of await listChannels(filesForDir(dir))) {
|
|
@@ -761,7 +770,10 @@ function createKhalaAgentClient(options) {
|
|
|
761
770
|
if (!existing || existing.legacy && !ref.legacy) stored.set(ref.key, ref);
|
|
762
771
|
roomSlots.add(ref.key);
|
|
763
772
|
await removeStateFile(ref.files.dir, "session.json");
|
|
764
|
-
await
|
|
773
|
+
const previous = await readStateFile(ref.files.dir, "status.json");
|
|
774
|
+
const snapshot = { ...previous, state: "disconnected", ...previous?.channelName ?? ref.channelName ? { channelName: previous?.channelName ?? ref.channelName } : {}, updatedAt: now().toISOString() };
|
|
775
|
+
priorStatuses.set(ref.key, snapshot);
|
|
776
|
+
await writeStateFile(ref.files.dir, "status.json", snapshot);
|
|
765
777
|
}
|
|
766
778
|
await removeStateFile(dir, "session.json");
|
|
767
779
|
await writeAggregate();
|
|
@@ -788,7 +800,7 @@ function createKhalaAgentClient(options) {
|
|
|
788
800
|
if (attempts.get(attempt.link) === attempt) attempts.delete(attempt.link);
|
|
789
801
|
}
|
|
790
802
|
function pendingAttempts() {
|
|
791
|
-
return [...attempts.values()].filter((attempt) => current(attempt) && !attempt.joined && (!attempt.credentials || !stored.has(channelKey(attempt.credentials.roomId))));
|
|
803
|
+
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
804
|
}
|
|
793
805
|
function select(channel) {
|
|
794
806
|
const pending = pendingAttempts();
|
|
@@ -844,10 +856,16 @@ function createKhalaAgentClient(options) {
|
|
|
844
856
|
signal.addEventListener("abort", abort, { once: true });
|
|
845
857
|
if (signal.aborted) abort();
|
|
846
858
|
});
|
|
859
|
+
void aborted.catch(() => {
|
|
860
|
+
});
|
|
847
861
|
const wait = (promise) => Promise.race([promise, aborted]);
|
|
848
862
|
try {
|
|
849
863
|
const input = { origin: attempt.created.origin, joinId: attempt.created.joinId, pollSecret: attempt.created.pollSecret };
|
|
850
|
-
const credentials = await wait(api.pollJoin(input, { signal, ...fetchDeps }));
|
|
864
|
+
const credentials = attempt.restore?.localCredentials ?? await wait(api.pollJoin(input, { signal, ...fetchDeps }));
|
|
865
|
+
if (attempt.restore && credentials.roomId !== attempt.restore.roomId) {
|
|
866
|
+
await removeStateFile(attempt.files.dir, "resume.json");
|
|
867
|
+
throw new KhalaClientError("not_connected", "unauthorized");
|
|
868
|
+
}
|
|
851
869
|
if (!current(attempt)) return;
|
|
852
870
|
const key = channelKey(credentials.roomId);
|
|
853
871
|
const claim = (roomChanges.get(key) ?? Promise.resolve()).catch(() => {
|
|
@@ -903,7 +921,10 @@ function createKhalaAgentClient(options) {
|
|
|
903
921
|
const session = await wait(starting);
|
|
904
922
|
if (session.onEnded) attempt.unsubscribeEnded = session.onEnded((reason) => {
|
|
905
923
|
if (!current(attempt)) return;
|
|
906
|
-
void
|
|
924
|
+
void (async () => {
|
|
925
|
+
if (terminal.includes(reason) && attempt.files) await removeStateFile(attempt.files.dir, "resume.json");
|
|
926
|
+
await setStatus(attempt, "disconnected", reason);
|
|
927
|
+
})().catch(() => {
|
|
907
928
|
});
|
|
908
929
|
attempt.controller.abort();
|
|
909
930
|
});
|
|
@@ -950,7 +971,7 @@ function createKhalaAgentClient(options) {
|
|
|
950
971
|
};
|
|
951
972
|
attempt.unsubscribe = session.onMessage(intake);
|
|
952
973
|
attempt.unsubscribeMode = session.onListeningModeCommand(intakeMode);
|
|
953
|
-
await wait(api.reportReady(input, fetchDeps));
|
|
974
|
+
if (!attempt.restore?.localCredentials) await wait(api.reportReady(input, fetchDeps));
|
|
954
975
|
await wait(session.waitForInvite(credentials.roomId, options.inviteTimeoutMs ?? 12e4));
|
|
955
976
|
await wait(session.join(credentials.roomId));
|
|
956
977
|
if (!current(attempt)) return;
|
|
@@ -969,18 +990,23 @@ function createKhalaAgentClient(options) {
|
|
|
969
990
|
});
|
|
970
991
|
}
|
|
971
992
|
await removeJoinFile(filesForDir(dir), attempt.link);
|
|
972
|
-
if (current(attempt))
|
|
993
|
+
if (current(attempt)) {
|
|
994
|
+
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 } : {} });
|
|
995
|
+
await setStatus(attempt, "connected");
|
|
996
|
+
}
|
|
973
997
|
} catch (error) {
|
|
974
998
|
if (current(attempt)) {
|
|
975
999
|
attempt.joined = false;
|
|
976
|
-
const
|
|
1000
|
+
const localStatus = attempt.restore?.localCredentials && typeof error === "object" && error !== null && "status" in error ? error.status : void 0;
|
|
1001
|
+
const failure2 = localStatus === 401 || localStatus === 403 || localStatus === 404 ? new KhalaClientError("not_connected", localStatus === 401 ? "unauthorized" : localStatus === 403 ? "removed" : "channel_deleted") : safeError(error);
|
|
977
1002
|
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(
|
|
1003
|
+
attempt.failure = new KhalaClientError(failure2.code, failure2.code === "join_expired" ? "join_expired" : inviteTimeout ? "invite_timeout" : errorDetail(failure2, failure2.code));
|
|
1004
|
+
if (terminal.includes(failure2.message) && attempt.files) await removeStateFile(attempt.files.dir, "resume.json");
|
|
979
1005
|
await cleanup(attempt);
|
|
980
1006
|
await setStatus(
|
|
981
1007
|
attempt,
|
|
982
1008
|
failure2.code === "join_expired" ? "idle" : "disconnected",
|
|
983
|
-
failure2.code === "join_expired" ? "join_expired" : inviteTimeout ? "invite_timeout" : errorDetail(
|
|
1009
|
+
failure2.code === "join_expired" ? "join_expired" : inviteTimeout ? "invite_timeout" : errorDetail(failure2, failure2.code)
|
|
984
1010
|
);
|
|
985
1011
|
attempt.controller.abort();
|
|
986
1012
|
}
|
|
@@ -1002,65 +1028,109 @@ function createKhalaAgentClient(options) {
|
|
|
1002
1028
|
if (current(attempt) && attempt.joined) return "connected";
|
|
1003
1029
|
return current(attempt) ? "pending" : "failed";
|
|
1004
1030
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1031
|
+
function join(link, label2, restore) {
|
|
1032
|
+
const task = (joins.get(link) ?? Promise.resolve()).catch(() => {
|
|
1033
|
+
}).then(async () => {
|
|
1034
|
+
await initialize();
|
|
1035
|
+
if (closed) throw new KhalaClientError("not_connected");
|
|
1036
|
+
const existing = attempts.get(link);
|
|
1037
|
+
if (existing && current(existing)) {
|
|
1038
|
+
if (existing.joined) return { state: "connected", channelName: existing.status.channelName, channels: refs().map((ref) => ref.roomId) };
|
|
1039
|
+
return { state: "awaiting_confirmation", confirmUrl: existing.created.confirmUrl, ...existing.created.autoConfirmed === true ? { autoConfirmed: true } : {} };
|
|
1040
|
+
}
|
|
1041
|
+
if (existing) await cancel(existing);
|
|
1042
|
+
const saved = await readJoinFile(filesForDir(dir), link);
|
|
1043
|
+
if (saved?.link === link && Date.parse(saved.expiresAt) <= now().getTime()) {
|
|
1044
|
+
await removeJoinFile(filesForDir(dir), link);
|
|
1045
|
+
await writeAggregate("join_expired");
|
|
1046
|
+
throw new KhalaClientError("join_expired");
|
|
1047
|
+
}
|
|
1048
|
+
if (closed) throw new KhalaClientError("not_connected");
|
|
1049
|
+
const attempt = {
|
|
1050
|
+
link,
|
|
1051
|
+
label: label2,
|
|
1052
|
+
...restore ? { restore, files: channelFiles(filesForDir(dir), restore.roomId) } : {},
|
|
1053
|
+
created: { origin: "", joinId: "", pollSecret: "", confirmUrl: "", expiresAt: "" },
|
|
1054
|
+
controller: new AbortController(),
|
|
1055
|
+
task: Promise.resolve(),
|
|
1056
|
+
joined: false,
|
|
1057
|
+
status: { state: "joining", updatedAt: now().toISOString() },
|
|
1058
|
+
appends: Promise.resolve(),
|
|
1059
|
+
acceptEventKey: createEventKeyFilter()
|
|
1060
|
+
};
|
|
1061
|
+
if (restore) attempt.status = { ...await readStateFile(attempt.files.dir, "status.json"), state: "joining", updatedAt: now().toISOString() };
|
|
1062
|
+
attempts.set(link, attempt);
|
|
1063
|
+
if (restore) {
|
|
1064
|
+
channels.set(channelKey(restore.roomId), attempt);
|
|
1065
|
+
await setStatus(attempt, "joining");
|
|
1066
|
+
}
|
|
1067
|
+
try {
|
|
1068
|
+
const signal = restore ? AbortSignal.any([attempt.controller.signal, AbortSignal.timeout(1e4)]) : attempt.controller.signal;
|
|
1069
|
+
let releaseAbort;
|
|
1070
|
+
const aborted = new Promise((_, reject) => {
|
|
1071
|
+
releaseAbort = () => reject(new KhalaClientError("not_connected"));
|
|
1072
|
+
signal.addEventListener("abort", releaseAbort, { once: true });
|
|
1073
|
+
if (signal.aborted) releaseAbort();
|
|
1074
|
+
});
|
|
1075
|
+
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 } }, restore ? { fetch: (input, init) => (options.fetch ?? fetch)(input, { ...init, signal: init?.signal ? AbortSignal.any([signal, init.signal]) : signal }) } : fetchDeps);
|
|
1076
|
+
const created = await Promise.race([request, aborted]).finally(() => {
|
|
1077
|
+
if (releaseAbort) signal.removeEventListener("abort", releaseAbort);
|
|
1078
|
+
});
|
|
1010
1079
|
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");
|
|
1080
|
+
if (restore && created.autoConfirmed !== true) {
|
|
1081
|
+
throw new KhalaClientError("not_connected", "rejoin_needed");
|
|
1022
1082
|
}
|
|
1083
|
+
attempt.created = created;
|
|
1084
|
+
const { joinId, pollSecret, confirmUrl, expiresAt } = created;
|
|
1085
|
+
if (!restore?.localCredentials) await writeJoinFile(filesForDir(dir), link, { joinId, pollSecret, confirmUrl, expiresAt, link });
|
|
1086
|
+
await writeAggregate();
|
|
1087
|
+
attempt.task = background(attempt).catch(() => {
|
|
1088
|
+
});
|
|
1089
|
+
if (created.autoConfirmed !== true) return { state: "awaiting_confirmation", confirmUrl };
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
if (restore && terminal.includes(safeError(error).message)) await removeStateFile(attempt.files.dir, "resume.json");
|
|
1092
|
+
if (restore && !closed && !attempt.controller.signal.aborted) await setStatus(attempt, "disconnected", safeError(error).message);
|
|
1093
|
+
attempts.delete(link);
|
|
1094
|
+
if (!closed) await writeAggregate(safeError(error).message);
|
|
1095
|
+
throw safeError(error);
|
|
1096
|
+
}
|
|
1097
|
+
const outcome = await settleAutoConfirmed(attempt);
|
|
1098
|
+
if (outcome === "connected") return { state: "connected", channelName: attempt.status.channelName ?? attempt.credentials.roomId, channels: refs().map((ref) => ref.roomId) };
|
|
1099
|
+
if (outcome === "failed") {
|
|
1023
1100
|
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
|
-
|
|
1101
|
+
throw attempt.failure ?? new KhalaClientError("internal_error", attempt.status.detail ?? "internal_error");
|
|
1102
|
+
}
|
|
1103
|
+
return { state: "awaiting_confirmation", confirmUrl: attempt.created.confirmUrl, autoConfirmed: true };
|
|
1104
|
+
});
|
|
1105
|
+
joins.set(link, task);
|
|
1106
|
+
const release = () => {
|
|
1107
|
+
if (joins.get(link) === task) joins.delete(link);
|
|
1108
|
+
};
|
|
1109
|
+
void task.then(release, release);
|
|
1110
|
+
return task;
|
|
1111
|
+
}
|
|
1112
|
+
return {
|
|
1113
|
+
join,
|
|
1114
|
+
resume() {
|
|
1115
|
+
return resuming ??= (async () => {
|
|
1116
|
+
await initialize();
|
|
1117
|
+
if (closed || !rejoinable) return;
|
|
1118
|
+
await Promise.all(refs().map(async (ref) => {
|
|
1119
|
+
const authorization = await readStateFile(ref.files.dir, "resume.json");
|
|
1120
|
+
if (!authorization) return;
|
|
1121
|
+
const previous = priorStatuses.get(ref.key);
|
|
1122
|
+
if (terminal.includes(previous?.detail ?? "")) {
|
|
1123
|
+
await removeStateFile(ref.files.dir, "resume.json");
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
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") {
|
|
1127
|
+
await removeStateFile(ref.files.dir, "resume.json");
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
void join(authorization.link, authorization.label, authorization).catch(() => {
|
|
1043
1131
|
});
|
|
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;
|
|
1132
|
+
}));
|
|
1133
|
+
})();
|
|
1064
1134
|
},
|
|
1065
1135
|
async status(channel) {
|
|
1066
1136
|
await initialize();
|
|
@@ -1201,7 +1271,7 @@ function createKhalaAgentClient(options) {
|
|
|
1201
1271
|
})));
|
|
1202
1272
|
await Promise.all([...attempts.values()].map(async (attempt) => {
|
|
1203
1273
|
await cancel(attempt);
|
|
1204
|
-
await setStatus(attempt, "disconnected", "closed");
|
|
1274
|
+
await setStatus(attempt, "disconnected", terminal.includes(attempt.status.detail ?? "") ? attempt.status.detail : "closed");
|
|
1205
1275
|
await removeJoinFile(filesForDir(dir), attempt.link);
|
|
1206
1276
|
}));
|
|
1207
1277
|
await removeStateFile(dir, "session.json");
|
|
@@ -1212,207 +1282,13 @@ function createKhalaAgentClient(options) {
|
|
|
1212
1282
|
};
|
|
1213
1283
|
}
|
|
1214
1284
|
|
|
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
1285
|
// src/mcp/wiring.ts
|
|
1412
1286
|
function createRealClientFactory(env, deps = {}) {
|
|
1413
1287
|
return ({ harness, sessionId }) => {
|
|
1414
1288
|
const files = sessionFiles(harness, sessionId, env);
|
|
1415
|
-
const
|
|
1289
|
+
const adapter = adapterFor(harness);
|
|
1290
|
+
const createWaker = adapter?.waker && (deps.createWaker ?? adapter.waker);
|
|
1291
|
+
const waker = createWaker?.({ files, threadId: sessionId });
|
|
1416
1292
|
const client = (deps.createClient ?? createKhalaAgentClient)({
|
|
1417
1293
|
harness,
|
|
1418
1294
|
sessionId,
|
|
@@ -1426,7 +1302,10 @@ function createRealClientFactory(env, deps = {}) {
|
|
|
1426
1302
|
const joinsDir = path2.join(files.dir, "joins");
|
|
1427
1303
|
await ensureStateDir(joinsDir);
|
|
1428
1304
|
await fs2.rm(joinsDir, { recursive: true, force: true });
|
|
1305
|
+
await client.resume?.();
|
|
1429
1306
|
})();
|
|
1307
|
+
void initialize().catch(() => {
|
|
1308
|
+
});
|
|
1430
1309
|
return {
|
|
1431
1310
|
async join(link, label2) {
|
|
1432
1311
|
await initialize();
|
|
@@ -1435,7 +1314,7 @@ function createRealClientFactory(env, deps = {}) {
|
|
|
1435
1314
|
async status(channel) {
|
|
1436
1315
|
await initialize();
|
|
1437
1316
|
const status = await (channel === void 0 ? client.status() : client.status(channel));
|
|
1438
|
-
return
|
|
1317
|
+
return adapter?.watcherStatus && ["connected", "send_failed"].includes(status.state) ? { ...status, watcherArmed: await monitorArmed(files) } : status;
|
|
1439
1318
|
},
|
|
1440
1319
|
async read(limit, before, channel) {
|
|
1441
1320
|
await initialize();
|
|
@@ -1507,17 +1386,21 @@ async function runMcpCommand(argv, deps) {
|
|
|
1507
1386
|
return 2;
|
|
1508
1387
|
}
|
|
1509
1388
|
const clients = /* @__PURE__ */ new Map();
|
|
1389
|
+
const clientForSession = (sessionId) => {
|
|
1390
|
+
let client = clients.get(sessionId);
|
|
1391
|
+
if (client === void 0) {
|
|
1392
|
+
client = deps.createClient({ harness, sessionId });
|
|
1393
|
+
clients.set(sessionId, client);
|
|
1394
|
+
}
|
|
1395
|
+
return client;
|
|
1396
|
+
};
|
|
1397
|
+
const startupSession = resolveSessionId(harness, void 0, env);
|
|
1398
|
+
if (startupSession !== null && harness !== "cursor") clientForSession(startupSession);
|
|
1510
1399
|
const tools = createKhalaTools({
|
|
1511
1400
|
harness,
|
|
1512
1401
|
clientFor(meta) {
|
|
1513
1402
|
const sessionId = resolveSessionId(harness, meta, env);
|
|
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;
|
|
1403
|
+
return sessionId === null ? null : clientForSession(sessionId);
|
|
1521
1404
|
}
|
|
1522
1405
|
});
|
|
1523
1406
|
try {
|