khala-cli 0.4.9 → 0.4.10

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 (45) hide show
  1. package/dist/THIRD_PARTY_NOTICES.txt +28 -0
  2. package/dist/{chunk-NIX5F4LT.mjs → chunk-2AMWQ7RB.mjs} +330 -66
  3. package/dist/{chunk-PAJ4JJPV.mjs → chunk-3E5DH3Y6.mjs} +43 -6
  4. package/dist/{chunk-NGSYOVQ4.mjs → chunk-4BP7V3OP.mjs} +2 -2
  5. package/dist/{chunk-TEFOQXBK.mjs → chunk-4BZQ4GVW.mjs} +3 -3
  6. package/dist/{chunk-ITFD3JZS.mjs → chunk-5JSGA6OB.mjs} +424 -78
  7. package/dist/{chunk-QKWEZXR3.mjs → chunk-6AQVW5W7.mjs} +12 -4
  8. package/dist/{chunk-UK5GJO4Y.mjs → chunk-6J2PUM7L.mjs} +6 -5
  9. package/dist/{chunk-P3YIN7G7.mjs → chunk-6SUV3EW6.mjs} +11 -10
  10. package/dist/{chunk-GSZADJMW.mjs → chunk-ARIDZOTI.mjs} +309 -81
  11. package/dist/chunk-C2WEDMAF.mjs +15 -0
  12. package/dist/{chunk-DJM23SIU.mjs → chunk-CHC4VZF5.mjs} +7 -8
  13. package/dist/{chunk-BLCRVRIK.mjs → chunk-CKLCR5G4.mjs} +456 -87
  14. package/dist/{chunk-7R3EIDGR.mjs → chunk-EVT2DWNZ.mjs} +1 -1
  15. package/dist/{chunk-KMLXOPBQ.mjs → chunk-FJVVSKZ3.mjs} +4 -2
  16. package/dist/{chunk-3KDOTR54.mjs → chunk-G3NMB4PY.mjs} +1 -1
  17. package/dist/{chunk-SSO5RFOX.mjs → chunk-HHEZVPRR.mjs} +1 -1
  18. package/dist/{chunk-BV45NK7K.mjs → chunk-JKOJQTFT.mjs} +100 -6
  19. package/dist/{chunk-T7IL7V7Q.mjs → chunk-NDMFD5IM.mjs} +13 -10
  20. package/dist/{chunk-TANCKTDK.mjs → chunk-OEV5DFLW.mjs} +70 -5
  21. package/dist/{chunk-BEO7WIIM.mjs → chunk-OIZ4R5H7.mjs} +28 -8
  22. package/dist/{chunk-FZEH2IXZ.mjs → chunk-PJKI7SRI.mjs} +176 -26
  23. package/dist/{chunk-NPSQNCOY.mjs → chunk-PN25BUQ4.mjs} +13 -4
  24. package/dist/{chunk-3R6HFSLL.mjs → chunk-QDPJ4VT2.mjs} +92 -38
  25. package/dist/{chunk-FO5GXCST.mjs → chunk-SRZTHCXP.mjs} +5 -5
  26. package/dist/{chunk-LBT47HLZ.mjs → chunk-TSK3HJEG.mjs} +10 -9
  27. package/dist/{chunk-6BO3JSUP.mjs → chunk-UX34IVN5.mjs} +2 -2
  28. package/dist/{chunk-V4F3SYKW.mjs → chunk-VAC3JTAV.mjs} +25 -17
  29. package/dist/{chunk-PDZ5Z5ZE.mjs → chunk-VDZK4LW3.mjs} +24 -9
  30. package/dist/chunk-W6HM6F2U.mjs +901 -0
  31. package/dist/{chunk-WDOHRV3D.mjs → chunk-WPNMWNCL.mjs} +14 -14
  32. package/dist/chunk-WUDXHF2Q.mjs +14 -0
  33. package/dist/{chunk-XBQDIKB5.mjs → chunk-WZJ6DQJV.mjs} +9 -8
  34. package/dist/{chunk-ANSR4IAW.mjs → chunk-XR625T3I.mjs} +2 -2
  35. package/dist/{chunk-K7V7X6WU.mjs → chunk-ZSKLNZWZ.mjs} +21 -4
  36. package/dist/iterm2_send.py +56 -0
  37. package/dist/khala.mjs +11 -11
  38. package/dist/web/assets/local-CSPuGnH2.css +1 -0
  39. package/dist/web/assets/local-Dtyxaacx.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-XUCWSYVS.mjs +0 -148
  44. package/dist/web/assets/local-CFzRHZpB.js +0 -9
  45. package/dist/web/assets/local-ciypUFoK.css +0 -1
@@ -1,26 +1,32 @@
1
1
  import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
2
- import "./chunk-SSO5RFOX.mjs";
3
2
  import {
4
- CHANNEL_EVENT_TYPE,
5
- encodeChannelEvent,
6
- memberRenameContent
7
- } from "./chunk-BV45NK7K.mjs";
3
+ retryUnexpired
4
+ } from "./chunk-C2WEDMAF.mjs";
5
+ import {
6
+ RESTORE_HISTORY_MAX_PAGES
7
+ } from "./chunk-WUDXHF2Q.mjs";
8
+ import "./chunk-HHEZVPRR.mjs";
8
9
  import {
9
10
  LISTENING_MODE_COMMAND_TYPE,
10
11
  LISTENING_MODE_MEMBER_KEY,
11
12
  memberListeningMode
12
- } from "./chunk-NGSYOVQ4.mjs";
13
+ } from "./chunk-4BP7V3OP.mjs";
14
+ import {
15
+ CHANNEL_EVENT_TYPE,
16
+ encodeChannelEvent,
17
+ memberRenameContent
18
+ } from "./chunk-JKOJQTFT.mjs";
13
19
  import {
14
20
  StateError
15
- } from "./chunk-NPSQNCOY.mjs";
21
+ } from "./chunk-PN25BUQ4.mjs";
16
22
  import {
17
23
  init_define_KHALA_BUNDLE
18
- } from "./chunk-7R3EIDGR.mjs";
24
+ } from "./chunk-EVT2DWNZ.mjs";
19
25
 
20
26
  // src/matrix/session.ts
21
27
  init_define_KHALA_BUNDLE();
22
28
  import { randomUUID } from "node:crypto";
23
- import { createClient, ClientEvent, RoomEvent, MatrixEventEvent, SyncState, Direction, Method, EventType } from "matrix-js-sdk";
29
+ import { createClient, ClientEvent, RoomEvent, MatrixEventEvent, SyncState, SetPresence, Direction, Method, EventType } from "matrix-js-sdk";
24
30
  function message(event) {
25
31
  const type = event.getType();
26
32
  const eventId = event.getId();
@@ -44,7 +50,7 @@ async function createAgentMatrixSession(creds, opts) {
44
50
  const started = Date.now();
45
51
  const rejoinTxnId = `khala.rejoin.${randomUUID()}`;
46
52
  const log = (line) => opts?.log?.(line);
47
- const persistent = opts?.cryptoStore ? await (await import("./chunk-PAJ4JJPV.mjs")).openCryptoStore(opts.cryptoStore.dir, opts.cryptoStore.root, creds) : void 0;
53
+ const persistent = opts?.cryptoStore ? await (await import("./chunk-3E5DH3Y6.mjs")).openCryptoStore(opts.cryptoStore.dir, opts.cryptoStore.root, creds) : void 0;
48
54
  const client = createClient({ ...persistent ? { store: persistent.sync } : {}, baseUrl: creds.homeserver, userId: creds.userId, accessToken: creds.accessToken, deviceId: creds.deviceId });
49
55
  const handlers = /* @__PURE__ */ new Set();
50
56
  const ended = /* @__PURE__ */ new Set();
@@ -52,13 +58,38 @@ async function createAgentMatrixSession(creds, opts) {
52
58
  const inviters = /* @__PURE__ */ new Map();
53
59
  const emitted = /* @__PURE__ */ new Set();
54
60
  const liveEvents = /* @__PURE__ */ new Set();
61
+ const undecryptableIds = new Map((persistent?.undecryptableEventIds ?? []).map((entry) => [entry.id, entry]));
62
+ let retrySave = Promise.resolve();
63
+ const saveRetries = () => {
64
+ const entries = [...undecryptableIds.values()];
65
+ retrySave = retrySave.then(() => persistent?.rememberUndecryptable(entries)).catch(() => log("decryption_retry_save_failed"));
66
+ };
67
+ const rememberDecryption = (event) => {
68
+ const id = event.getId();
69
+ if (stopped || !persistent || !id) return;
70
+ const failed = event.getType() === "m.room.encrypted" || event.isDecryptionFailure();
71
+ if (failed) {
72
+ if (undecryptableIds.has(id)) return;
73
+ undecryptableIds.set(id, { id, firstSeen: Date.now() });
74
+ } else if (!undecryptableIds.delete(id)) return;
75
+ if (undecryptableIds.size > 100) undecryptableIds.delete(undecryptableIds.keys().next().value);
76
+ saveRetries();
77
+ };
55
78
  const joinTimes = /* @__PURE__ */ new Map();
56
79
  const cancellations = /* @__PURE__ */ new Set();
57
80
  let joinedRoom;
58
81
  let stopped = false;
82
+ let presenceTimer;
83
+ let away;
84
+ const publishPresence = () => {
85
+ void client.setSyncPresence(away ? SetPresence.Offline : void 0);
86
+ void client.setPresence(away ? { presence: "offline", status_msg: away } : { presence: "online", status_msg: "" }).catch(() => log("presence_failed"));
87
+ };
59
88
  let recovering = persistent?.restored ?? false;
60
89
  const deliver = (event) => {
61
- if (stopped || recovering || !liveEvents.has(event)) return;
90
+ if (stopped) return;
91
+ if (liveEvents.has(event)) rememberDecryption(event);
92
+ if (recovering || !liveEvents.has(event)) return;
62
93
  const command = event.getType() === LISTENING_MODE_COMMAND_TYPE && !event.isDecryptionFailure() && event.getId() && event.getRoomId() && event.getSender() ? { eventId: event.getId(), roomId: event.getRoomId(), sender: event.getSender(), ts: event.getTs(), content: event.getContent() } : void 0;
63
94
  const m = command ?? message(event);
64
95
  if (!m) {
@@ -96,7 +127,10 @@ async function createAgentMatrixSession(creds, opts) {
96
127
  const timeline = (event, _room, toStart, _removed, data) => {
97
128
  if (stopped || toStart || data?.liveEvent === false && !persistent?.restored) return;
98
129
  liveEvents.add(event);
99
- void client.decryptEventIfNeeded(event).then(() => deliver(event), () => log("live_decryption_failed"));
130
+ void client.decryptEventIfNeeded(event).then(() => deliver(event), () => {
131
+ rememberDecryption(event);
132
+ log("live_decryption_failed");
133
+ });
100
134
  };
101
135
  const decrypted = (event) => deliver(event);
102
136
  const syncLog = (state, _previous, data) => {
@@ -167,6 +201,24 @@ async function createAgentMatrixSession(creds, opts) {
167
201
  const stopSession = async () => {
168
202
  if (stopped) return;
169
203
  stopped = true;
204
+ clearInterval(presenceTimer);
205
+ if (presenceTimer && !wipe) {
206
+ await Promise.race([
207
+ client.setPresence({ presence: "offline", status_msg: "session ended" }).catch(() => log("presence_failed")),
208
+ new Promise((resolve) => setTimeout(resolve, 3e3).unref())
209
+ ]);
210
+ }
211
+ await retrySave;
212
+ if (wipe) {
213
+ try {
214
+ await client.http.authedRequest(Method.Post, "/logout", void 0, {}, { localTimeoutMs: 5e3 });
215
+ } catch (error) {
216
+ if (matrixCode(error) !== "M_UNKNOWN_TOKEN" && !(typeof error === "object" && error !== null && "httpStatus" in error && error.httpStatus === 401)) {
217
+ log("discarded_device_logout_failed");
218
+ console.error("khala: discarded_device_logout_failed");
219
+ }
220
+ }
221
+ }
170
222
  if (wipe && persistent?.forgetIdentity) {
171
223
  try {
172
224
  await persistent.forgetIdentity();
@@ -236,8 +288,7 @@ async function createAgentMatrixSession(creds, opts) {
236
288
  await client.initRustCrypto(persistent ? { useIndexedDB: true, cryptoDatabasePrefix: persistent.prefix } : { useIndexedDB: false });
237
289
  } catch (error) {
238
290
  if (!persistent?.restored || error instanceof StateError && error.code === "unsafe_state_dir") throw error;
239
- wipe = true;
240
- throw new (await import("./chunk-PAJ4JJPV.mjs")).CryptoStoreCorruptError();
291
+ throw new (await import("./chunk-3E5DH3Y6.mjs")).CryptoStoreCorruptError();
241
292
  }
242
293
  const crypto = client.getCrypto();
243
294
  if (!crypto) throw new Error("crypto_unavailable");
@@ -264,6 +315,9 @@ async function createAgentMatrixSession(creds, opts) {
264
315
  }, () => client.getSyncState() === SyncState.Prepared || client.getSyncState() === SyncState.Syncing, 3e4, "sync_timeout");
265
316
  await Promise.all([client.startClient({ initialSyncLimit: 30 }), prepared]);
266
317
  log(`session_ready_ms=${Date.now() - started}`);
318
+ publishPresence();
319
+ presenceTimer = setInterval(publishPresence, 6e4);
320
+ presenceTimer.unref();
267
321
  } catch (error) {
268
322
  await stop();
269
323
  throw error;
@@ -282,6 +336,7 @@ async function createAgentMatrixSession(creds, opts) {
282
336
  return {
283
337
  userId: creds.userId,
284
338
  ...persistent?.recovered ? { cryptoReset: true } : {},
339
+ ...persistent?.restored ? { recoversOnJoin: true } : {},
285
340
  onEnded(handler) {
286
341
  if (!stopped) ended.add(handler);
287
342
  return () => {
@@ -317,7 +372,6 @@ async function createAgentMatrixSession(creds, opts) {
317
372
  async join(roomId) {
318
373
  if (stopped) throw new Error("session_stopped");
319
374
  if (joinedRoom === roomId) return;
320
- const rejoinedAt = Date.now();
321
375
  const membership = client.getRoom(roomId)?.getMyMembership();
322
376
  if (membership !== "join") {
323
377
  if (membership !== "invite") throw new Error("not_invited");
@@ -338,30 +392,80 @@ async function createAgentMatrixSession(creds, opts) {
338
392
  const content = { ...ownJoin.getContent(), membership: "join", "com.khala.invited_by": inviters.get(roomId) };
339
393
  await client.sendStateEvent(roomId, EventType.RoomMember, content, creds.userId);
340
394
  }
341
- const joinedAt = persistent?.joinedAt ?? (membership === "join" ? rejoinedAt : ownJoin.getTs());
395
+ const joinedAt = persistent?.joinedAt ?? ownJoin.getTs();
342
396
  await persistent?.rememberJoin(joinedAt);
343
397
  joinTimes.set(roomId, joinedAt);
344
398
  if (persistent?.restored) {
399
+ let pruned = false;
400
+ for (const [id, entry] of [...undecryptableIds]) {
401
+ if (stopped) throw new Error("session_stopped");
402
+ if (!retryUnexpired(entry)) {
403
+ undecryptableIds.delete(id);
404
+ pruned = true;
405
+ continue;
406
+ }
407
+ try {
408
+ const raw = await client.fetchRoomEvent(roomId, id);
409
+ const event = client.getEventMapper()({ ...raw, room_id: roomId });
410
+ if (event.getTs() < joinedAt) {
411
+ undecryptableIds.delete(id);
412
+ pruned = true;
413
+ continue;
414
+ }
415
+ liveEvents.add(event);
416
+ await client.decryptEventIfNeeded(event).catch(() => log("recovery_decryption_failed"));
417
+ rememberDecryption(event);
418
+ } catch (error) {
419
+ const status = typeof error === "object" && error !== null && "httpStatus" in error ? error.httpStatus : void 0;
420
+ if (matrixCode(error) === "M_NOT_FOUND" || matrixCode(error) === "M_FORBIDDEN" || status === 404 || status === 403) {
421
+ undecryptableIds.delete(id);
422
+ pruned = true;
423
+ }
424
+ log("recovery_event_retry_failed");
425
+ }
426
+ }
427
+ if (pruned) saveRetries();
345
428
  let token = null;
346
429
  const seenTokens = /* @__PURE__ */ new Set();
430
+ let pages = 0;
431
+ let undecryptable = 0;
347
432
  for (; ; ) {
348
433
  if (stopped) throw new Error("session_stopped");
349
434
  const page = await client.createMessagesRequest(roomId, token, 100, Direction.Backward);
350
- let reachedJoin = false;
435
+ pages++;
436
+ let reachedBoundary = false;
351
437
  for (const raw of page.chunk) {
352
438
  const event = client.getEventMapper()({ ...raw, room_id: roomId });
353
- if (event.getTs() < joinedAt) {
354
- reachedJoin = true;
355
- continue;
439
+ if (opts?.restoreStopAtEventId && event.getId() === opts.restoreStopAtEventId || event.getTs() < joinedAt) {
440
+ reachedBoundary = true;
441
+ break;
356
442
  }
357
443
  liveEvents.add(event);
358
444
  await client.decryptEventIfNeeded(event).catch(() => log("recovery_decryption_failed"));
445
+ rememberDecryption(event);
446
+ if (event.getType() === "m.room.encrypted" || event.isDecryptionFailure()) {
447
+ undecryptable++;
448
+ liveEvents.delete(event);
449
+ }
450
+ }
451
+ if (reachedBoundary || page.chunk.length === 0 || !page.end) break;
452
+ if (pages >= RESTORE_HISTORY_MAX_PAGES) {
453
+ const count = `restore_catchup_truncated pages=${pages}`;
454
+ if (opts?.log) log(count);
455
+ else process.stderr.write(`${count}
456
+ `);
457
+ break;
359
458
  }
360
- if (reachedJoin || page.chunk.length === 0 || !page.end) break;
361
459
  if (seenTokens.has(page.end)) throw new Error("recovery_pagination_stalled");
362
460
  seenTokens.add(page.end);
363
461
  token = page.end;
364
462
  }
463
+ if (undecryptable > 0) {
464
+ const count = `history_undecryptable=${undecryptable}`;
465
+ if (opts?.log) log(count);
466
+ else process.stderr.write(`${count}
467
+ `);
468
+ }
365
469
  }
366
470
  recovering = false;
367
471
  if (!joinedRoom) client.on(RoomEvent.MyMembership, membershipEnded);
@@ -370,7 +474,8 @@ async function createAgentMatrixSession(creds, opts) {
370
474
  if (event.getRoomId() === roomId) deliver(event);
371
475
  }
372
476
  },
373
- async history(roomId, limit, before) {
477
+ joinedAt: (roomId) => joinTimes.get(roomId),
478
+ async history(roomId, limit, before, options) {
374
479
  if (stopped) throw new Error("session_stopped");
375
480
  let token = null;
376
481
  if (before !== void 0) {
@@ -380,16 +485,26 @@ async function createAgentMatrixSession(creds, opts) {
380
485
  const res = await guarded(roomId, () => client.createMessagesRequest(roomId, token, limit, Direction.Backward));
381
486
  const messages = [];
382
487
  let undecryptable = 0;
488
+ let oldest;
489
+ let oldestTs;
490
+ let reachedBoundary = false;
383
491
  for (const raw of res.chunk) {
384
492
  const event = client.getEventMapper()({ ...raw, room_id: roomId });
493
+ oldest = event.getId();
494
+ oldestTs = Math.min(oldestTs ?? event.getTs(), event.getTs());
495
+ if (options?.stopAtEventId && oldest === options.stopAtEventId) {
496
+ reachedBoundary = true;
497
+ break;
498
+ }
385
499
  try {
386
500
  await client.decryptEventIfNeeded(event);
387
501
  } catch {
388
502
  }
503
+ rememberDecryption(event);
389
504
  if (event.getType() === "m.room.encrypted" || event.isDecryptionFailure()) {
390
505
  undecryptable++;
391
506
  const eventId = event.getId(), sender = event.getSender();
392
- if (eventId && sender && eventId !== before) {
507
+ if (options?.includeUnavailable !== false && eventId && sender && eventId !== before) {
393
508
  const body = "[Encrypted message unavailable: this device does not have its key. Keys may not have been shared for messages sent before joining or while this agent was offline.]";
394
509
  messages.push({
395
510
  eventId,
@@ -406,10 +521,16 @@ async function createAgentMatrixSession(creds, opts) {
406
521
  const m = message(event);
407
522
  if (m && m.eventId !== before) messages.push(m);
408
523
  }
409
- log(`history_undecryptable=${undecryptable}`);
524
+ const count = `history_undecryptable=${undecryptable}`;
525
+ log(count);
526
+ if (options?.includeUnavailable === false && undecryptable > 0 && !opts?.log) process.stderr.write(`${count}
527
+ `);
410
528
  messages.reverse();
411
- const oldest = messages[0];
412
- return typeof res.end === "string" && oldest ? { messages, nextBefore: oldest.eventId } : { messages };
529
+ return {
530
+ messages,
531
+ ...typeof res.end === "string" && oldest ? { nextBefore: oldest } : {},
532
+ ...options?.includeUnavailable === false || options?.stopAtEventId ? { ...oldestTs === void 0 ? {} : { oldestTs }, reachedBoundary } : {}
533
+ };
413
534
  },
414
535
  async send(roomId, text) {
415
536
  if (stopped) throw new Error("session_stopped");
@@ -428,6 +549,35 @@ async function createAgentMatrixSession(creds, opts) {
428
549
  const value = joinedRoom && client.getRoom(joinedRoom)?.currentState.getStateEvents("m.room.member", userId)?.getContent().displayname;
429
550
  return typeof value === "string" && value.length > 0 ? value : void 0;
430
551
  },
552
+ members(roomId) {
553
+ return (client.getRoom(roomId)?.getJoinedMembers() ?? []).filter((member) => member.userId !== creds.userId).map((member) => {
554
+ const user = client.getUser(member.userId);
555
+ const event = user?.events.presence;
556
+ if (!user || !event) return { name: member.name, presence: "unknown" };
557
+ if (user.presence !== "offline") return { name: member.name, presence: "online" };
558
+ const reason = event.getContent().status_msg;
559
+ return {
560
+ name: member.name,
561
+ presence: "away",
562
+ since: new Date(user.lastPresenceTs - (user.lastActiveAgo ?? 0)).toISOString(),
563
+ ...typeof reason === "string" && reason ? { reason: reason.slice(0, 80) } : {}
564
+ };
565
+ });
566
+ },
567
+ seenBy(roomId, eventId) {
568
+ const room = client.getRoom(roomId);
569
+ if (!room?.findEventById(eventId)) return [];
570
+ return room.getJoinedMembers().filter((member) => member.userId !== creds.userId && room.hasUserReadEvent(member.userId, eventId)).map((member) => member.name);
571
+ },
572
+ setAway(reason) {
573
+ if (reason === away) return;
574
+ away = reason;
575
+ if (presenceTimer && !stopped) publishPresence();
576
+ },
577
+ async markRead(roomId, eventId) {
578
+ if (stopped) throw new Error("session_stopped");
579
+ await guarded(roomId, () => client.setRoomReadMarkersHttpRequest(roomId, eventId, eventId));
580
+ },
431
581
  stop
432
582
  };
433
583
  }
@@ -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-EVT2DWNZ.mjs";
5
5
 
6
6
  // ../contracts/src/m1/harness.ts
7
7
  init_define_KHALA_BUNDLE();
@@ -18,8 +18,8 @@ var HARNESS_REGISTRY = [
18
18
  { id: "copilot", displayName: "Copilot CLI", modelName: "Copilot", logoKey: "copilot", steer: true, sync: true, idleWake: "opt-in", registered: true },
19
19
  { id: "vscode", displayName: "Copilot (VS Code)", modelName: "VSCode", logoKey: "copilot", steer: true, sync: true, idleWake: "opt-in", registered: true },
20
20
  { id: "gemini", displayName: "Gemini CLI", modelName: "Gemini", logoKey: "gemini", steer: true, sync: true, idleWake: "opt-in", registered: true },
21
- // U29 establishes Antigravity capabilities after its spike.
22
- { id: "antigravity", displayName: "Antigravity CLI", modelName: "Antigravity", logoKey: null, steer: false, sync: false, idleWake: "none", registered: true },
21
+ // Spike-backed capabilities; transport availability is reported per session.
22
+ { id: "antigravity", displayName: "Antigravity CLI", modelName: "Antigravity", logoKey: null, steer: true, sync: true, idleWake: "opt-in", registered: true },
23
23
  { id: "qwen", displayName: "Qwen Code", modelName: "Qwen", logoKey: "qwen", steer: true, sync: true, idleWake: "default", registered: true },
24
24
  { id: "muse", displayName: "Muse Code", modelName: "Muse", logoKey: null, steer: true, sync: true, idleWake: "default", registered: true },
25
25
  { id: "generic", displayName: "MCP agent", modelName: "Agent", logoKey: null, steer: false, sync: false, idleWake: "none", registered: true }
@@ -328,6 +328,14 @@ function cachedProcessReader(read = readProcess) {
328
328
  return result;
329
329
  };
330
330
  }
331
+ async function readProcessArguments(pid) {
332
+ if (!validPid(pid) || process.platform !== "linux") return null;
333
+ try {
334
+ return (await readFile(`/proc/${pid}/cmdline`, "utf8")).split("\0").slice(0, -1);
335
+ } catch {
336
+ return null;
337
+ }
338
+ }
331
339
 
332
340
  // src/state.ts
333
341
  var SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
@@ -512,7 +520,7 @@ async function readWatcherStatus(files, read = readProcess, now = () => /* @__PU
512
520
  return writer && writer.startTime !== owner?.startTime ? { ...status, state: "disconnected", detail: "process_exited" } : status;
513
521
  }
514
522
  async function writeStateFile(dir, name, value) {
515
- if (!["join.json", "session.json", "cursor.json", "status.json", "mode.json", "rejoin.json", "channel.json", "resume.json"].includes(name)) throw new StateError("storage_failed");
523
+ if (!["join.json", "session.json", "cursor.json", "status.json", "mode.json", "rejoin.json", "channel.json", "resume.json", "fold.json"].includes(name)) throw new StateError("storage_failed");
516
524
  await writeJsonAtomic(path.join(dir, name), name === "status.json" ? await ownStatus(value) : value);
517
525
  }
518
526
  function readStateFile(dir, name) {
@@ -565,6 +573,7 @@ export {
565
573
  ancestors,
566
574
  nearestNonShellAncestor,
567
575
  cachedProcessReader,
576
+ readProcessArguments,
568
577
  SESSION_ID_PATTERN,
569
578
  StateError,
570
579
  stateRoot,
@@ -1,43 +1,44 @@
1
1
  import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
2
- import {
3
- isWakeEntry
4
- } from "./chunk-47XNLAJG.mjs";
5
- import "./chunk-BV45NK7K.mjs";
6
2
  import {
7
3
  wakeDisableNotice
8
- } from "./chunk-XUCWSYVS.mjs";
4
+ } from "./chunk-W6HM6F2U.mjs";
9
5
  import {
10
6
  adapterFor,
11
7
  createCopilotCodec,
12
8
  recordHookSession
13
- } from "./chunk-ITFD3JZS.mjs";
9
+ } from "./chunk-5JSGA6OB.mjs";
14
10
  import {
15
11
  capturePane
16
- } from "./chunk-QKWEZXR3.mjs";
12
+ } from "./chunk-6AQVW5W7.mjs";
17
13
  import {
18
14
  readListeningMode
19
- } from "./chunk-NIX5F4LT.mjs";
20
- import "./chunk-NGSYOVQ4.mjs";
15
+ } from "./chunk-2AMWQ7RB.mjs";
16
+ import "./chunk-4BP7V3OP.mjs";
21
17
  import {
22
18
  advanceCursor,
19
+ clearUsageLimit,
20
+ isWakeEntry,
23
21
  listChannels,
22
+ markUsageLimit,
24
23
  readActivity,
25
24
  unread,
25
+ wakesAgent,
26
26
  writeActivity
27
- } from "./chunk-TANCKTDK.mjs";
27
+ } from "./chunk-OEV5DFLW.mjs";
28
+ import "./chunk-JKOJQTFT.mjs";
28
29
  import {
29
30
  StateError,
30
31
  TERMINAL_SESSION_DETAILS,
31
32
  readJson,
32
33
  readStatus,
33
34
  sessionFiles
34
- } from "./chunk-NPSQNCOY.mjs";
35
+ } from "./chunk-PN25BUQ4.mjs";
35
36
  import {
36
37
  settleAttempts
37
- } from "./chunk-K7V7X6WU.mjs";
38
+ } from "./chunk-ZSKLNZWZ.mjs";
38
39
  import {
39
40
  init_define_KHALA_BUNDLE
40
- } from "./chunk-7R3EIDGR.mjs";
41
+ } from "./chunk-EVT2DWNZ.mjs";
41
42
 
42
43
  // hooks/deliver.ts
43
44
  init_define_KHALA_BUNDLE();
@@ -53,26 +54,36 @@ import * as fs from "node:fs/promises";
53
54
  import { constants } from "node:fs";
54
55
  var MAX_TAIL_BYTES = 64 * 1024;
55
56
  var MARKERS = /* @__PURE__ */ new Set(["[Request interrupted by user]", "[Request interrupted by user for tool use]"]);
56
- async function claudeTranscriptInterrupted(transcriptPath, activityAt) {
57
- if (!transcriptPath) return false;
57
+ async function tailLines(transcriptPath) {
58
+ if (!transcriptPath) return [];
58
59
  let handle;
59
60
  try {
60
61
  handle = await fs.open(transcriptPath, constants.O_RDONLY | constants.O_NONBLOCK);
61
62
  const stat2 = await handle.stat();
62
- if (!stat2.isFile()) return false;
63
+ if (!stat2.isFile()) return [];
63
64
  const length = Math.min(stat2.size, MAX_TAIL_BYTES);
64
65
  const start = stat2.size - length;
65
66
  const buffer = Buffer.alloc(length);
66
67
  const { bytesRead } = await handle.read(buffer, 0, length, start);
67
- if (bytesRead !== length) return false;
68
+ if (bytesRead !== length) return [];
68
69
  let tail = buffer.toString("utf8");
69
70
  if (start > 0) {
70
71
  const newline = tail.indexOf("\n");
71
- if (newline < 0) return false;
72
+ if (newline < 0) return [];
72
73
  tail = tail.slice(newline + 1);
73
74
  }
74
- const lines = tail.split("\n").filter((line) => line.trim() !== "");
75
- if (!lines.length) return false;
75
+ return tail.split("\n").filter((line) => line.trim() !== "");
76
+ } catch {
77
+ return [];
78
+ } finally {
79
+ await handle?.close().catch(() => {
80
+ });
81
+ }
82
+ }
83
+ async function claudeTranscriptInterrupted(transcriptPath, activityAt) {
84
+ const lines = await tailLines(transcriptPath);
85
+ if (!lines.length) return false;
86
+ try {
76
87
  const entry = JSON.parse(lines.at(-1));
77
88
  if (entry?.type !== "user" || entry.message?.role !== "user" || typeof entry.timestamp !== "string" || !(Date.parse(entry.timestamp) > Date.parse(activityAt))) return false;
78
89
  const content = entry.message.content;
@@ -80,11 +91,24 @@ async function claudeTranscriptInterrupted(transcriptPath, activityAt) {
80
91
  return Array.isArray(content) && content.length === 1 && content[0]?.type === "text" && typeof content[0].text === "string" && MARKERS.has(content[0].text);
81
92
  } catch {
82
93
  return false;
83
- } finally {
84
- await handle?.close().catch(() => {
85
- });
86
94
  }
87
95
  }
96
+ async function claudeTranscriptVersion(transcriptPath) {
97
+ for (const line of (await tailLines(transcriptPath)).reverse()) {
98
+ try {
99
+ const version = JSON.parse(line)?.version;
100
+ if (typeof version === "string" && /^\d+\.\d+\.\d+$/.test(version)) return version;
101
+ } catch {
102
+ }
103
+ }
104
+ return void 0;
105
+ }
106
+ function versionAtLeast(version, floor) {
107
+ if (version === void 0) return false;
108
+ const a = version.split(".").map(Number), b = floor.split(".").map(Number);
109
+ for (let i = 0; i < b.length; i++) if ((a[i] ?? 0) !== b[i]) return (a[i] ?? 0) > b[i];
110
+ return true;
111
+ }
88
112
 
89
113
  // src/harness/deliver-core.ts
90
114
  var MAX_FRAME_BYTES = 64 * 1024;
@@ -162,7 +186,8 @@ function selectFrames(groups, budget = MAX_FRAME_BYTES) {
162
186
  }
163
187
  return selected;
164
188
  }
165
- async function channelFrames(files, steerOnly, requireWake, io) {
189
+ async function channelFrames(files, steerOnly, requireWake, io, quiet, shown) {
190
+ const wakes = (group) => group.consumed.some((entry) => quiet ? wakesAgent(entry, group.mode, group.you) : isWakeEntry(entry));
166
191
  const groups = [];
167
192
  for (const channel of await listChannels(files)) {
168
193
  const mode = await readListeningMode(channel.files);
@@ -171,6 +196,7 @@ async function channelFrames(files, steerOnly, requireWake, io) {
171
196
  groups.push({
172
197
  channel: { ...channel, ...status?.channelName !== void 0 ? { channelName: status.channelName } : {} },
173
198
  ...pending,
199
+ mode,
174
200
  you: typeof status?.displayName === "string" ? status.displayName : void 0
175
201
  });
176
202
  }
@@ -179,16 +205,16 @@ async function channelFrames(files, steerOnly, requireWake, io) {
179
205
  const reserved = selected.reduce((sum, group) => sum + Buffer.byteLength(group.frame), 0) + Math.max(0, selected.length - 1);
180
206
  let spare = MAX_FRAME_BYTES - reserved;
181
207
  const order = selected.map((group, index) => ({ group, index }));
182
- if (requireWake) order.sort((a, b) => Number(b.group.consumed.some(isWakeEntry)) - Number(a.group.consumed.some(isWakeEntry)));
208
+ if (requireWake) order.sort((a, b) => Number(wakes(b.group)) - Number(wakes(a.group)));
183
209
  let hasWake = false;
184
210
  const failures = [];
185
211
  for (const { group, index } of order) {
186
- if (requireWake && !hasWake && !group.consumed.some(isWakeEntry)) continue;
212
+ if (requireWake && !hasWake && !wakes(group)) continue;
187
213
  try {
188
214
  let current = group;
189
215
  for (let attempt = 0; attempt < 2; attempt++) {
190
- if (!current.frame || requireWake && !hasWake && !current.consumed.some(isWakeEntry)) break;
191
- if (await advanceCursor(current.channel.files, current.cursor, current.consumed) === "conflict") {
216
+ if (!current.frame || requireWake && !hasWake && !wakes(current)) break;
217
+ if (await advanceCursor(current.channel.files, current.cursor, current.consumed, shown) === "conflict") {
192
218
  if (attempt === 0) {
193
219
  const pending = await unread(current.channel.files);
194
220
  const retry = selectFrame(
@@ -204,7 +230,7 @@ async function channelFrames(files, steerOnly, requireWake, io) {
204
230
  }
205
231
  spare += Buffer.byteLength(group.frame) - Buffer.byteLength(current.frame);
206
232
  emitted.set(index, current.frame);
207
- hasWake ||= current.consumed.some(isWakeEntry);
233
+ hasWake ||= wakes(current);
208
234
  break;
209
235
  }
210
236
  } catch (error) {
@@ -241,7 +267,7 @@ async function deliverCore(stdin, adapter, io) {
241
267
  if (!codec) return 0;
242
268
  const input = codec.parse(stdin);
243
269
  let output = codec.noop(input?.event);
244
- if (input && (input.event === "prompt" || input.event === "start") && input.sessionId && adapter.sessionSources.some((source) => source.kind === "hook-map")) {
270
+ if (input && (input.event === "prompt" || input.event === "start" || adapter.id === "opencode" && input.event === "stop") && input.sessionId && adapter.sessionSources.some((source) => source.kind === "hook-map")) {
245
271
  try {
246
272
  await recordHookSession(adapter.id, input.sessionId, io.env, { now: io.now, ...io.pid !== void 0 ? { pid: io.pid } : {}, ...io.readProcess ? { readProcess: io.readProcess } : {}, ...input.workspace !== void 0 ? { workspace: input.workspace } : {} });
247
273
  } catch (error) {
@@ -257,6 +283,18 @@ async function deliverCore(stdin, adapter, io) {
257
283
  }
258
284
  if (files) {
259
285
  try {
286
+ if (input.event === "limit") {
287
+ await markUsageLimit(files, io.now);
288
+ return 0;
289
+ }
290
+ if (input.event === "tool" || input.event === "stop") await clearUsageLimit(files);
291
+ if (adapter.verifyWake) {
292
+ try {
293
+ await adapter.verifyWake(stdin, files, io.now().getTime());
294
+ } catch {
295
+ diagnostic(io, "wake_verification_failed");
296
+ }
297
+ }
260
298
  if (adapter.emptyPrompt && (input.event === "start" || input.event === "prompt")) {
261
299
  try {
262
300
  await capturePane(files, io.env, {
@@ -288,18 +326,21 @@ async function deliverCore(stdin, adapter, io) {
288
326
  }
289
327
  if (input.event !== "start") {
290
328
  let steerOnly = input.event === "tool";
291
- if (input.event === "prompt") {
292
- const activity = await readActivity(files);
293
- steerOnly = adapter.id === "claude" && activity.state === "busy" && Date.parse(activity.updatedAt) !== 0 && io.now().getTime() - Date.parse(activity.updatedAt) < CLAUDE_STALE_BUSY_MS && !await claudeTranscriptInterrupted(input.transcriptPath, activity.updatedAt);
329
+ if (adapter.hookPromptText || input.event === "prompt") {
294
330
  try {
295
331
  await settleAttempts(files.dir, {
296
332
  now: io.now().getTime(),
297
- activity,
298
- promptText: input.promptText ?? ""
333
+ activity: await readActivity(files),
334
+ promptText: adapter.hookPromptText ? await adapter.hookPromptText(input, files) : input.promptText ?? "",
335
+ deferVerification: Boolean(adapter.hookPromptText)
299
336
  });
300
337
  } catch {
301
338
  diagnostic(io, "wake_verification_failed");
302
339
  }
340
+ }
341
+ if (input.event === "prompt") {
342
+ const activity = await readActivity(files);
343
+ steerOnly = adapter.id === "claude" && activity.state === "busy" && Date.parse(activity.updatedAt) !== 0 && io.now().getTime() - Date.parse(activity.updatedAt) < CLAUDE_STALE_BUSY_MS && !await claudeTranscriptInterrupted(input.transcriptPath, activity.updatedAt);
303
344
  await writeActivity(files, "busy", io.now);
304
345
  }
305
346
  if (adapter.id === "claude" && input.event === "tool" && (await readActivity(files)).state === "busy") await writeActivity(files, "busy", io.now);
@@ -316,16 +357,21 @@ async function deliverCore(stdin, adapter, io) {
316
357
  return 0;
317
358
  }
318
359
  }
360
+ const stopContext = input.event === "stop" && codec.stopContextFloor !== void 0 && versionAtLeast(await claudeTranscriptVersion(input.transcriptPath), codec.stopContextFloor);
319
361
  const frame = await channelFrames(
320
362
  files,
321
363
  steerOnly,
322
364
  input.event !== "prompt" || !codec.promptDeliversWithoutWake,
323
- io
365
+ io,
366
+ input.event === "stop",
367
+ // Stop shows the frame only as verified Claude context; every other Stop is a fixed nudge,
368
+ // which consumes the batch without showing it and so earns no read receipt.
369
+ input.event !== "stop" || stopContext
324
370
  );
325
371
  if (frame) {
326
372
  if (!wake) await writeActivity(files, "busy", io.now);
327
373
  const notice = await wakeDisableNotice(files.dir);
328
- output = (wake ? wake + "\n" : "") + codec.render(input.event, frame + (notice ? "\n" + notice : ""));
374
+ output = (wake ? wake + "\n" : "") + codec.render(input.event, frame + (notice ? "\n" + notice : ""), stopContext);
329
375
  } else if (input.event === "stop") await writeActivity(files, "idle", io.now);
330
376
  }
331
377
  }
@@ -357,7 +403,15 @@ async function deliver(stdin, argv, io = {
357
403
  let env = io.env;
358
404
  const museRoots = argv.length === 6 && argv[0] === "--harness" && argv[1] === "muse" && argv[2] === "--state-home" && argv[4] === "--data-home" && path.isAbsolute(argv[3]) && path.isAbsolute(argv[5]);
359
405
  if (museRoots) env = { ...env, XDG_STATE_HOME: argv[3], XDG_DATA_HOME: argv[5] };
360
- let adapter = (argv.length === 2 || museRoots || argv.length === 4 && argv[1] === "copilot" && argv[2] === "--event") && argv[0] === "--harness" ? adapterFor(argv[1]) : void 0;
406
+ const antigravityEvent = argv.length === 4 && argv[0] === "--harness" && argv[1] === "antigravity" && argv[2] === "--event" && ["PreInvocation", "Stop"].includes(argv[3]) ? argv[3] : void 0;
407
+ let adapter = (argv.length === 2 || museRoots || antigravityEvent || argv.length === 4 && argv[1] === "copilot" && argv[2] === "--event") && argv[0] === "--harness" ? adapterFor(argv[1]) : void 0;
408
+ if (antigravityEvent) {
409
+ try {
410
+ stdin = JSON.stringify({ ...JSON.parse(stdin), khalaHookEvent: antigravityEvent });
411
+ } catch {
412
+ stdin = "{}";
413
+ }
414
+ }
361
415
  if (adapter?.id === "copilot" && argv.length === 4) adapter = { ...adapter, codec: createCopilotCodec(argv[3]) };
362
416
  if (!adapter?.codec) {
363
417
  diagnostic(io, "invalid_harness");