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.
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/host/src/drivers/codex/desktop-ipc.d.ts +24 -4
- package/dist/host/src/drivers/codex/desktop-ipc.js +81 -21
- package/dist/host/src/drivers/codex/desktop-ipc.js.map +1 -1
- package/dist/host/src/drivers/codex/driver.d.ts +16 -2
- package/dist/host/src/drivers/codex/driver.js +89 -10
- package/dist/host/src/drivers/codex/driver.js.map +1 -1
- package/dist/host/src/drivers/types.d.ts +2 -2
- package/dist/host/src/hub.js +2 -2
- package/dist/host/src/hub.js.map +1 -1
- package/package.json +3 -3
|
@@ -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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
22
|
-
const
|
|
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
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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
|
-
|
|
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
|
|
47
|
-
const registered = await bus.request("initialize", { clientType: "linkshell" }, 0);
|
|
48
|
-
bus.clientId = String(registered.clientId ?? "");
|
|
49
|
-
await bus.request("thread-
|
|
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
|
|
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
|
|
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: (
|
|
172
|
+
resolve: (response) => {
|
|
113
173
|
clearTimeout(timer);
|
|
114
|
-
resolve(
|
|
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":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAe,MAAM,UAAU,CAAC;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,2EAA2E;AAC3E,4EAA4E;AAC5E,yEAAyE;AACzE,uEAAuE;AACvE,0EAA0E;AAC1E,
|
|
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
|
-
/**
|
|
144
|
-
|
|
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
|
-
|
|
29
|
-
const
|
|
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
|
|
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
|
-
/**
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
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
|