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.
Files changed (30) hide show
  1. package/dist/{chunk-TZKEQIHP.mjs → chunk-2U4E7AM7.mjs} +18 -3
  2. package/dist/{chunk-EXHCVFVH.mjs → chunk-2XTYRBYX.mjs} +10 -4
  3. package/dist/{chunk-MBLP7LQJ.mjs → chunk-4X35OUAN.mjs} +11 -11
  4. package/dist/{chunk-PXKGKVGW.mjs → chunk-55KBLEX5.mjs} +1 -1
  5. package/dist/{chunk-RSDOTTZQ.mjs → chunk-6E3UCO22.mjs} +32 -11
  6. package/dist/{chunk-7G2G4E4U.mjs → chunk-AWBKVTLV.mjs} +15 -13
  7. package/dist/{chunk-V7M27QLT.mjs → chunk-B2MOLCJM.mjs} +7 -5
  8. package/dist/{chunk-IR3VN2ID.mjs → chunk-CWHGNVZV.mjs} +0 -18
  9. package/dist/chunk-DPW43YNC.mjs +36 -0
  10. package/dist/{chunk-V4Y26O4R.mjs → chunk-FH2CZX5T.mjs} +14 -12
  11. package/dist/{chunk-HB3UGIZG.mjs → chunk-IULQYQJZ.mjs} +3 -3
  12. package/dist/{chunk-Y4WVILEJ.mjs → chunk-JG5J3SHW.mjs} +1 -1
  13. package/dist/{chunk-Y2DRTRBJ.mjs → chunk-OECJ45NM.mjs} +2 -2
  14. package/dist/{chunk-KCJFSVDN.mjs → chunk-OW3DSTEZ.mjs} +4 -2
  15. package/dist/{chunk-OLZ62X7S.mjs → chunk-PFDUXY3N.mjs} +1 -1
  16. package/dist/{chunk-H46OG22B.mjs → chunk-PY2ZNVNY.mjs} +1 -1
  17. package/dist/{chunk-SQXDATEC.mjs → chunk-RRBIVI77.mjs} +1 -1
  18. package/dist/{chunk-6LC66VHK.mjs → chunk-SZIEG3RS.mjs} +6 -5
  19. package/dist/chunk-WHONG7UF.mjs +267 -0
  20. package/dist/{chunk-T7YRVE43.mjs → chunk-XUJIJ4KB.mjs} +8 -30
  21. package/dist/{chunk-EW22K3JO.mjs → chunk-Z4EDYB6Y.mjs} +176 -293
  22. package/dist/chunk-ZHMGB3PP.mjs +82 -0
  23. package/dist/khala.mjs +10 -10
  24. package/dist/web/assets/local-K8EcTVxW.js +9 -0
  25. package/dist/web/assets/local-R2y0Gl4T.css +1 -0
  26. package/dist/web/index.html +2 -2
  27. package/package.json +1 -1
  28. package/dist/chunk-VUE2DDYY.mjs +0 -34
  29. package/dist/web/assets/local-C1vz22ps.js +0 -9
  30. 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-Y2DRTRBJ.mjs";
8
+ } from "./chunk-OECJ45NM.mjs";
9
9
  import {
10
10
  KHALA_AGENT_VERSION
11
- } from "./chunk-SQXDATEC.mjs";
11
+ } from "./chunk-RRBIVI77.mjs";
12
12
  import {
13
13
  createEventKeyFilter,
14
14
  isWakeEntry,
15
- readActivity,
16
15
  toEventInboxEntry
17
- } from "./chunk-IR3VN2ID.mjs";
16
+ } from "./chunk-CWHGNVZV.mjs";
18
17
  import {
19
18
  KhalaClientError,
20
19
  LOCAL_OWNER_USER_ID,
21
20
  MODEL_NAMES
22
- } from "./chunk-TZKEQIHP.mjs";
23
- import "./chunk-OLZ62X7S.mjs";
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-7G2G4E4U.mjs";
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-T7YRVE43.mjs";
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-KCJFSVDN.mjs";
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-PXKGKVGW.mjs";
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
- return JSON.stringify(status) + (status.watcherArmed === false ? "\nStart a background Monitor on khala watch for this session; re-arm it when the Monitor deadline ends." : "");
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-6LC66VHK.mjs")).createLocalSession(creds) : (await import("./chunk-Y4WVILEJ.mjs")).createAgentMatrixSession(creds, options);
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 = !(options.harness === "cursor" && options.sessionId === CURSOR_DEFAULT_SESSION);
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 writeStateFile(ref.files.dir, "status.json", { state: "disconnected", channelName: ref.channelName, updatedAt: now().toISOString() });
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 setStatus(attempt, "disconnected", reason).catch(() => {
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)) await setStatus(attempt, "connected");
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 failure2 = safeError(error);
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(error, failure2.code));
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(error, failure2.code)
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
- return {
1006
- join(link, label2) {
1007
- const task = (joins.get(link) ?? Promise.resolve()).catch(() => {
1008
- }).then(async () => {
1009
- await initialize();
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
- const existing = attempts.get(link);
1012
- if (existing && current(existing)) {
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
- const attempt = {
1025
- link,
1026
- created: { origin: "", joinId: "", pollSecret: "", confirmUrl: "", expiresAt: "" },
1027
- controller: new AbortController(),
1028
- task: Promise.resolve(),
1029
- joined: false,
1030
- status: { state: "joining", updatedAt: now().toISOString() },
1031
- appends: Promise.resolve(),
1032
- acceptEventKey: createEventKeyFilter()
1033
- };
1034
- attempts.set(link, attempt);
1035
- try {
1036
- const created = await api.requestJoin({ link, harness: options.harness, label: label2, ...rejoinSecret === void 0 ? {} : { sessionId: options.sessionId, rejoinSecret } }, fetchDeps);
1037
- if (closed) throw new KhalaClientError("not_connected");
1038
- attempt.created = created;
1039
- const { joinId, pollSecret, confirmUrl, expiresAt } = created;
1040
- await writeJoinFile(filesForDir(dir), link, { joinId, pollSecret, confirmUrl, expiresAt, link });
1041
- await writeAggregate();
1042
- attempt.task = background(attempt).catch(() => {
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
- if (created.autoConfirmed !== true) return { state: "awaiting_confirmation", confirmUrl };
1045
- } catch (error) {
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 waker = harness === "codex" ? (deps.createWaker ?? createCodexWaker)({ files, threadId: sessionId }) : void 0;
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 harness === "claude" && ["connected", "send_failed"].includes(status.state) ? { ...status, watcherArmed: await monitorArmed(files) } : status;
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
- if (sessionId === null) return null;
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 {