linkshell-cli 0.10.2 → 0.10.4

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.
@@ -1,8 +1,28 @@
1
1
  /** Where the desktop app's bus listens (the first Codex window to start serves it). */
2
2
  export declare function desktopBusSocket(env?: NodeJS.ProcessEnv): string;
3
3
  /**
4
- * Asks whichever Codex window runs `threadId` to stop its turn, as its own
5
- * Stop button does. Resolves when that window says it has; rejects when no
6
- * window runs the thread, none answers in time, or the app isn't there.
4
+ * The window that runs the thread was asked and didn't say how it went: the
5
+ * app gives its own windows five seconds to answer each other, and starting a
6
+ * turn can take it longer than that. What was asked for usually still happens.
7
7
  */
8
- export declare function interruptThroughDesktop(socketPath: string, threadId: string, turnId: string | undefined, timeoutMs?: number): Promise<void>;
8
+ export declare class DesktopUnconfirmed extends Error {
9
+ }
10
+ /**
11
+ * Asks the window that runs `threadId` to stop its turn, as its own Stop
12
+ * button does. Resolves when that window says it has.
13
+ */
14
+ export declare function interruptThroughDesktop(socketPath: string, threadId: string, turnId: string | undefined): Promise<void>;
15
+ export interface DesktopMessage {
16
+ /** Codex's own input items (text, images). */
17
+ input: unknown[];
18
+ text: string;
19
+ clientMessageId: string;
20
+ cwd?: string;
21
+ }
22
+ /**
23
+ * Puts a message into the turn the window that runs `threadId` is running, as
24
+ * its own "Steer" does for a message waiting in its queue.
25
+ */
26
+ export declare function steerThroughDesktop(socketPath: string, threadId: string, message: DesktopMessage): Promise<void>;
27
+ /** Starts a turn with a message in the window that runs `threadId`, as typing it there does. */
28
+ export declare function startThroughDesktop(socketPath: string, threadId: string, message: DesktopMessage): Promise<void>;
@@ -8,18 +8,38 @@ import { join } from "node:path";
8
8
  // is a message bus between their windows, on a socket in Codex's home: a
9
9
  // window that doesn't run a thread asks the one that does to act on it
10
10
  // ("thread-follower-…" requests). That is the only way to stop a turn the
11
- // desktop app is running without stopping the app.
11
+ // desktop app is running, or to put a message into it, without stopping the
12
+ // app.
12
13
  //
13
14
  // The bus is the app's own, not a published interface: each method carries a
14
15
  // version the receiver checks, and a request it doesn't recognise is refused.
15
16
  // So everything here is one question with a yes or a no; a no is reported as
16
- // "can't be stopped from here", never retried in another shape.
17
+ // "can't be done from here", never retried in another shape.
17
18
  /** Where the desktop app's bus listens (the first Codex window to start serves it). */
18
19
  export function desktopBusSocket(env = process.env) {
19
20
  return join(env.CODEX_HOME || join(homedir(), ".codex"), "ipc", "ipc.sock");
20
21
  }
21
- /** The version of the interrupt request the installed apps accept: with the turn it is meant for, or (older) without. */
22
- const INTERRUPT_VERSION = { withTurn: 4, anyTurn: 3 };
22
+ /** The versions of the requests the installed apps accept. */
23
+ const VERSIONS = {
24
+ owner: 1,
25
+ start: 2,
26
+ steer: 1,
27
+ /** With the turn it is meant for, or (older) without. */
28
+ interrupt: { withTurn: 4, anyTurn: 3 },
29
+ };
30
+ /**
31
+ * The window that runs a thread says so at once. A thread no window runs is
32
+ * only reported after the bus has waited ten seconds for every client that
33
+ * doesn't answer it, which is too long to keep a phone waiting for a no.
34
+ */
35
+ const OWNER_TIMEOUT_MS = 3000;
36
+ /**
37
+ * The window that runs the thread was asked and didn't say how it went: the
38
+ * app gives its own windows five seconds to answer each other, and starting a
39
+ * turn can take it longer than that. What was asked for usually still happens.
40
+ */
41
+ export class DesktopUnconfirmed extends Error {
42
+ }
23
43
  function frame(message) {
24
44
  const body = Buffer.from(JSON.stringify(message), "utf8");
25
45
  const head = Buffer.alloc(4);
@@ -27,14 +47,11 @@ function frame(message) {
27
47
  return Buffer.concat([head, body]);
28
48
  }
29
49
  /**
30
- * Asks whichever Codex window runs `threadId` to stop its turn, as its own
31
- * Stop button does. Resolves when that window says it has; rejects when no
32
- * window runs the thread, none answers in time, or the app isn't there.
50
+ * One request to the window that runs `threadId`: found first, then asked.
51
+ * Rejects when the app isn't there, no window runs the thread, or the window
52
+ * refuses.
33
53
  */
34
- export async function interruptThroughDesktop(socketPath, threadId, turnId,
35
- // The window that runs the thread answers at once. The bus itself waits ten
36
- // seconds for every client that doesn't answer before it says nobody does.
37
- timeoutMs = 3000) {
54
+ async function askOwner(socketPath, threadId, method, params, version, timeoutMs) {
38
55
  if (!existsSync(socketPath))
39
56
  throw new Error("the Codex desktop app isn't running");
40
57
  const socket = connect(socketPath);
@@ -43,17 +60,60 @@ timeoutMs = 3000) {
43
60
  socket.once("connect", resolve);
44
61
  socket.once("error", reject);
45
62
  });
46
- const bus = busClient(socket, timeoutMs);
47
- const registered = await bus.request("initialize", { clientType: "linkshell" }, 0);
48
- bus.clientId = String(registered.clientId ?? "");
49
- await bus.request("thread-follower-interrupt-turn", { conversationId: threadId, mode: "user-stop", ...(turnId ? { expectedTurnId: turnId } : {}) }, turnId ? INTERRUPT_VERSION.withTurn : INTERRUPT_VERSION.anyTurn);
63
+ const bus = busClient(socket);
64
+ const registered = await bus.request("initialize", { clientType: "linkshell" }, 0, OWNER_TIMEOUT_MS);
65
+ bus.clientId = String(registered.result?.clientId ?? "");
66
+ const found = await bus.request("thread-owner-discovery", { hostId: "local", conversationId: threadId }, VERSIONS.owner, OWNER_TIMEOUT_MS).catch((error) => {
67
+ throw new Error(`no window of the Codex desktop app runs this thread (${error.message})`);
68
+ });
69
+ const owner = typeof found.handledByClientId === "string" ? found.handledByClientId : undefined;
70
+ const answered = await bus.request(method, params, version, timeoutMs, owner).catch((error) => {
71
+ throw /timeout$/.test(error.message) ? new DesktopUnconfirmed(error.message) : error;
72
+ });
73
+ return answered.result;
50
74
  }
51
75
  finally {
52
76
  socket.destroy();
53
77
  }
54
78
  }
79
+ /**
80
+ * Asks the window that runs `threadId` to stop its turn, as its own Stop
81
+ * button does. Resolves when that window says it has.
82
+ */
83
+ export async function interruptThroughDesktop(socketPath, threadId, turnId) {
84
+ await askOwner(socketPath, threadId, "thread-follower-interrupt-turn", { conversationId: threadId, mode: "user-stop", ...(turnId ? { expectedTurnId: turnId } : {}) }, turnId ? VERSIONS.interrupt.withTurn : VERSIONS.interrupt.anyTurn, 12_000);
85
+ }
86
+ /**
87
+ * Puts a message into the turn the window that runs `threadId` is running, as
88
+ * its own "Steer" does for a message waiting in its queue.
89
+ */
90
+ export async function steerThroughDesktop(socketPath, threadId, message) {
91
+ await askOwner(socketPath, threadId, "thread-follower-steer-turn", {
92
+ conversationId: threadId,
93
+ clientUserMessageId: message.clientMessageId,
94
+ input: message.input,
95
+ attachments: [],
96
+ // What the window puts back in its composer if the turn ends before the
97
+ // message is taken: the app builds one for every message it steers, and
98
+ // reads the fields below from it without checking that it is there.
99
+ restoreMessage: {
100
+ id: message.clientMessageId,
101
+ text: message.text,
102
+ context: { prompt: message.text, addedFiles: [], fileAttachments: [], ideContext: null, imageAttachments: [], commentAttachments: [] },
103
+ cwd: message.cwd ?? null,
104
+ createdAt: Date.now(),
105
+ },
106
+ }, VERSIONS.steer, 20_000);
107
+ }
108
+ /** Starts a turn with a message in the window that runs `threadId`, as typing it there does. */
109
+ export async function startThroughDesktop(socketPath, threadId, message) {
110
+ await askOwner(socketPath, threadId, "thread-follower-start-turn", {
111
+ conversationId: threadId,
112
+ turnStart: { request: { threadId, input: message.input, clientUserMessageId: message.clientMessageId }, context: {} },
113
+ }, VERSIONS.start, 30_000);
114
+ }
55
115
  /** One connection to the bus: length-prefixed JSON, requests answered by `requestId`. */
56
- function busClient(socket, timeoutMs) {
116
+ function busClient(socket) {
57
117
  const pending = new Map();
58
118
  let buffered = Buffer.alloc(0);
59
119
  const failAll = (error) => {
@@ -83,7 +143,7 @@ function busClient(socket, timeoutMs) {
83
143
  continue;
84
144
  pending.delete(String(message.requestId));
85
145
  if (message.resultType === "success")
86
- waiting.resolve(message.result);
146
+ waiting.resolve(message);
87
147
  else
88
148
  waiting.reject(new Error(String(message.error ?? "refused")));
89
149
  }
@@ -101,7 +161,7 @@ function busClient(socket, timeoutMs) {
101
161
  socket.on("error", () => { });
102
162
  const client = {
103
163
  clientId: "initializing-client",
104
- request(method, params, version) {
164
+ request(method, params, version, timeoutMs, targetClientId) {
105
165
  const requestId = randomUUID();
106
166
  return new Promise((resolve, reject) => {
107
167
  const timer = setTimeout(() => {
@@ -109,16 +169,16 @@ function busClient(socket, timeoutMs) {
109
169
  reject(new Error("timeout"));
110
170
  }, timeoutMs);
111
171
  pending.set(requestId, {
112
- resolve: (result) => {
172
+ resolve: (response) => {
113
173
  clearTimeout(timer);
114
- resolve(result);
174
+ resolve(response);
115
175
  },
116
176
  reject: (error) => {
117
177
  clearTimeout(timer);
118
178
  reject(error);
119
179
  },
120
180
  });
121
- socket.write(frame({ type: "request", requestId, sourceClientId: client.clientId, version, method, params, timeoutMs }));
181
+ socket.write(frame({ type: "request", requestId, sourceClientId: client.clientId, version, method, params, timeoutMs, ...(targetClientId ? { targetClientId } : {}) }));
122
182
  });
123
183
  },
124
184
  };
@@ -1 +1 @@
1
- {"version":3,"file":"desktop-ipc.js","sourceRoot":"","sources":["../../../../../../host/src/drivers/codex/desktop-ipc.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"desktop-ipc.js","sourceRoot":"","sources":["../../../../../../host/src/drivers/codex/desktop-ipc.ts"],"names":[],"mappings":"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"}
@@ -22,6 +22,8 @@ export interface CodexDriverOptions {
22
22
  observeIntervalMs?: number;
23
23
  /** How long a thread no device has open, and that isn't working, stays joined. */
24
24
  releaseDelayMs?: number;
25
+ /** How long to look for a turn the desktop app was asked to start and didn't confirm. */
26
+ confirmStartMs?: number;
25
27
  }
26
28
  /**
27
29
  * Codex, tier multi_client: LinkShell and the desktop TUI (`codex --remote`)
@@ -93,6 +95,7 @@ export declare class CodexDriver implements AgentDriver {
93
95
  private readonly announced;
94
96
  private readonly observeIntervalMs;
95
97
  private readonly releaseDelayMs;
98
+ private readonly confirmStartMs;
96
99
  private readonly sharedSocketPath?;
97
100
  private readonly desktopBusPath?;
98
101
  constructor(options: CodexDriverOptions);
@@ -140,8 +143,19 @@ export declare class CodexDriver implements AgentDriver {
140
143
  private syncWaiting;
141
144
  private setWaiting;
142
145
  unqueue(nativeId: string, clientMessageId: string): boolean;
143
- /** A queued message can't be put into the turn another Codex is running: that turn is stopped, and the queue goes on. */
144
- sendQueuedNow(nativeId: string): Promise<void>;
146
+ /**
147
+ * A message waiting in Codex's queue goes into the turn another Codex is
148
+ * running, by asking that Codex's app: what its own "Steer" on the waiting
149
+ * message does.
150
+ */
151
+ sendQueuedNow(nativeId: string, clientMessageId?: string): Promise<void>;
152
+ /**
153
+ * Starts a turn in the desktop app that has the thread. The app often
154
+ * starts it and answers too late for its own bus: then the thread itself
155
+ * says whether the turn began, so the message isn't queued as well and run twice.
156
+ */
157
+ private startElsewhere;
158
+ private desktopMessage;
145
159
  /**
146
160
  * Stops the turn a Codex that can't be joined is running. Only its own app
147
161
  * can: it is asked over the bus its windows share.
@@ -6,7 +6,7 @@ import { APPROVAL_METHODS, QUESTION_METHODS, mapApprovalRequest, mapQuestionRequ
6
6
  import { nestHistory, nestUnder } from "../nesting.js";
7
7
  import { configOptions, effective, settingsFrom, turnOverrides } from "./settings.js";
8
8
  import { COMMANDS, INIT_PROMPT, commandOf } from "./commands.js";
9
- import { desktopBusSocket, interruptThroughDesktop } from "./desktop-ipc.js";
9
+ import { DesktopUnconfirmed, desktopBusSocket, interruptThroughDesktop, startThroughDesktop, steerThroughDesktop } from "./desktop-ipc.js";
10
10
  /** Codex's refusal to load a thread that another process has loaded. */
11
11
  function heldElsewhere(error) {
12
12
  return error instanceof Error && /already has an active writer/i.test(error.message);
@@ -20,13 +20,18 @@ function fileStamp(path) {
20
20
  return undefined;
21
21
  }
22
22
  }
23
+ /** The text of a message as Codex holds it. */
24
+ function textOf(input) {
25
+ return input.map((part) => (part.type === "text" && typeof part.text === "string" ? part.text : "")).join("").trim();
26
+ }
23
27
  /** A request id from Codex's background server: both servers count theirs from the same numbers. */
24
28
  const sharedRequestId = (id) => `shared-${id}`;
25
29
  // (Shown as one small line in the conversation: short enough to read at a glance.)
26
- const HELD_NOTICE = { title: "会话开在电脑的另一个 Codex 里 · 消息会排队,等这一轮结束后发送" };
30
+ const HELD_NOTICE = { title: "会话开在电脑的另一个 Codex 里 · 这一轮结束前发的消息会排队" };
27
31
  const HELD_SEND = "会话开在电脑的另一个 Codex 里,这条消息没能排进它的队列。在那边关掉这个会话后再发。";
28
- const HELD_STOP = "这一轮跑在电脑的另一个 Codex(桌面 App 或 IDE 插件)里,手机停不了它:请在电脑上停止。";
29
- const HELD_NOW = "这一轮跑在电脑的另一个 Codex(桌面 App 或 IDE 插件)里,手机插不进去。它结束后这条消息会自动发送;想现在就插入,在电脑上对这条排队消息点 Steer。";
32
+ // (The desktop app's windows can lose each other, after which none of them answers for its threads until the app is restarted.)
33
+ const HELD_STOP = "这一轮跑在电脑的 Codex 桌面 App(或 IDE 插件)里,这次没能从手机停下它。请在电脑上停止;一直这样的话,重启一下 Codex 桌面 App。";
34
+ const HELD_NOW = "这一轮跑在电脑的 Codex 桌面 App(或 IDE 插件)里,这次没能从手机插进去。它结束后这条消息会自动发送;想现在就插入,在电脑上对这条排队消息点 Steer。一直这样的话,重启一下 Codex 桌面 App。";
30
35
  const HELD_COMMAND = "会话开在电脑的另一个 Codex 里,要在那边关掉它之后才能从手机做。";
31
36
  /**
32
37
  * Codex, tier multi_client: LinkShell and the desktop TUI (`codex --remote`)
@@ -98,6 +103,7 @@ export class CodexDriver {
98
103
  announced = new Map();
99
104
  observeIntervalMs;
100
105
  releaseDelayMs;
106
+ confirmStartMs;
101
107
  sharedSocketPath;
102
108
  desktopBusPath;
103
109
  constructor(options) {
@@ -107,6 +113,7 @@ export class CodexDriver {
107
113
  this.sharedSocketPath = options.sharedSocketPath === false ? undefined : (options.sharedSocketPath ?? sharedServerSocket(options.env));
108
114
  this.observeIntervalMs = options.observeIntervalMs ?? 2000;
109
115
  this.releaseDelayMs = options.releaseDelayMs ?? 30_000;
116
+ this.confirmStartMs = options.confirmStartMs ?? 20_000;
110
117
  }
111
118
  status() {
112
119
  return this.current;
@@ -467,6 +474,17 @@ export class CodexDriver {
467
474
  * That process starts it when it is idle, or right after the turn it is running.
468
475
  */
469
476
  async enqueue(threadId, input, clientMessageId) {
477
+ const watch = this.observed.get(threadId);
478
+ if (watch && !watch.running && this.desktopBusPath) {
479
+ // Idle: the app starts it at once when asked, where its queue is only looked at every so often.
480
+ try {
481
+ await this.startElsewhere(threadId, this.desktopMessage(threadId, input, clientMessageId));
482
+ return "started";
483
+ }
484
+ catch (error) {
485
+ this.host?.log(`[codex] the Codex desktop app didn't start a turn in ${threadId}, queueing instead: ${error instanceof Error ? error.message : String(error)}`);
486
+ }
487
+ }
470
488
  try {
471
489
  await this.rpc("thread/queue/add", { threadId, input, clientUserMessageId: clientMessageId });
472
490
  }
@@ -487,8 +505,9 @@ export class CodexDriver {
487
505
  this.setWaiting(threadId, listed.data.map((entry) => ({
488
506
  clientMessageId: entry.clientUserMessageId,
489
507
  submissionId: entry.id,
490
- text: entry.input.map((part) => (part.type === "text" && typeof part.text === "string" ? part.text : "")).join("").trim(),
508
+ text: textOf(entry.input),
491
509
  images: entry.input.filter((part) => part.type === "image" || part.type === "localImage").length,
510
+ input: entry.input,
492
511
  })));
493
512
  }
494
513
  setWaiting(threadId, waiting) {
@@ -511,13 +530,73 @@ export class CodexDriver {
511
530
  void this.rpc("thread/queue/delete", { threadId: nativeId, queuedSubmissionId: entry.submissionId }).catch(() => { });
512
531
  return true;
513
532
  }
514
- /** A queued message can't be put into the turn another Codex is running: that turn is stopped, and the queue goes on. */
515
- async sendQueuedNow(nativeId) {
516
- if (!this.waiting.has(nativeId))
533
+ /**
534
+ * A message waiting in Codex's queue goes into the turn another Codex is
535
+ * running, by asking that Codex's app: what its own "Steer" on the waiting
536
+ * message does.
537
+ */
538
+ async sendQueuedNow(nativeId, clientMessageId) {
539
+ const waiting = this.waiting.get(nativeId) ?? [];
540
+ const entry = clientMessageId ? waiting.find((candidate) => candidate.clientMessageId === clientMessageId) : waiting[0];
541
+ const watch = this.observed.get(nativeId);
542
+ // (Joined since: what waits is started by the app-server the thread is in now.)
543
+ if (!entry || !watch)
517
544
  return;
518
- await this.stopElsewhere(nativeId).catch(() => {
545
+ try {
546
+ if (!this.desktopBusPath)
547
+ throw new Error("not asked");
548
+ const message = this.desktopMessage(nativeId, entry.input, entry.clientMessageId);
549
+ if (watch.running) {
550
+ await steerThroughDesktop(this.desktopBusPath, nativeId, message).catch((error) => {
551
+ // Asked, and the app is at it: left in the queue too, it would be said twice.
552
+ if (!(error instanceof DesktopUnconfirmed))
553
+ throw error;
554
+ this.host?.log(`[codex] the Codex desktop app took a message for ${nativeId} without saying how it went`);
555
+ });
556
+ }
557
+ else {
558
+ await this.startElsewhere(nativeId, message);
559
+ }
560
+ }
561
+ catch (error) {
562
+ this.host?.log(`[codex] couldn't put a message into ${nativeId} through the Codex desktop app: ${error instanceof Error ? error.message : String(error)}`);
519
563
  throw RpcError.app("busy", HELD_NOW);
520
- });
564
+ }
565
+ // In the turn now. (Taken out of the queue only after: a message that runs twice is better than one that is lost.)
566
+ this.setWaiting(nativeId, waiting.filter((candidate) => candidate !== entry));
567
+ await this.rpc("thread/queue/delete", { threadId: nativeId, queuedSubmissionId: entry.submissionId }).catch(() => { });
568
+ void this.refresh(nativeId);
569
+ }
570
+ /**
571
+ * Starts a turn in the desktop app that has the thread. The app often
572
+ * starts it and answers too late for its own bus: then the thread itself
573
+ * says whether the turn began, so the message isn't queued as well and run twice.
574
+ */
575
+ async startElsewhere(threadId, message) {
576
+ if (!this.desktopBusPath)
577
+ throw new Error("not asked");
578
+ try {
579
+ await startThroughDesktop(this.desktopBusPath, threadId, message);
580
+ }
581
+ catch (error) {
582
+ if (!(error instanceof DesktopUnconfirmed))
583
+ throw error;
584
+ const deadline = Date.now() + this.confirmStartMs;
585
+ for (;;) {
586
+ await this.refresh(threadId);
587
+ // (Joined meanwhile: then the turn is reported by the server the thread is in.)
588
+ const watch = this.observed.get(threadId);
589
+ if (!watch || watch.running)
590
+ break;
591
+ if (Date.now() >= deadline)
592
+ throw new Error(`${error.message}, and no turn began`);
593
+ await new Promise((resolve) => setTimeout(resolve, Math.min(500, this.observeIntervalMs)));
594
+ }
595
+ }
596
+ void this.refresh(threadId);
597
+ }
598
+ desktopMessage(threadId, input, clientMessageId) {
599
+ return { input, text: textOf(input), clientMessageId, cwd: this.cwds.get(threadId) };
521
600
  }
522
601
  /**
523
602
  * Stops the turn a Codex that can't be joined is running. Only its own app