khala-cli 0.4.9 → 0.4.11

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 (48) hide show
  1. package/dist/THIRD_PARTY_NOTICES.txt +28 -0
  2. package/dist/{chunk-DJM23SIU.mjs → chunk-3KHLSF4A.mjs} +7 -8
  3. package/dist/{chunk-PDZ5Z5ZE.mjs → chunk-4CENUF7M.mjs} +24 -9
  4. package/dist/{chunk-QKWEZXR3.mjs → chunk-4PUNVPC3.mjs} +12 -4
  5. package/dist/{chunk-WDOHRV3D.mjs → chunk-7FFHZ7EI.mjs} +14 -14
  6. package/dist/chunk-AQX2OYEF.mjs +19 -0
  7. package/dist/{chunk-ITFD3JZS.mjs → chunk-BVG7PJNZ.mjs} +424 -78
  8. package/dist/{chunk-NGSYOVQ4.mjs → chunk-CXVANPY5.mjs} +2 -2
  9. package/dist/chunk-CYINL2V3.mjs +20 -0
  10. package/dist/{chunk-KMLXOPBQ.mjs → chunk-E5QRD6W3.mjs} +4 -2
  11. package/dist/{chunk-FO5GXCST.mjs → chunk-FGHPOKXG.mjs} +5 -5
  12. package/dist/{chunk-FZEH2IXZ.mjs → chunk-GVZD4TLW.mjs} +180 -26
  13. package/dist/{chunk-V4F3SYKW.mjs → chunk-H2L5GQ5W.mjs} +25 -17
  14. package/dist/chunk-HZH74L5T.mjs +901 -0
  15. package/dist/{chunk-NPSQNCOY.mjs → chunk-JNSB3E6C.mjs} +13 -4
  16. package/dist/{chunk-7R3EIDGR.mjs → chunk-LDW4HOQW.mjs} +1 -1
  17. package/dist/chunk-LNKI3CZO.mjs +15 -0
  18. package/dist/{chunk-TEFOQXBK.mjs → chunk-MAIMFERG.mjs} +3 -3
  19. package/dist/chunk-MYIZQCRW.mjs +27 -0
  20. package/dist/{chunk-TANCKTDK.mjs → chunk-N6UYX3SY.mjs} +72 -5
  21. package/dist/{chunk-6BO3JSUP.mjs → chunk-NE4S7N3R.mjs} +2 -2
  22. package/dist/{chunk-BLCRVRIK.mjs → chunk-NJ5ICCVZ.mjs} +456 -87
  23. package/dist/{chunk-3R6HFSLL.mjs → chunk-NN5RHU6C.mjs} +92 -38
  24. package/dist/{chunk-NIX5F4LT.mjs → chunk-NOKTVPSY.mjs} +330 -66
  25. package/dist/{chunk-3KDOTR54.mjs → chunk-NUQSDSVT.mjs} +1 -1
  26. package/dist/{chunk-BEO7WIIM.mjs → chunk-OSVXLZKJ.mjs} +28 -8
  27. package/dist/{chunk-SSO5RFOX.mjs → chunk-R5NDTJRA.mjs} +1 -1
  28. package/dist/{chunk-K7V7X6WU.mjs → chunk-TVQ4MSEI.mjs} +21 -4
  29. package/dist/{chunk-BV45NK7K.mjs → chunk-VWWI4S5A.mjs} +100 -6
  30. package/dist/{chunk-UK5GJO4Y.mjs → chunk-WA5LB5GN.mjs} +6 -5
  31. package/dist/chunk-X3BAMHAR.mjs +14 -0
  32. package/dist/{chunk-PAJ4JJPV.mjs → chunk-YDEZFVF7.mjs} +43 -6
  33. package/dist/{chunk-GSZADJMW.mjs → chunk-YFX5NDSL.mjs} +338 -81
  34. package/dist/{chunk-ANSR4IAW.mjs → chunk-YIKJI26V.mjs} +2 -2
  35. package/dist/{chunk-T7IL7V7Q.mjs → chunk-YNUCRFM2.mjs} +13 -10
  36. package/dist/iterm2_send.py +56 -0
  37. package/dist/khala.mjs +11 -11
  38. package/dist/web/assets/local-DPcMk9Xz.css +1 -0
  39. package/dist/web/assets/local-SDOFp8OA.js +9 -0
  40. package/dist/web/index.html +2 -2
  41. package/package.json +1 -1
  42. package/dist/chunk-47XNLAJG.mjs +0 -38
  43. package/dist/chunk-LBT47HLZ.mjs +0 -19
  44. package/dist/chunk-P3YIN7G7.mjs +0 -26
  45. package/dist/chunk-XBQDIKB5.mjs +0 -18
  46. package/dist/chunk-XUCWSYVS.mjs +0 -148
  47. package/dist/web/assets/local-CFzRHZpB.js +0 -9
  48. package/dist/web/assets/local-ciypUFoK.css +0 -1
@@ -3,32 +3,33 @@ import {
3
3
  WAKE_HARNESSES,
4
4
  wakeDrivers,
5
5
  wakeStatus
6
- } from "./chunk-XUCWSYVS.mjs";
6
+ } from "./chunk-HZH74L5T.mjs";
7
7
  import {
8
8
  adapterFor,
9
+ registerAntigravityWake,
9
10
  resolveSources
10
- } from "./chunk-ITFD3JZS.mjs";
11
+ } from "./chunk-BVG7PJNZ.mjs";
11
12
  import {
12
13
  CURSOR_DEFAULT_SESSION,
13
14
  updateWakeSettings
14
- } from "./chunk-NIX5F4LT.mjs";
15
+ } from "./chunk-NOKTVPSY.mjs";
15
16
  import {
16
17
  SESSION_ID_PATTERN,
17
18
  filesForDir,
18
19
  stateRoot
19
- } from "./chunk-NPSQNCOY.mjs";
20
+ } from "./chunk-JNSB3E6C.mjs";
20
21
  import {
21
22
  resetWakeDriver
22
- } from "./chunk-K7V7X6WU.mjs";
23
+ } from "./chunk-TVQ4MSEI.mjs";
23
24
  import {
24
25
  init_define_KHALA_BUNDLE
25
- } from "./chunk-7R3EIDGR.mjs";
26
+ } from "./chunk-LDW4HOQW.mjs";
26
27
 
27
28
  // src/wake/cli.ts
28
29
  init_define_KHALA_BUNDLE();
29
30
  import * as fs from "node:fs/promises";
30
31
  import path from "node:path";
31
- var WAKE_USAGE = "usage: khala wake on|off|status [--driver <d>] [--harness <id>] [--json]";
32
+ var WAKE_USAGE = "usage: khala wake on|off|status|register [--driver <d>] [--harness <id>] [--json]";
32
33
  async function sessionIds(harness, env) {
33
34
  try {
34
35
  return (await fs.readdir(path.join(stateRoot(env), harness), { withFileTypes: true })).filter((entry) => entry.isDirectory() && SESSION_ID_PATTERN.test(entry.name)).map((entry) => entry.name);
@@ -53,10 +54,15 @@ async function setWake(harness, drivers, on, env) {
53
54
  }
54
55
  });
55
56
  if (on) for (const id of await sessionIds(harness, env)) {
56
- for (const driver of drivers) await resetWakeDriver(filesForDir(path.join(root, harness, id)).dir, driver);
57
+ for (const driver of drivers) {
58
+ const dir = filesForDir(path.join(root, harness, id)).dir;
59
+ await resetWakeDriver(dir, driver);
60
+ if (harness === "qwen" && driver === "socket") await fs.rm(path.join(dir, "qwen-receipt.json"), { force: true });
61
+ }
57
62
  }
58
63
  }
59
64
  function consentLine(harness, drivers, on) {
65
+ if (harness === "antigravity" && on) return `Idle wake is on (${drivers.join(", ")}): native wake preserves drafts; terminal fallback requires an empty prompt. Wake and Sync continuation use billed model turns. Run \`khala wake off --harness antigravity\` to turn it off.`;
60
66
  return on ? `Idle wake is on (${drivers.join(", ")}): Khala may send a fixed wake line into this agent's existing session when messages wait and ${harness === "muse" ? "the session is idle (through Muse\u2019s native monitor)" : "the prompt is empty"}. Run \`khala wake off --harness ${harness}\` to turn it off.` : `Idle wake is off for ${drivers.join(", ")}. Run \`khala wake on --harness ${harness}\` to turn it on.`;
61
67
  }
62
68
  async function currentSessionId(harness, env) {
@@ -85,6 +91,20 @@ async function runWake(argv, deps = {}) {
85
91
  return 2;
86
92
  }
87
93
  }
94
+ if (command === "register") {
95
+ if (harness !== "antigravity" || driver || json) {
96
+ err(WAKE_USAGE);
97
+ return 2;
98
+ }
99
+ try {
100
+ await registerAntigravityWake(env);
101
+ out("Registered Antigravity native wake for this session.");
102
+ return 0;
103
+ } catch {
104
+ err("khala: Antigravity wake registration unavailable; run this command through the agent shell.");
105
+ return 1;
106
+ }
107
+ }
88
108
  if (!["on", "off", "status"].includes(command ?? "")) {
89
109
  err(WAKE_USAGE);
90
110
  return 2;
@@ -1,7 +1,7 @@
1
1
  import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
2
2
  import {
3
3
  init_define_KHALA_BUNDLE
4
- } from "./chunk-7R3EIDGR.mjs";
4
+ } from "./chunk-LDW4HOQW.mjs";
5
5
 
6
6
  // src/events/member-rename.ts
7
7
  init_define_KHALA_BUNDLE();
@@ -1,7 +1,7 @@
1
1
  import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
2
2
  import {
3
3
  init_define_KHALA_BUNDLE
4
- } from "./chunk-7R3EIDGR.mjs";
4
+ } from "./chunk-LDW4HOQW.mjs";
5
5
 
6
6
  // src/wake/shared/nonce.ts
7
7
  init_define_KHALA_BUNDLE();
@@ -138,8 +138,14 @@ async function settleAttempts(dir, input) {
138
138
  const baselineAt = typeof attempt.activityUpdatedAt === "number" ? attempt.activityUpdatedAt : attempt.activityUpdatedAt === void 0 ? attempt.at : Date.parse(attempt.activityUpdatedAt);
139
139
  const verified = input.promptText?.includes(`(k-${attempt.nonce})`);
140
140
  const activityChanged = input.activity && (input.activity.state !== "idle" || activityAt > baselineAt);
141
- if (verified && input.verifiedAt !== void 0 && input.verifiedAt >= attempt.at && input.verifiedAt <= attempt.deadline) status = "success";
142
- else if (activityChanged && !(attempt.startsActivity && input.activity?.state === "busy" && input.promptText === void 0)) status = "void";
141
+ if (input.deferVerification && !activityChanged) attempt.verificationDeferred = true;
142
+ if (attempt.verification === "transcript") {
143
+ if (input.now >= attempt.deadline && input.activity?.state === "idle") status = "failure";
144
+ } else if (verified && input.verifiedAt !== void 0 && input.verifiedAt >= attempt.at && input.verifiedAt <= attempt.deadline) status = "success";
145
+ else if (attempt.verificationDeferred) {
146
+ if (verified && input.now <= attempt.deadline) status = "success";
147
+ else if (input.now >= attempt.deadline) status = activityChanged ? "void" : "failure";
148
+ } else if (activityChanged && !(attempt.startsActivity && input.activity?.state === "busy" && input.promptText === void 0)) status = "void";
143
149
  else if (verified && input.verifiedAt === void 0 && input.now <= attempt.deadline) status = "success";
144
150
  else if (input.now >= attempt.deadline && input.activity?.state === "idle") status = "failure";
145
151
  else if (input.promptText !== void 0) status = "void";
@@ -192,8 +198,18 @@ async function takeWakeDisableNotices(dir) {
192
198
  return drivers;
193
199
  });
194
200
  }
201
+ async function confirmWakeText(dir, driver, text, now) {
202
+ await locked(dir, (data) => {
203
+ data.attempts = data.attempts.filter((attempt) => {
204
+ if (attempt.driver !== driver || now > attempt.deadline || !text.includes(`(k-${attempt.nonce})`)) return true;
205
+ data.state[driver] = { failures: 0 };
206
+ return false;
207
+ });
208
+ });
209
+ }
195
210
 
196
211
  export {
212
+ CODEX_IDLE_WAKE_NOTICE,
197
213
  newNonce,
198
214
  wakeLine,
199
215
  withWakeLock,
@@ -203,5 +219,6 @@ export {
203
219
  cancelAttempt,
204
220
  failAttempt,
205
221
  resetWakeDriver,
206
- takeWakeDisableNotices
222
+ takeWakeDisableNotices,
223
+ confirmWakeText
207
224
  };
@@ -5,20 +5,22 @@ import {
5
5
  fail,
6
6
  label,
7
7
  literal,
8
+ object,
8
9
  safeInteger,
9
10
  utcTimestamp,
10
11
  utf8Length,
11
12
  version
12
- } from "./chunk-NPSQNCOY.mjs";
13
+ } from "./chunk-JNSB3E6C.mjs";
13
14
  import {
14
15
  init_define_KHALA_BUNDLE
15
- } from "./chunk-7R3EIDGR.mjs";
16
+ } from "./chunk-LDW4HOQW.mjs";
16
17
 
17
18
  // ../contracts/src/m1/channel-event.ts
18
19
  init_define_KHALA_BUNDLE();
19
20
  var CHANNEL_EVENT_TYPE = "com.khala.event.v1";
20
21
  var CHANNEL_EVENT_STATUSES = ["info", "pending", "success", "failure", "attention"];
21
22
  var MAX_CHANNEL_EVENT_BYTES = 4096;
23
+ var COORDINATION_KINDS = ["handoff", "handoff_ack", "verdict", "claim", "release", "note"];
22
24
  function record(input, path) {
23
25
  if (typeof input !== "object" || input === null || Array.isArray(input)) fail(path, "not_object");
24
26
  const prototype = Object.getPrototypeOf(input);
@@ -76,10 +78,66 @@ function decodeChannelEvent(raw) {
76
78
  ...r.has("url") ? { url: readUrl(r.field("url")) } : {},
77
79
  ...r.has("occurred_at") ? { occurred_at: utcTimestamp(r.field("occurred_at"), "occurred_at") } : {},
78
80
  ...r.has("source") ? { source: readSource(r.field("source")) } : {},
79
- ...r.has("key") ? { key: nonempty(label(r.field("key"), "key", 200), "key") } : {}
81
+ ...r.has("key") ? { key: nonempty(label(r.field("key"), "key", 200), "key") } : {},
82
+ // Older senders may use these kind names without data: such events stay plain events.
83
+ ...r.has("data") && isCoordinationKind(kind) ? { data: readData(kind, r.field("data")) } : {}
80
84
  };
81
85
  }));
82
86
  }
87
+ function isCoordinationKind(kind) {
88
+ return COORDINATION_KINDS.includes(kind);
89
+ }
90
+ var short = (input, path) => display(input, path, 256, 64);
91
+ var reason = (input, path) => display(input, path, 800, 200);
92
+ function readData(kind, input) {
93
+ const fields = {
94
+ handoff: ["ticket", "from", "to", "reason", "state"],
95
+ handoff_ack: ["ticket"],
96
+ verdict: ["pr", "verdict", "reason"],
97
+ claim: ["key"],
98
+ release: ["key"],
99
+ note: ["key", "value"]
100
+ }[kind];
101
+ const r = object(input, "data", fields);
102
+ const read = (key) => {
103
+ const value = r.field(key);
104
+ switch (key) {
105
+ case "pr": {
106
+ const pr = safeInteger(value, r.at(key));
107
+ if (pr === 0) fail(r.at(key), "invalid_value");
108
+ return pr;
109
+ }
110
+ case "verdict":
111
+ return literal(value, r.at(key), ["approved", "changes_requested"]);
112
+ case "reason":
113
+ return reason(value, r.at(key));
114
+ case "key":
115
+ return nonempty(label(value, r.at(key), 128), r.at(key));
116
+ // An empty value clears the note.
117
+ case "value":
118
+ return typeof value === "string" && value === "" ? "" : reason(value, r.at(key));
119
+ default:
120
+ return short(value, r.at(key));
121
+ }
122
+ };
123
+ return Object.fromEntries(fields.map((key) => [key, read(key)]));
124
+ }
125
+ function coordinationSummary(kind, d) {
126
+ switch (kind) {
127
+ case "handoff":
128
+ return `handoff ${d.ticket} ${d.from} \u2192 ${d.to} (${d.state}): ${d.reason}`;
129
+ case "handoff_ack":
130
+ return `handoff ${d.ticket} acknowledged`;
131
+ case "verdict":
132
+ return `PR #${d.pr} ${d.verdict === "approved" ? "approved" : "changes requested"}: ${d.reason}`;
133
+ case "claim":
134
+ return `claimed ${d.key}`;
135
+ case "release":
136
+ return `released ${d.key}`;
137
+ case "note":
138
+ return d.value === "" ? `note ${d.key} cleared` : `note ${d.key} = ${d.value}`;
139
+ }
140
+ }
83
141
  function readSubject(input) {
84
142
  const r = record(input, "subject");
85
143
  let pr;
@@ -127,8 +185,23 @@ function formatChannelEventLine(content) {
127
185
  function encodeChannelEvent(input) {
128
186
  return safely(() => {
129
187
  const checked = decodeWith(() => {
130
- record(input, "");
131
- return { ...input, v: 1, body: "" };
188
+ const r = record(input, "");
189
+ const raw = input;
190
+ const kind = raw.kind;
191
+ if (typeof kind !== "string" || !isCoordinationKind(kind)) {
192
+ if (r.has("data")) fail("data", "invalid_value");
193
+ return { ...raw, v: 1, body: "" };
194
+ }
195
+ const data = readData(kind, r.field("data"));
196
+ const derived = coordinationSummary(kind, data);
197
+ return {
198
+ ...raw,
199
+ v: 1,
200
+ body: "",
201
+ data,
202
+ ...r.has("summary") ? {} : { summary: [...derived].length > 200 ? [...derived].slice(0, 199).join("") + "\u2026" : derived },
203
+ ...r.has("status") || kind !== "verdict" ? {} : { status: data.verdict === "approved" ? "success" : "attention" }
204
+ };
132
205
  });
133
206
  if (!checked.ok) return checked;
134
207
  const decoded = decodeChannelEvent(checked.value);
@@ -163,6 +236,26 @@ function statusFor(content) {
163
236
  return content.kind.startsWith("agent.attention.") && !content.kind.endsWith(".resolved") ? "attention" : "info";
164
237
  }
165
238
  }
239
+ function foldCoordination(events) {
240
+ const handoffs = /* @__PURE__ */ new Map();
241
+ const claims = /* @__PURE__ */ new Map();
242
+ const notes = /* @__PURE__ */ new Map();
243
+ for (const { sender, by, at, content: { kind, data } } of events) {
244
+ if (!data) continue;
245
+ const s = (key) => String(data[key]);
246
+ if (kind === "handoff") {
247
+ handoffs.delete(s("ticket"));
248
+ handoffs.set(s("ticket"), { ticket: s("ticket"), from: s("from"), to: s("to"), reason: s("reason"), state: s("state"), by, at });
249
+ } else if (kind === "handoff_ack") handoffs.delete(s("ticket"));
250
+ else if (kind === "claim" && !claims.has(s("key"))) claims.set(s("key"), { key: s("key"), by, sender, at });
251
+ else if (kind === "release" && claims.get(s("key"))?.sender === sender) claims.delete(s("key"));
252
+ else if (kind === "note") {
253
+ notes.delete(s("key"));
254
+ if (s("value") !== "") notes.set(s("key"), s("value"));
255
+ }
256
+ }
257
+ return { openHandoffs: [...handoffs.values()], activeClaims: [...claims.values()], notes: Object.fromEntries(notes) };
258
+ }
166
259
 
167
260
  export {
168
261
  CHANNEL_EVENT_TYPE,
@@ -170,5 +263,6 @@ export {
170
263
  formatChannelEventLine,
171
264
  encodeChannelEvent,
172
265
  memberRenameContent,
173
- statusFor
266
+ statusFor,
267
+ foldCoordination
174
268
  };
@@ -1,22 +1,23 @@
1
1
  import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
2
2
  import {
3
3
  sessionStartWakeChannels
4
- } from "./chunk-TEFOQXBK.mjs";
4
+ } from "./chunk-MAIMFERG.mjs";
5
5
  import {
6
6
  capturePane
7
- } from "./chunk-QKWEZXR3.mjs";
7
+ } from "./chunk-4PUNVPC3.mjs";
8
8
  import {
9
9
  listChannels,
10
10
  writeActivity
11
- } from "./chunk-TANCKTDK.mjs";
11
+ } from "./chunk-N6UYX3SY.mjs";
12
+ import "./chunk-VWWI4S5A.mjs";
12
13
  import {
13
14
  TERMINAL_SESSION_DETAILS,
14
15
  openSessionDir,
15
16
  readJson
16
- } from "./chunk-NPSQNCOY.mjs";
17
+ } from "./chunk-JNSB3E6C.mjs";
17
18
  import {
18
19
  init_define_KHALA_BUNDLE
19
- } from "./chunk-7R3EIDGR.mjs";
20
+ } from "./chunk-LDW4HOQW.mjs";
20
21
 
21
22
  // hooks/session-start.ts
22
23
  init_define_KHALA_BUNDLE();
@@ -0,0 +1,14 @@
1
+ import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
2
+ import {
3
+ init_define_KHALA_BUNDLE
4
+ } from "./chunk-LDW4HOQW.mjs";
5
+
6
+ // src/transport.ts
7
+ init_define_KHALA_BUNDLE();
8
+ var RESTORE_HISTORY_MAX_PAGES = 20;
9
+ var startChannelSession = async (creds, options) => creds.transport === "local" ? (await import("./chunk-YNUCRFM2.mjs")).createLocalSession(creds) : (await import("./chunk-GVZD4TLW.mjs")).createAgentMatrixSession(creds, options);
10
+
11
+ export {
12
+ RESTORE_HISTORY_MAX_PAGES,
13
+ startChannelSession
14
+ };
@@ -1,15 +1,18 @@
1
1
  import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
2
+ import {
3
+ retryUnexpired
4
+ } from "./chunk-LNKI3CZO.mjs";
2
5
  import {
3
6
  StateError,
4
7
  ensureStateDir,
5
8
  writeJsonAtomic
6
- } from "./chunk-NPSQNCOY.mjs";
9
+ } from "./chunk-JNSB3E6C.mjs";
7
10
  import {
8
11
  __commonJS,
9
12
  __export,
10
13
  __toESM,
11
14
  init_define_KHALA_BUNDLE
12
- } from "./chunk-7R3EIDGR.mjs";
15
+ } from "./chunk-LDW4HOQW.mjs";
13
16
 
14
17
  // ../../node_modules/.pnpm/immediate@3.3.0/node_modules/immediate/lib/nextTick.js
15
18
  var require_nextTick = __commonJS({
@@ -9623,7 +9626,8 @@ async function readIdentity(file) {
9623
9626
  const stat = await handle.stat();
9624
9627
  if (!stat.isFile() || process.platform !== "win32" && (stat.mode & 63) !== 0 || typeof process.getuid === "function" && stat.uid !== process.getuid()) throw new StateError("unsafe_state_dir");
9625
9628
  const value = JSON.parse(await handle.readFile("utf8"));
9626
- if (!value || typeof value.homeserver !== "string" || typeof value.userId !== "string" || typeof value.deviceId !== "string" || value.accessToken !== void 0 && typeof value.accessToken !== "string" || value.joinedAt !== void 0 && !Number.isFinite(value.joinedAt)) throw new CryptoStoreCorruptError();
9629
+ if (!value || typeof value.homeserver !== "string" || typeof value.userId !== "string" || typeof value.deviceId !== "string" || value.accessToken !== void 0 && typeof value.accessToken !== "string" || value.joinedAt !== void 0 && !Number.isFinite(value.joinedAt) || value.undecryptableEventIds !== void 0 && (!Array.isArray(value.undecryptableEventIds) || value.undecryptableEventIds.length > 100 || value.undecryptableEventIds.some((entry) => typeof entry !== "string" && (!entry || typeof entry !== "object" || typeof entry.id !== "string" || typeof entry.firstSeen !== "number" || !Number.isFinite(entry.firstSeen) || entry.firstSeen < 0 || entry.firstSeen > Number.MAX_SAFE_INTEGER)))) throw new CryptoStoreCorruptError();
9630
+ if (value.undecryptableEventIds) value.undecryptableEventIds = value.undecryptableEventIds.filter((entry) => typeof entry !== "string" && retryUnexpired(entry));
9627
9631
  return value;
9628
9632
  } catch (error) {
9629
9633
  if (error instanceof StateError) throw error;
@@ -9735,18 +9739,32 @@ async function openCryptoStore(dir, root, creds) {
9735
9739
  if (!restored) {
9736
9740
  await deleteStores(prefix, dir);
9737
9741
  }
9738
- await writeJsonAtomic(identityFile, { ...identity, ...restored && previous?.joinedAt !== void 0 ? { joinedAt: previous?.joinedAt } : {} });
9742
+ const saved = { ...identity, ...restored && previous?.joinedAt !== void 0 ? { joinedAt: previous.joinedAt } : {}, ...restored && previous?.undecryptableEventIds ? { undecryptableEventIds: previous.undecryptableEventIds } : {} };
9743
+ await writeJsonAtomic(identityFile, saved);
9739
9744
  const sync = new DurableSyncStore({ indexedDB: globalThis.indexedDB, dbName: prefix });
9740
9745
  let closed;
9746
+ let identitySave = Promise.resolve();
9747
+ const saveIdentity = () => {
9748
+ const saving = identitySave.then(() => writeJsonAtomic(identityFile, saved));
9749
+ identitySave = saving.catch(() => {
9750
+ });
9751
+ return saving;
9752
+ };
9741
9753
  return {
9742
9754
  prefix,
9743
9755
  sync,
9744
9756
  restored,
9745
9757
  ...recovered ? { recovered } : {},
9746
9758
  ...restored && previous?.joinedAt !== void 0 ? { joinedAt: previous?.joinedAt } : {},
9759
+ undecryptableEventIds: saved.undecryptableEventIds ?? [],
9760
+ async rememberUndecryptable(ids) {
9761
+ saved.undecryptableEventIds = ids.filter((entry) => retryUnexpired(entry)).slice(-100);
9762
+ await saveIdentity();
9763
+ },
9747
9764
  async rememberJoin(ts) {
9748
9765
  if (this.joinedAt === void 0) {
9749
- await writeJsonAtomic(identityFile, { ...identity, joinedAt: ts });
9766
+ saved.joinedAt = ts;
9767
+ await saveIdentity();
9750
9768
  this.joinedAt = ts;
9751
9769
  }
9752
9770
  },
@@ -9767,7 +9785,7 @@ async function openCryptoStore(dir, root, creds) {
9767
9785
  throw error;
9768
9786
  }
9769
9787
  }
9770
- async function wipeCryptoStore(dir, root) {
9788
+ async function wipeCryptoStore(dir, root, fetcher = fetch) {
9771
9789
  try {
9772
9790
  const names = await fs3.readdir(dir);
9773
9791
  if (!["crypto.json", "crypto.sqlite", "sync.sqlite"].some((name) => names.includes(name))) return;
@@ -9784,6 +9802,25 @@ async function wipeCryptoStore(dir, root) {
9784
9802
  retries: { retries: 12, minTimeout: 1e3, maxTimeout: 1e3 }
9785
9803
  });
9786
9804
  try {
9805
+ let identity;
9806
+ try {
9807
+ identity = await readIdentity(path.join(dir, "crypto.json"));
9808
+ } catch {
9809
+ identity = null;
9810
+ }
9811
+ if (identity?.accessToken) {
9812
+ try {
9813
+ const response = await fetcher(`${identity.homeserver}/_matrix/client/v3/logout`, {
9814
+ method: "POST",
9815
+ headers: { authorization: `Bearer ${identity.accessToken}`, "content-type": "application/json" },
9816
+ body: "{}",
9817
+ signal: AbortSignal.timeout(5e3)
9818
+ });
9819
+ if (!response.ok && response.status !== 401) console.error("khala: discarded_device_logout_failed");
9820
+ } catch {
9821
+ console.error("khala: discarded_device_logout_failed");
9822
+ }
9823
+ }
9787
9824
  await unlink2(path.join(dir, "crypto.json"));
9788
9825
  await deleteStores(prefix, dir);
9789
9826
  } finally {