@zetaloop/chappie 1.0.0 → 1.1.0

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.
@@ -3,25 +3,27 @@ import {
3
3
  ProviderOutput,
4
4
  Session,
5
5
  createMessage
6
- } from "./chunk-5CKVVDZ2.js";
6
+ } from "./chunk-G4LG7HH2.js";
7
7
  import {
8
8
  definitions,
9
9
  localTools
10
- } from "./chunk-DM523QRR.js";
10
+ } from "./chunk-3MLW5U2C.js";
11
11
  import "./chunk-EIPDEA3I.js";
12
12
  import {
13
13
  package_default
14
- } from "./chunk-DN7OHZK6.js";
14
+ } from "./chunk-DDBZIUBF.js";
15
15
  import {
16
16
  historyPage,
17
17
  historyResult,
18
18
  readConfig,
19
19
  src_default
20
- } from "./chunk-GWYP7B66.js";
20
+ } from "./chunk-ASDHNBR2.js";
21
21
  import {
22
+ __callDispose,
22
23
  __commonJS,
23
24
  __require,
24
- __toESM
25
+ __toESM,
26
+ __using
25
27
  } from "./chunk-UOFCCHBS.js";
26
28
 
27
29
  // node_modules/.pnpm/isexe@2.0.0/node_modules/isexe/windows.js
@@ -4197,10 +4199,12 @@ import {
4197
4199
  createServer
4198
4200
  } from "node:http";
4199
4201
  import { homedir, hostname } from "node:os";
4200
- import { join } from "node:path";
4202
+ import { join as join2 } from "node:path";
4201
4203
 
4202
4204
  // src/appserver.ts
4203
4205
  var import_cross_spawn = __toESM(require_cross_spawn(), 1);
4206
+ import { addAbortListener } from "node:events";
4207
+ import { createInterface } from "node:readline";
4204
4208
  import { Duplex, Readable, Writable } from "node:stream";
4205
4209
 
4206
4210
  // node_modules/.pnpm/ws@8.21.3/node_modules/ws/wrapper.mjs
@@ -4215,7 +4219,7 @@ var import_websocket_server = __toESM(require_websocket_server(), 1);
4215
4219
  var wrapper_default = import_websocket.default;
4216
4220
 
4217
4221
  // src/appserver.ts
4218
- var AppServer = class {
4222
+ var AppServer = class _AppServer {
4219
4223
  #pending = /* @__PURE__ */ new Map();
4220
4224
  #receive;
4221
4225
  #stopped = Promise.withResolvers();
@@ -4225,6 +4229,35 @@ var AppServer = class {
4225
4229
  constructor(receive) {
4226
4230
  this.#receive = receive;
4227
4231
  }
4232
+ static async query(method, params = {}, signal) {
4233
+ signal?.throwIfAborted();
4234
+ const child = (0, import_cross_spawn.spawn)("codex", ["app-server"], {
4235
+ stdio: ["pipe", "pipe", "inherit"],
4236
+ signal
4237
+ });
4238
+ const rpc = new _AppServer(() => {
4239
+ });
4240
+ const reader = createInterface({ input: child.stdout });
4241
+ reader.on("line", (line) => rpc.#message(line));
4242
+ reader.on("error", (error) => rpc.#ended(error));
4243
+ child.on("error", (error) => rpc.#ended(error));
4244
+ child.stdin.on("error", (error) => rpc.#ended(error));
4245
+ child.once(
4246
+ "exit",
4247
+ () => rpc.#ended(new Error("Codex query process exited"))
4248
+ );
4249
+ rpc.#send = (message) => {
4250
+ child.stdin.write(`${JSON.stringify(message)}
4251
+ `);
4252
+ };
4253
+ try {
4254
+ await rpc.#initialize();
4255
+ return await rpc.request(method, params);
4256
+ } finally {
4257
+ reader.close();
4258
+ if (!child.killed) child.kill();
4259
+ }
4260
+ }
4228
4261
  get closed() {
4229
4262
  return this.#stopped.promise;
4230
4263
  }
@@ -4271,27 +4304,42 @@ var AppServer = class {
4271
4304
  socket.terminate();
4272
4305
  stream?.destroy();
4273
4306
  };
4274
- await ready.promise;
4275
- this.#send = (message) => socket.send(JSON.stringify(message));
4307
+ try {
4308
+ await ready.promise;
4309
+ this.#send = (message) => socket.send(JSON.stringify(message));
4310
+ await this.#initialize();
4311
+ } catch (error) {
4312
+ this.close();
4313
+ throw error;
4314
+ }
4315
+ }
4316
+ async #initialize() {
4276
4317
  await this.request("initialize", {
4277
4318
  clientInfo: { name: "chappie", version: package_default.version },
4278
4319
  capabilities: { experimentalApi: true }
4279
4320
  });
4280
4321
  this.#send?.({ method: "initialized" });
4281
4322
  }
4282
- request(method, params = {}) {
4283
- if (!this.#send)
4284
- return Promise.reject(new Error("Codex app-server is not connected"));
4285
- const id = this.#nextId++;
4286
- const completion = Promise.withResolvers();
4287
- this.#pending.set(id, completion);
4323
+ async request(method, params = {}, signal) {
4324
+ var _stack = [];
4288
4325
  try {
4289
- this.#send({ id, method, params });
4290
- } catch (error) {
4291
- this.#pending.delete(id);
4292
- completion.reject(error);
4326
+ signal?.throwIfAborted();
4327
+ if (!this.#send) throw new Error("Codex app-server is not connected");
4328
+ const id = this.#nextId++;
4329
+ const completion = Promise.withResolvers();
4330
+ this.#pending.set(id, completion);
4331
+ const _listener = __using(_stack, signal ? addAbortListener(signal, () => completion.reject(signal.reason)) : void 0);
4332
+ try {
4333
+ this.#send({ id, method, params });
4334
+ return await completion.promise;
4335
+ } finally {
4336
+ this.#pending.delete(id);
4337
+ }
4338
+ } catch (_) {
4339
+ var _error = _, _hasError = true;
4340
+ } finally {
4341
+ __callDispose(_stack, _error, _hasError);
4293
4342
  }
4294
- return completion.promise;
4295
4343
  }
4296
4344
  close() {
4297
4345
  this.#close?.();
@@ -4313,7 +4361,6 @@ var AppServer = class {
4313
4361
  if (typeof message.id !== "number") return;
4314
4362
  const pending = this.#pending.get(message.id);
4315
4363
  if (!pending) return;
4316
- this.#pending.delete(message.id);
4317
4364
  if (message.error) pending.reject(new Error(message.error.message));
4318
4365
  else pending.resolve(message.result);
4319
4366
  }
@@ -4325,8 +4372,194 @@ var AppServer = class {
4325
4372
  }
4326
4373
  };
4327
4374
 
4328
- // src/responses.ts
4375
+ // src/desktop.ts
4329
4376
  import { randomUUID } from "node:crypto";
4377
+ import { once } from "node:events";
4378
+ import { connect } from "node:net";
4379
+ import { join } from "node:path";
4380
+ var Desktop = class {
4381
+ #receive;
4382
+ #stopped = Promise.withResolvers();
4383
+ #pending = /* @__PURE__ */ new Map();
4384
+ #snapshots = /* @__PURE__ */ new Map();
4385
+ #socket;
4386
+ #length = 0;
4387
+ #clientId = "initializing-client";
4388
+ constructor(receive) {
4389
+ this.#receive = receive;
4390
+ }
4391
+ get closed() {
4392
+ return this.#stopped.promise;
4393
+ }
4394
+ async connect(home) {
4395
+ const socket = connect(
4396
+ process.platform === "win32" ? "\\\\.\\pipe\\codex-ipc" : join(home, "ipc", "ipc.sock")
4397
+ );
4398
+ this.#socket = socket;
4399
+ socket.on("error", (error) => this.#ended(error));
4400
+ socket.once(
4401
+ "close",
4402
+ () => this.#ended(new Error("Codex desktop connection closed"))
4403
+ );
4404
+ socket.on("readable", () => {
4405
+ try {
4406
+ for (; ; ) {
4407
+ if (this.#length === 0) {
4408
+ const header = socket.read(4);
4409
+ if (!header) break;
4410
+ this.#length = header.readUInt32LE();
4411
+ if (this.#length === 0 || this.#length > 268435456)
4412
+ throw new Error(
4413
+ `Invalid Codex desktop frame length: ${this.#length}`
4414
+ );
4415
+ }
4416
+ const body = socket.read(this.#length);
4417
+ if (!body) break;
4418
+ this.#length = 0;
4419
+ this.#message(JSON.parse(body.toString("utf8")));
4420
+ }
4421
+ } catch (error) {
4422
+ socket.destroy(
4423
+ error instanceof Error ? error : new Error(String(error))
4424
+ );
4425
+ }
4426
+ });
4427
+ await once(socket, "connect");
4428
+ const response = await this.request(
4429
+ "initialize",
4430
+ { clientType: "chappie" },
4431
+ 0
4432
+ );
4433
+ this.#clientId = response.result.clientId;
4434
+ }
4435
+ async request(method, params, version, owner, timeoutMs) {
4436
+ const requestId = randomUUID();
4437
+ const pending = Promise.withResolvers();
4438
+ this.#pending.set(requestId, pending);
4439
+ try {
4440
+ this.#send({
4441
+ type: "request",
4442
+ requestId,
4443
+ sourceClientId: this.#clientId,
4444
+ method,
4445
+ params,
4446
+ version,
4447
+ timeoutMs,
4448
+ ...owner ? { targetClientId: owner } : {}
4449
+ });
4450
+ const message = await pending.promise;
4451
+ return {
4452
+ result: message.result,
4453
+ handledByClientId: message.handledByClientId
4454
+ };
4455
+ } finally {
4456
+ this.#pending.delete(requestId);
4457
+ }
4458
+ }
4459
+ async owner(threadId, clientId) {
4460
+ try {
4461
+ const result = await this.request(
4462
+ "thread-owner-discovery",
4463
+ { hostId: "local", conversationId: threadId },
4464
+ 1,
4465
+ clientId
4466
+ );
4467
+ return result.handledByClientId;
4468
+ } catch (error) {
4469
+ if (error.code === "no-client-found")
4470
+ return void 0;
4471
+ throw error;
4472
+ }
4473
+ }
4474
+ follow(threadId, clientId) {
4475
+ this.#send({
4476
+ type: "broadcast",
4477
+ method: "thread-stream-following-changed",
4478
+ sourceClientId: this.#clientId,
4479
+ ...clientId ? { targetClientIds: [clientId] } : {},
4480
+ version: 1,
4481
+ params: { hostId: "local", conversationId: threadId, following: true }
4482
+ });
4483
+ }
4484
+ async snapshot(threadId, owner) {
4485
+ const pending = { owner, ...Promise.withResolvers() };
4486
+ const requests = this.#snapshots.get(threadId) ?? /* @__PURE__ */ new Set();
4487
+ this.#snapshots.set(threadId, requests);
4488
+ requests.add(pending);
4489
+ const timer = setTimeout(
4490
+ () => pending.reject(new Error("Codex desktop snapshot timed out")),
4491
+ 1e4
4492
+ );
4493
+ try {
4494
+ this.follow(threadId);
4495
+ return await pending.promise;
4496
+ } finally {
4497
+ clearTimeout(timer);
4498
+ requests.delete(pending);
4499
+ if (requests.size === 0) this.#snapshots.delete(threadId);
4500
+ }
4501
+ }
4502
+ close() {
4503
+ this.#socket?.destroy();
4504
+ this.#ended(new Error("Codex desktop connection closed"));
4505
+ }
4506
+ #send(message) {
4507
+ if (!this.#socket?.writable)
4508
+ throw new Error("Codex desktop is not connected");
4509
+ const body = Buffer.from(JSON.stringify(message));
4510
+ const size = Buffer.alloc(4);
4511
+ size.writeUInt32LE(body.length);
4512
+ this.#socket.write(Buffer.concat([size, body]));
4513
+ }
4514
+ #message(message) {
4515
+ if (message.type === "client-discovery-request") {
4516
+ this.#send({
4517
+ type: "client-discovery-response",
4518
+ requestId: message.requestId,
4519
+ response: { canHandle: false }
4520
+ });
4521
+ return;
4522
+ }
4523
+ if (message.type === "response" && message.requestId) {
4524
+ const pending = this.#pending.get(message.requestId);
4525
+ if (message.resultType === "error")
4526
+ pending?.reject(
4527
+ Object.assign(new Error(message.error), { code: message.error })
4528
+ );
4529
+ else pending?.resolve(message);
4530
+ return;
4531
+ }
4532
+ const params = message.params;
4533
+ if (message.method === "thread-stream-state-changed" && params) {
4534
+ const change = params.change;
4535
+ if (change.type === "snapshot" && typeof params.conversationId === "string") {
4536
+ for (const pending of this.#snapshots.get(params.conversationId) ?? [])
4537
+ if (pending.owner === message.sourceClientId)
4538
+ pending.resolve(change.conversationState);
4539
+ }
4540
+ }
4541
+ if (message.method === "client-status-changed" && params?.status === "disconnected") {
4542
+ for (const requests of this.#snapshots.values())
4543
+ for (const pending of requests)
4544
+ if (pending.owner === params.clientId)
4545
+ pending.reject(
4546
+ new Error("Codex desktop session owner disconnected")
4547
+ );
4548
+ }
4549
+ this.#receive(message);
4550
+ }
4551
+ #ended(error) {
4552
+ for (const pending of this.#pending.values()) pending.reject(error);
4553
+ this.#pending.clear();
4554
+ for (const requests of this.#snapshots.values())
4555
+ for (const pending of requests) pending.reject(error);
4556
+ this.#snapshots.clear();
4557
+ this.#stopped.resolve();
4558
+ }
4559
+ };
4560
+
4561
+ // src/responses.ts
4562
+ import { randomUUID as randomUUID2 } from "node:crypto";
4330
4563
  function responseTools(tools, namespace) {
4331
4564
  return tools.flatMap((tool) => {
4332
4565
  if (tool.type === "namespace" && tool.name)
@@ -4398,7 +4631,7 @@ var ResponsesOutput = class extends ProviderOutput {
4398
4631
  "pending"
4399
4632
  );
4400
4633
  const items = [];
4401
- const id = `resp_${randomUUID()}`;
4634
+ const id = `resp_${randomUUID2()}`;
4402
4635
  const created = Math.floor(Date.now() / 1e3);
4403
4636
  let sequence = 0;
4404
4637
  let heartbeat;
@@ -4438,7 +4671,7 @@ data: ${JSON.stringify({ type, sequence_number: sequence++, ...fields })}
4438
4671
  break;
4439
4672
  case "text_start": {
4440
4673
  const item = {
4441
- id: `msg_${randomUUID()}`,
4674
+ id: `msg_${randomUUID2()}`,
4442
4675
  type: "message",
4443
4676
  role: "assistant",
4444
4677
  status: "in_progress",
@@ -4500,7 +4733,7 @@ data: ${JSON.stringify({ type, sequence_number: sequence++, ...fields })}
4500
4733
  if (!tool) throw new Error(`Unknown Codex tool: ${call.name}`);
4501
4734
  const { custom, ...wire } = tool.wire;
4502
4735
  const item = {
4503
- id: `${custom ? "ctc" : "fc"}_${randomUUID()}`,
4736
+ id: `${custom ? "ctc" : "fc"}_${randomUUID2()}`,
4504
4737
  type: custom ? "custom_tool_call" : "function_call",
4505
4738
  call_id: call.id,
4506
4739
  ...wire,
@@ -4588,23 +4821,70 @@ data: ${JSON.stringify({ type, sequence_number: sequence++, ...fields })}
4588
4821
 
4589
4822
  // src/codex.ts
4590
4823
  var codexHome = await realpath(
4591
- process.env.CODEX_HOME ?? join(homedir(), ".codex")
4824
+ process.env.CODEX_HOME ?? join2(homedir(), ".codex")
4592
4825
  );
4826
+ var RequestError = class extends Error {
4827
+ };
4828
+ function parseObject(text) {
4829
+ let value;
4830
+ try {
4831
+ value = JSON.parse(text);
4832
+ } catch (error) {
4833
+ throw new RequestError(
4834
+ error instanceof Error ? error.message : String(error),
4835
+ { cause: error }
4836
+ );
4837
+ }
4838
+ if (!value || typeof value !== "object" || Array.isArray(value))
4839
+ throw new RequestError("A JSON object is required");
4840
+ return value;
4841
+ }
4842
+ async function* loaded(connection) {
4843
+ let cursor = null;
4844
+ do {
4845
+ const page = await connection.request(
4846
+ "thread/loaded/list",
4847
+ cursor ? { cursor } : {}
4848
+ );
4849
+ yield* page.data;
4850
+ cursor = page.nextCursor;
4851
+ } while (cursor);
4852
+ }
4853
+ function desktopThread(snapshot) {
4854
+ return {
4855
+ id: snapshot.id,
4856
+ cwd: snapshot.latestThreadSettings?.cwd ?? snapshot.cwd,
4857
+ name: snapshot.title,
4858
+ model: snapshot.latestThreadSettings?.model ?? snapshot.latestModel,
4859
+ modelProvider: snapshot.latestThreadSettings?.modelProvider ?? snapshot.modelProvider,
4860
+ status: snapshot.threadRuntimeStatus ?? { type: "idle" }
4861
+ };
4862
+ }
4863
+ function desktopTurns(snapshot) {
4864
+ const history = snapshot.turnHistory?.history;
4865
+ return history ? history.islands.flatMap(
4866
+ (island) => island.entries.map(({ value }) => ({
4867
+ key: value,
4868
+ turn: history.entitiesByKey[value]
4869
+ }))
4870
+ ) : snapshot.turns.map((turn, key) => ({ key, turn }));
4871
+ }
4593
4872
  var CodexSession = class {
4594
4873
  session;
4595
4874
  tools;
4596
- #server;
4875
+ connection;
4597
4876
  #inputs = /* @__PURE__ */ new Map();
4598
4877
  #seenInputs = /* @__PURE__ */ new Set();
4599
4878
  #results = /* @__PURE__ */ new Map();
4600
4879
  #attached = false;
4880
+ #desktopTurn;
4601
4881
  thread;
4602
4882
  turn;
4603
4883
  output;
4604
4884
  catalog;
4605
- constructor(thread, server, config) {
4885
+ constructor(thread, connection, config) {
4606
4886
  this.thread = thread;
4607
- this.#server = server;
4887
+ this.connection = connection;
4608
4888
  this.session = new Session(this, config);
4609
4889
  this.tools = localTools(this.session);
4610
4890
  }
@@ -4619,15 +4899,16 @@ var CodexSession = class {
4619
4899
  };
4620
4900
  }
4621
4901
  active() {
4622
- return this.thread.modelProvider === "chappie";
4902
+ return this.thread.modelProvider === "chappie" && this.thread.status.type !== "notLoaded";
4623
4903
  }
4624
4904
  isIdle() {
4625
- return this.thread.status.type === "idle";
4905
+ return this.thread.status.type === "idle" || this.thread.status.type === "systemError";
4626
4906
  }
4627
4907
  async inspect(signal) {
4628
4908
  await this.session.ready(signal);
4629
- const result = await this.#server.request("skills/list", { cwds: [this.thread.cwd] });
4630
- const path = join(codexHome, "AGENTS.md");
4909
+ const request = this.connection instanceof AppServer ? this.connection.request.bind(this.connection) : AppServer.query;
4910
+ const result = await request("skills/list", { cwds: [this.thread.cwd] }, signal);
4911
+ const path = join2(codexHome, "AGENTS.md");
4631
4912
  let globalAgents;
4632
4913
  try {
4633
4914
  await access(path);
@@ -4642,10 +4923,49 @@ var CodexSession = class {
4642
4923
  };
4643
4924
  }
4644
4925
  async history(range) {
4926
+ if (!(this.connection instanceof AppServer)) {
4927
+ const { desktop } = this.connection;
4928
+ const { handledByClientId } = await desktop.request(
4929
+ "thread-follower-load-complete-history",
4930
+ { conversationId: this.thread.id },
4931
+ 1,
4932
+ void 0,
4933
+ 3e5
4934
+ );
4935
+ const snapshot = await desktop.snapshot(
4936
+ this.thread.id,
4937
+ handledByClientId
4938
+ );
4939
+ const page2 = historyPage(
4940
+ desktopTurns(snapshot).flatMap(
4941
+ ({ turn }) => turn.items.map((item) => ({
4942
+ id: item.id,
4943
+ item,
4944
+ turnId: turn.turnId,
4945
+ timestamp: turn.turnStartedAtMs
4946
+ }))
4947
+ ),
4948
+ range
4949
+ );
4950
+ return historyResult(
4951
+ {
4952
+ entries: await Promise.all(
4953
+ page2.entries.map(async ({ item, turnId, timestamp }) => ({
4954
+ ...await itemMessage(item),
4955
+ id: item.id,
4956
+ turnId,
4957
+ ...timestamp === void 0 ? {} : { timestamp: new Date(timestamp).toISOString() }
4958
+ }))
4959
+ ),
4960
+ hasMore: page2.hasMore
4961
+ },
4962
+ this.thread.id
4963
+ );
4964
+ }
4645
4965
  const items = [];
4646
4966
  let cursor = null;
4647
4967
  do {
4648
- const page2 = await this.#server.request("thread/items/list", {
4968
+ const page2 = await this.connection.request("thread/items/list", {
4649
4969
  threadId: this.thread.id,
4650
4970
  sortDirection: "desc",
4651
4971
  limit: range.limit + 1,
@@ -4697,33 +5017,58 @@ var CodexSession = class {
4697
5017
  if (!this.isIdle()) return;
4698
5018
  this.thread.status = { type: "active" };
4699
5019
  try {
4700
- const result = await this.#server.request("turn/start", {
4701
- threadId: this.thread.id,
4702
- input: []
4703
- });
4704
- this.turn = result.turn.id;
5020
+ const input = { threadId: this.thread.id, input: [] };
5021
+ if (this.connection instanceof AppServer) {
5022
+ const result = await this.connection.request(
5023
+ "turn/start",
5024
+ input
5025
+ );
5026
+ this.turn = result.turn.id;
5027
+ } else {
5028
+ await this.connection.desktop.request(
5029
+ "thread-follower-start-turn",
5030
+ { conversationId: this.thread.id, turnStart: { request: input } },
5031
+ 2
5032
+ );
5033
+ }
4705
5034
  } catch (error) {
4706
5035
  this.thread.status = { type: "idle" };
4707
5036
  throw error;
4708
5037
  }
4709
5038
  }
4710
5039
  async abort() {
4711
- if (this.turn)
4712
- await this.#server.request("turn/interrupt", {
5040
+ if (!this.turn) return;
5041
+ if (this.connection instanceof AppServer)
5042
+ await this.connection.request("turn/interrupt", {
4713
5043
  threadId: this.thread.id,
4714
5044
  turnId: this.turn
4715
5045
  });
5046
+ else
5047
+ await this.connection.desktop.request(
5048
+ "thread-follower-interrupt-turn",
5049
+ {
5050
+ conversationId: this.thread.id,
5051
+ mode: "user-stop",
5052
+ expectedTurnId: this.turn
5053
+ },
5054
+ 4
5055
+ );
4716
5056
  }
4717
5057
  async item(item) {
5058
+ await this.#capture(item);
5059
+ this.session.historyChanged();
5060
+ }
5061
+ async #capture(item) {
4718
5062
  if (!this.active()) return;
4719
5063
  if (this.output?.message.content.some(
4720
5064
  (call) => call.type === "toolCall" && call.id === item.id
4721
5065
  ))
4722
5066
  this.#results.set(item.id, item);
4723
- if (item.type === "userMessage" && item.content?.length && !this.#seenInputs.has(item.id)) {
4724
- this.#seenInputs.add(item.id);
4725
- this.#inputs.set(item.id, {
4726
- id: item.id,
5067
+ const id = item.type === "userMessage" ? item.id : item.type === "steeringUserMessage" ? item.serverUserMessageId : void 0;
5068
+ if (id && (item.content ?? item.input)?.length && !this.#seenInputs.has(id)) {
5069
+ this.#seenInputs.add(id);
5070
+ this.#inputs.set(id, {
5071
+ id,
4727
5072
  sessionId: this.thread.id,
4728
5073
  message: {
4729
5074
  role: "user",
@@ -4732,19 +5077,80 @@ var CodexSession = class {
4732
5077
  }
4733
5078
  });
4734
5079
  }
5080
+ }
5081
+ async snapshot(snapshot) {
5082
+ Object.assign(this.thread, desktopThread(snapshot));
5083
+ const turns = desktopTurns(snapshot);
5084
+ const current = turns.find(({ turn }) => turn.turnId === this.turn) ?? turns.findLast(({ turn }) => turn.status === "inProgress");
5085
+ if (current) {
5086
+ this.#desktopTurn = current.key;
5087
+ if (current.turn.turnId) this.turn = current.turn.turnId;
5088
+ for (const item of current.turn.items) await this.#capture(item);
5089
+ if (current.turn.turnId && current.turn.status !== "inProgress")
5090
+ await this.settled({
5091
+ id: current.turn.turnId,
5092
+ status: current.turn.status,
5093
+ items: current.turn.items,
5094
+ error: current.turn.error ?? null
5095
+ });
5096
+ }
5097
+ this.session.update();
5098
+ }
5099
+ async patches(patches) {
5100
+ if (this.connection instanceof AppServer) return;
5101
+ let refresh = false;
5102
+ for (const patch of patches) {
5103
+ if ([
5104
+ "title",
5105
+ "cwd",
5106
+ "latestModel",
5107
+ "modelProvider",
5108
+ "latestThreadSettings",
5109
+ "threadRuntimeStatus"
5110
+ ].includes(String(patch.path[0])))
5111
+ refresh = true;
5112
+ const path = patch.path[0] === "turnHistory" && patch.path[1] === "history" && patch.path[2] === "entitiesByKey" ? patch.path.slice(3) : patch.path[0] === "turns" ? patch.path.slice(1) : [];
5113
+ if (path[0] !== this.#desktopTurn) continue;
5114
+ if (path.length === 1 || path[1] === "status" || path[1] === "error" || path[1] === "turnId")
5115
+ refresh = true;
5116
+ if (path[1] === "items" && patch.op !== "remove") {
5117
+ if (path.length === 2 && Array.isArray(patch.value))
5118
+ for (const item of patch.value) await this.#capture(item);
5119
+ else if (path.length === 3 && patch.value && typeof patch.value === "object")
5120
+ await this.#capture(patch.value);
5121
+ }
5122
+ }
5123
+ if (refresh)
5124
+ await this.snapshot(
5125
+ await this.connection.desktop.snapshot(
5126
+ this.thread.id,
5127
+ this.connection.owner
5128
+ )
5129
+ );
4735
5130
  this.session.historyChanged();
4736
5131
  }
4737
- async respond(request, response, signal) {
5132
+ async respond(request, context, response, signal) {
5133
+ if (context?.request_kind === "turn" && context.turn_id)
5134
+ this.turn = context.turn_id;
4738
5135
  if (!this.#attached) {
4739
- const result = await this.#server.request("thread/resume", {
4740
- threadId: this.thread.id,
4741
- excludeTurns: true,
4742
- initialTurnsPage: { limit: 1, itemsView: "full" }
4743
- });
4744
- this.thread = result.thread;
4745
- for (const turn of result.initialTurnsPage.data) {
4746
- if (turn.status === "inProgress") this.turn = turn.id;
4747
- for (const item of turn.items) await this.item(item);
5136
+ if (this.connection instanceof AppServer) {
5137
+ const result = await this.connection.request("thread/resume", {
5138
+ threadId: this.thread.id,
5139
+ excludeTurns: true,
5140
+ initialTurnsPage: { limit: 1, itemsView: "full" }
5141
+ });
5142
+ this.thread = result.thread;
5143
+ for (const turn of result.initialTurnsPage.data) {
5144
+ if (turn.status === "inProgress") this.turn = turn.id;
5145
+ for (const item of turn.items) await this.item(item);
5146
+ }
5147
+ } else {
5148
+ await this.snapshot(
5149
+ await this.connection.desktop.snapshot(
5150
+ this.thread.id,
5151
+ this.connection.owner
5152
+ )
5153
+ );
4748
5154
  }
4749
5155
  this.#attached = true;
4750
5156
  }
@@ -4766,15 +5172,14 @@ var CodexSession = class {
4766
5172
  toolName: call.name,
4767
5173
  content: responseContent(item.output),
4768
5174
  isError: itemFailed(native),
4769
- ...native?.result?.structuredContent?.details ? { details: native.result.structuredContent.details } : {}
5175
+ ...native?.result?._meta?.details ? { details: native.result._meta.details } : {}
4770
5176
  });
4771
5177
  }
4772
5178
  this.session.complete(previous.message, results);
4773
5179
  this.output = void 0;
4774
5180
  this.#results.clear();
4775
5181
  }
4776
- const metadata = request.client_metadata?.["x-codex-turn-metadata"];
4777
- const kind = metadata ? JSON.parse(metadata).request_kind : void 0;
5182
+ const kind = context?.request_kind;
4778
5183
  if (kind === "compaction") {
4779
5184
  const output2 = new ResponsesOutput(request, [], response, signal);
4780
5185
  try {
@@ -4784,11 +5189,11 @@ var CodexSession = class {
4784
5189
  }
4785
5190
  return;
4786
5191
  }
4787
- this.catalog = responseTools(request.tools ?? []).flatMap((tool) => {
5192
+ this.catalog = responseTools(request.tools ?? []).map((tool) => {
4788
5193
  const local = ["chappie", "mcp__chappie"].includes(
4789
5194
  tool.wire.namespace ?? ""
4790
5195
  ) ? definitions.find((definition) => definition.name === tool.wire.name) : void 0;
4791
- return local ? local.name === "transfer" ? [{ ...tool, name: local.name }] : [] : [tool];
5196
+ return local ? { ...tool, name: local.name } : tool;
4792
5197
  });
4793
5198
  const output = new ResponsesOutput(request, this.catalog, response, signal);
4794
5199
  this.output = output;
@@ -4821,7 +5226,7 @@ var CodexSession = class {
4821
5226
  }
4822
5227
  ],
4823
5228
  isError: !item || item.status === "inProgress" || itemFailed(item),
4824
- ...item?.result?.structuredContent?.details ? { details: item.result.structuredContent.details } : {}
5229
+ ...item?.result?._meta?.details ? { details: item.result._meta.details } : {}
4825
5230
  };
4826
5231
  })
4827
5232
  );
@@ -4834,6 +5239,7 @@ var CodexSession = class {
4834
5239
  }
4835
5240
  this.#results.clear();
4836
5241
  this.#seenInputs.clear();
5242
+ this.#desktopTurn = void 0;
4837
5243
  this.catalog = void 0;
4838
5244
  await this.session.settled(
4839
5245
  turn.error ? new Error(turn.error.message) : void 0
@@ -4856,21 +5262,27 @@ async function itemMessage(item) {
4856
5262
  const message = { ...item };
4857
5263
  let content;
4858
5264
  switch (item.type) {
5265
+ case "steeringUserMessage":
4859
5266
  case "userMessage":
4860
5267
  delete message.content;
5268
+ delete message.input;
5269
+ delete message.restoreMessage;
5270
+ delete message.compareKey;
4861
5271
  message.role = "user";
4862
5272
  content = (await Promise.all(
4863
- (item.content ?? []).map(async (block) => {
4864
- if (block.type === "text" && typeof block.text === "string")
4865
- return [{ type: "text", text: block.text }];
4866
- if (block.type === "localImage" && typeof block.path === "string")
4867
- return [await image(block.path)];
4868
- if (block.type === "image" && typeof block.url === "string")
4869
- return responseContent([
4870
- { type: "input_image", image_url: block.url }
4871
- ]);
4872
- return [{ type: "text", text: JSON.stringify(block) }];
4873
- })
5273
+ (item.content ?? item.input ?? []).map(
5274
+ async (block) => {
5275
+ if (block.type === "text" && typeof block.text === "string")
5276
+ return [{ type: "text", text: block.text }];
5277
+ if (block.type === "localImage" && typeof block.path === "string")
5278
+ return [await image(block.path)];
5279
+ if (block.type === "image" && typeof block.url === "string")
5280
+ return responseContent([
5281
+ { type: "input_image", image_url: block.url }
5282
+ ]);
5283
+ return [{ type: "text", text: JSON.stringify(block) }];
5284
+ }
5285
+ )
4874
5286
  )).flat();
4875
5287
  break;
4876
5288
  case "agentMessage":
@@ -4886,8 +5298,7 @@ async function itemMessage(item) {
4886
5298
  if (item.result) {
4887
5299
  const { content: blocks, ...metadata } = item.result;
4888
5300
  message.result = metadata;
4889
- if (metadata.structuredContent?.details)
4890
- message.details = metadata.structuredContent.details;
5301
+ if (metadata._meta?.details) message.details = metadata._meta.details;
4891
5302
  content = responseContent(blocks);
4892
5303
  } else {
4893
5304
  delete message.error;
@@ -4923,42 +5334,233 @@ async function serveCodex() {
4923
5334
  const clients = /* @__PURE__ */ new Set();
4924
5335
  const stopped = Promise.withResolvers();
4925
5336
  const stop = () => stopped.resolve();
4926
- let events = Promise.resolve();
4927
- const rpc = new AppServer((message) => {
4928
- events = events.then(() => receive(message)).catch((error) => console.error(error));
4929
- });
4930
- function register(thread) {
5337
+ const events = { rpc: Promise.resolve(), desktop: Promise.resolve() };
5338
+ let rpc;
5339
+ let desktop;
5340
+ let rpcConnecting;
5341
+ let desktopConnecting;
5342
+ function register(thread, connection) {
4931
5343
  let state = sessions.get(thread.id);
5344
+ if (state && state.connection !== connection) {
5345
+ if (state.connection instanceof AppServer || connection instanceof AppServer || state.connection.desktop !== connection.desktop) {
5346
+ state.session.close();
5347
+ state = void 0;
5348
+ } else state.connection.owner = connection.owner;
5349
+ }
4932
5350
  if (state) state.thread = thread;
4933
5351
  else {
4934
- state = new CodexSession(thread, rpc, config);
5352
+ state = new CodexSession(thread, connection, config);
4935
5353
  sessions.set(thread.id, state);
4936
5354
  }
4937
5355
  state.session.update();
4938
5356
  return state;
4939
5357
  }
4940
- const get = (threadId) => {
5358
+ function disconnected(connection) {
5359
+ if (connection === rpc) rpc = void 0;
5360
+ if (connection === desktop) desktop = void 0;
5361
+ for (const [id, state] of sessions)
5362
+ if (state.connection === connection || !(state.connection instanceof AppServer) && state.connection.desktop === connection) {
5363
+ state.session.close();
5364
+ sessions.delete(id);
5365
+ }
5366
+ }
5367
+ function attachDesktop() {
5368
+ desktopConnecting ??= (async () => {
5369
+ if (desktop) return desktop;
5370
+ const connection = new Desktop((message) => {
5371
+ events.desktop = events.desktop.then(async () => {
5372
+ await desktopConnecting;
5373
+ if (desktop === connection)
5374
+ await receiveDesktop(connection, message);
5375
+ }).catch((error) => console.error(error));
5376
+ });
5377
+ desktop = connection;
5378
+ void connection.closed.then(() => disconnected(connection));
5379
+ try {
5380
+ await connection.connect(codexHome);
5381
+ return connection;
5382
+ } catch (error) {
5383
+ connection.close();
5384
+ if (!["ENOENT", "ECONNREFUSED"].includes(
5385
+ error.code ?? ""
5386
+ ))
5387
+ throw error;
5388
+ return void 0;
5389
+ }
5390
+ })().finally(() => {
5391
+ desktopConnecting = void 0;
5392
+ });
5393
+ return desktopConnecting;
5394
+ }
5395
+ function attachServer() {
5396
+ rpcConnecting ??= (async () => {
5397
+ if (rpc) return rpc;
5398
+ if (!config.codex?.appServer) {
5399
+ try {
5400
+ await access(
5401
+ join2(codexHome, "app-server-control", "app-server-control.sock")
5402
+ );
5403
+ } catch (error) {
5404
+ if (error.code === "ENOENT")
5405
+ return void 0;
5406
+ throw error;
5407
+ }
5408
+ }
5409
+ const connection = new AppServer((message) => {
5410
+ events.rpc = events.rpc.then(async () => {
5411
+ await rpcConnecting;
5412
+ if (rpc === connection) await receive(message, connection);
5413
+ }).catch((error) => console.error(error));
5414
+ });
5415
+ rpc = connection;
5416
+ void connection.closed.then(() => disconnected(connection));
5417
+ await connection.connect(appServer);
5418
+ for await (const threadId of loaded(connection)) {
5419
+ try {
5420
+ const { thread } = await connection.request(
5421
+ "thread/read",
5422
+ { threadId }
5423
+ );
5424
+ register(thread, connection);
5425
+ } catch (error) {
5426
+ console.error(error);
5427
+ }
5428
+ }
5429
+ return connection;
5430
+ })().finally(() => {
5431
+ rpcConnecting = void 0;
5432
+ });
5433
+ return rpcConnecting;
5434
+ }
5435
+ function attach() {
5436
+ void attachDesktop().catch((error) => console.error(error));
5437
+ void attachServer().catch((error) => console.error(error));
5438
+ }
5439
+ const get = (threadId, control = false) => {
4941
5440
  const existing = sessions.get(threadId);
4942
- if (existing) return Promise.resolve(existing);
5441
+ if (existing && (!control || existing.thread.status.type !== "notLoaded"))
5442
+ return Promise.resolve(existing);
4943
5443
  let pending = loading.get(threadId);
4944
5444
  if (!pending) {
4945
- pending = rpc.request("thread/read", { threadId }).then(({ thread }) => register(thread)).finally(() => loading.delete(threadId));
5445
+ pending = (async () => {
5446
+ const connection = await Promise.any([
5447
+ attachServer().then(async (connection2) => {
5448
+ if (connection2) {
5449
+ for await (const id of loaded(connection2))
5450
+ if (id === threadId) return connection2;
5451
+ }
5452
+ throw new RequestError(
5453
+ "Thread is not loaded in the Codex app-server"
5454
+ );
5455
+ }),
5456
+ attachDesktop().then(async (desktop2) => {
5457
+ const owner = await desktop2?.owner(threadId);
5458
+ if (desktop2 && owner) return { desktop: desktop2, owner };
5459
+ throw new RequestError("Thread has no Codex desktop owner");
5460
+ })
5461
+ ]).catch((error) => {
5462
+ const Failure = error.errors.every(
5463
+ (reason) => reason instanceof RequestError
5464
+ ) ? RequestError : Error;
5465
+ throw new Failure(
5466
+ `No connected Codex client owns thread ${threadId}: ${error.errors.map(String).join("; ")}`,
5467
+ { cause: error }
5468
+ );
5469
+ });
5470
+ if (connection instanceof AppServer) {
5471
+ const { thread } = await connection.request(
5472
+ "thread/read",
5473
+ { threadId }
5474
+ );
5475
+ return register(thread, connection);
5476
+ }
5477
+ const snapshot = await connection.desktop.snapshot(
5478
+ threadId,
5479
+ connection.owner
5480
+ );
5481
+ const state = register(desktopThread(snapshot), connection);
5482
+ await state.snapshot(snapshot);
5483
+ return state;
5484
+ })().finally(() => loading.delete(threadId));
4946
5485
  loading.set(threadId, pending);
4947
5486
  }
4948
- return pending;
5487
+ return control ? pending : pending.catch(async (error) => {
5488
+ const connection = await attachServer();
5489
+ if (!connection) throw error;
5490
+ const { thread } = await connection.request(
5491
+ "thread/read",
5492
+ { threadId }
5493
+ );
5494
+ return register(thread, connection);
5495
+ });
4949
5496
  };
4950
- async function receive(message) {
5497
+ async function receiveDesktop(connection, message) {
5498
+ const params = message.params;
5499
+ if (!params) return;
5500
+ if (message.method === "client-status-changed") {
5501
+ if (params.status === "connected" && params.isSelf !== true && typeof params.clientId === "string") {
5502
+ for (const [id, state2] of sessions)
5503
+ if (!(state2.connection instanceof AppServer) && state2.connection.desktop === connection)
5504
+ connection.follow(id, params.clientId);
5505
+ }
5506
+ if (params.status === "disconnected") {
5507
+ for (const [id, state2] of sessions)
5508
+ if (!(state2.connection instanceof AppServer) && state2.connection.owner === params.clientId) {
5509
+ state2.session.close();
5510
+ sessions.delete(id);
5511
+ }
5512
+ }
5513
+ return;
5514
+ }
5515
+ if (params.hostId !== "local" || typeof params.conversationId !== "string" || !message.sourceClientId)
5516
+ return;
5517
+ const threadId = params.conversationId;
5518
+ if (message.method === "thread-stream-following-changed" && params.following === true || message.method === "thread-stream-following-status-requested") {
5519
+ if (sessions.has(threadId) && message.method === "thread-stream-following-changed")
5520
+ return;
5521
+ const owner = message.method === "thread-stream-following-status-requested" ? message.sourceClientId : await connection.owner(threadId, message.sourceClientId);
5522
+ if (owner) {
5523
+ const snapshot = await connection.snapshot(threadId, owner);
5524
+ await register(desktopThread(snapshot), {
5525
+ desktop: connection,
5526
+ owner
5527
+ }).snapshot(snapshot);
5528
+ }
5529
+ return;
5530
+ }
5531
+ const state = sessions.get(threadId);
5532
+ if (!state || state.connection instanceof AppServer || state.connection.desktop !== connection)
5533
+ return;
5534
+ if (message.method === "thread-stream-state-changed") {
5535
+ const change = params.change;
5536
+ if (state.connection.owner !== message.sourceClientId) {
5537
+ const owner = await connection.owner(threadId, message.sourceClientId);
5538
+ if (!owner) return;
5539
+ const snapshot = change.type === "snapshot" ? change.conversationState : await connection.snapshot(threadId, owner);
5540
+ await register(desktopThread(snapshot), {
5541
+ desktop: connection,
5542
+ owner
5543
+ }).snapshot(snapshot);
5544
+ } else if (change.type === "snapshot")
5545
+ await state.snapshot(change.conversationState);
5546
+ else if (change.type === "patches") await state.patches(change.patches);
5547
+ } else if (message.method === "thread-archived" && state.connection.owner === message.sourceClientId) {
5548
+ state.session.close();
5549
+ sessions.delete(threadId);
5550
+ }
5551
+ }
5552
+ async function receive(message, connection) {
4951
5553
  const params = message.params;
4952
5554
  if (!params) return;
4953
5555
  if (message.id !== void 0) return;
4954
5556
  if (message.method === "thread/started") {
4955
5557
  const thread = params.thread;
4956
- register(thread);
5558
+ register(thread, connection);
4957
5559
  return;
4958
5560
  }
4959
5561
  if (typeof params.threadId !== "string") return;
4960
5562
  const state = sessions.get(params.threadId);
4961
- if (!state) return;
5563
+ if (!state || state.connection !== connection) return;
4962
5564
  switch (message.method) {
4963
5565
  case "thread/settings/updated": {
4964
5566
  const settings = params.threadSettings;
@@ -4972,6 +5574,7 @@ async function serveCodex() {
4972
5574
  break;
4973
5575
  case "thread/status/changed":
4974
5576
  state.thread.status = params.status;
5577
+ state.session.update();
4975
5578
  break;
4976
5579
  case "thread/closed":
4977
5580
  case "thread/archived":
@@ -4994,13 +5597,16 @@ async function serveCodex() {
4994
5597
  }
4995
5598
  const server = createServer((request, response) => {
4996
5599
  void handle(request, response).catch((error) => {
5600
+ const status = error instanceof RequestError ? 400 : 500;
5601
+ if (status === 500) console.error(error);
4997
5602
  if (response.headersSent)
4998
5603
  response.destroy(error instanceof Error ? error : void 0);
4999
5604
  else {
5000
- response.writeHead(500, { "Content-Type": "application/json" });
5605
+ response.writeHead(status, { "Content-Type": "application/json" });
5001
5606
  response.end(
5002
5607
  JSON.stringify({
5003
5608
  error: {
5609
+ type: status === 400 ? "invalid_request_error" : "server_error",
5004
5610
  message: error instanceof Error ? error.message : String(error)
5005
5611
  }
5006
5612
  })
@@ -5016,7 +5622,7 @@ async function serveCodex() {
5016
5622
  controller.abort(new Error("Codex request cancelled"));
5017
5623
  });
5018
5624
  if (request.method === "GET" && route === "/client") {
5019
- await connected;
5625
+ attach();
5020
5626
  clients.add(response);
5021
5627
  response.once("close", () => {
5022
5628
  clients.delete(response);
@@ -5027,43 +5633,54 @@ async function serveCodex() {
5027
5633
  return;
5028
5634
  }
5029
5635
  if (request.method === "GET" && route === "/") {
5030
- await connected;
5031
5636
  response.setHeader("Content-Type", "application/json");
5032
5637
  response.end(JSON.stringify({ name: "chappie", codexHome, appServer }));
5033
5638
  return;
5034
5639
  }
5035
- if (request.method !== "POST") {
5036
- response.writeHead(404);
5037
- response.end();
5038
- return;
5039
- }
5040
- await connected;
5640
+ if (request.method !== "POST")
5641
+ throw new RequestError(
5642
+ `Unsupported Chappie request: ${request.method} ${route}`
5643
+ );
5041
5644
  const chunks = [];
5042
5645
  for await (const chunk of request) chunks.push(Buffer.from(chunk));
5043
- const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
5646
+ const body = parseObject(Buffer.concat(chunks).toString("utf8"));
5044
5647
  if (route === "/v1/responses") {
5648
+ const payload = body;
5649
+ const metadata = payload.client_metadata?.["x-codex-turn-metadata"];
5650
+ const context = metadata ? parseObject(metadata) : void 0;
5651
+ if (context?.thread_source === "memory_consolidation")
5652
+ throw new RequestError(
5653
+ "Background memory generation is not supported by Chappie"
5654
+ );
5045
5655
  const threadId = request.headers["thread-id"];
5046
5656
  if (typeof threadId !== "string")
5047
- throw new Error("Codex did not provide a thread-id header");
5048
- const state = await get(threadId);
5049
- await events;
5657
+ throw new RequestError("Codex did not provide a thread-id header");
5658
+ const state = await get(threadId, true);
5659
+ await (state.connection instanceof AppServer ? events.rpc : events.desktop);
5050
5660
  if (!state.active()) {
5051
- const { thread } = await rpc.request(
5052
- "thread/read",
5053
- { threadId }
5054
- );
5055
- register(thread);
5661
+ if (state.connection instanceof AppServer) {
5662
+ const { thread } = await state.connection.request(
5663
+ "thread/read",
5664
+ { threadId }
5665
+ );
5666
+ register(thread, state.connection);
5667
+ } else {
5668
+ await state.snapshot(
5669
+ await state.connection.desktop.snapshot(
5670
+ threadId,
5671
+ state.connection.owner
5672
+ )
5673
+ );
5674
+ }
5056
5675
  }
5057
- await state.respond(
5058
- body,
5059
- response,
5060
- controller.signal
5061
- );
5676
+ if (!state.active())
5677
+ throw new RequestError("Chappie is not active for this session");
5678
+ await state.respond(payload, context, response, controller.signal);
5062
5679
  return;
5063
5680
  }
5064
5681
  if (route === "/resource") {
5065
5682
  if (typeof body.threadId !== "string" || typeof body.uri !== "string")
5066
- throw new Error("Resource requests require threadId and uri");
5683
+ throw new RequestError("Resource requests require threadId and uri");
5067
5684
  const state = await get(body.threadId);
5068
5685
  const resource = await state.session.resource(
5069
5686
  body.uri,
@@ -5075,20 +5692,18 @@ async function serveCodex() {
5075
5692
  }
5076
5693
  if (route.startsWith("/tools/")) {
5077
5694
  if (typeof body.threadId !== "string")
5078
- throw new Error("Codex did not provide threadId metadata");
5695
+ throw new RequestError("Codex did not provide threadId metadata");
5079
5696
  const state = await get(body.threadId);
5080
5697
  const name = decodeURIComponent(route.slice("/tools/".length));
5081
5698
  const tool = state.tools.find((tool2) => tool2.name === name);
5082
- if (!tool) throw new Error(`Unknown Chappie tool: ${name}`);
5699
+ if (!tool) throw new RequestError(`Unknown Chappie tool: ${name}`);
5083
5700
  const result = await tool.execute(body.arguments, controller.signal);
5084
5701
  response.setHeader("Content-Type", "application/json");
5085
5702
  response.end(JSON.stringify(result));
5086
5703
  return;
5087
5704
  }
5088
- response.writeHead(404);
5089
- response.end();
5705
+ throw new RequestError(`Unknown Chappie endpoint: ${route}`);
5090
5706
  }
5091
- let connected;
5092
5707
  try {
5093
5708
  try {
5094
5709
  await new Promise((resolve, reject) => {
@@ -5109,32 +5724,17 @@ async function serveCodex() {
5109
5724
  process.send?.({ ready: true });
5110
5725
  return;
5111
5726
  }
5112
- connected = rpc.connect(appServer);
5113
- await connected;
5727
+ attach();
5114
5728
  process.send?.({ ready: true });
5115
- let cursor = null;
5116
- do {
5117
- const page = await rpc.request(
5118
- "thread/loaded/list",
5119
- cursor ? { cursor } : {}
5120
- );
5121
- for (const id of page.data) {
5122
- try {
5123
- await get(id);
5124
- } catch (error) {
5125
- console.error(error);
5126
- }
5127
- }
5128
- cursor = page.nextCursor;
5129
- } while (cursor);
5130
5729
  process.once("SIGINT", stop);
5131
5730
  process.once("SIGTERM", stop);
5132
- await Promise.race([stopped.promise, rpc.closed]);
5731
+ await stopped.promise;
5133
5732
  process.off("SIGINT", stop);
5134
5733
  process.off("SIGTERM", stop);
5135
5734
  } finally {
5136
5735
  for (const state of sessions.values()) state.session.close();
5137
- rpc.close();
5736
+ rpc?.close();
5737
+ desktop?.close();
5138
5738
  server.closeAllConnections();
5139
5739
  await new Promise((resolve) => server.close(() => resolve()));
5140
5740
  }