codexteer 0.17.0 → 0.18.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.
Files changed (82) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/README.md +127 -14
  3. package/assets/remote-device-key.swift +94 -0
  4. package/bin/codexteer-update.mjs +3 -0
  5. package/bin/codexteer.mjs +6 -2
  6. package/docs/cloud.md +113 -0
  7. package/docs/desktop-cloud.md +36 -0
  8. package/docs/distribution.md +11 -19
  9. package/docs/orchestration.md +120 -0
  10. package/docs/remote-cli-validation.json +139 -0
  11. package/docs/remote-control.md +65 -0
  12. package/docs/remote-device-enrollment.md +91 -0
  13. package/docs/remote-observation.md +57 -0
  14. package/docs/remote-operations.md +89 -0
  15. package/docs/thread-create.md +20 -45
  16. package/package.json +11 -2
  17. package/scripts/update-desktop.applescript +51 -0
  18. package/src/agent-launch.mjs +27 -0
  19. package/src/app-server.mjs +34 -10
  20. package/src/checkpoint.mjs +52 -16
  21. package/src/cli.mjs +201 -23
  22. package/src/cloud-api.mjs +118 -0
  23. package/src/cloud-cli.mjs +319 -0
  24. package/src/cloud-connection.mjs +64 -0
  25. package/src/cloud-id.mjs +16 -0
  26. package/src/cloud-observe.mjs +178 -0
  27. package/src/cloud-send.mjs +33 -0
  28. package/src/cloud.mjs +172 -0
  29. package/src/connection.mjs +12 -1
  30. package/src/creation-verification.mjs +30 -10
  31. package/src/desktop-cloud-api.mjs +119 -0
  32. package/src/desktop-cloud-id.mjs +18 -0
  33. package/src/desktop-cloud-project.mjs +148 -0
  34. package/src/desktop-cloud.mjs +98 -0
  35. package/src/desktop.mjs +3 -1
  36. package/src/directive.mjs +5 -2
  37. package/src/distribution.mjs +2 -2
  38. package/src/evidence.mjs +3 -0
  39. package/src/findings.mjs +7 -7
  40. package/src/help.mjs +140 -28
  41. package/src/journal.mjs +37 -9
  42. package/src/launcher.mjs +16 -9
  43. package/src/monitor.mjs +18 -6
  44. package/src/native-command.mjs +10 -2
  45. package/src/native-rpc.mjs +15 -2
  46. package/src/observe.mjs +21 -7
  47. package/src/orchestrate-prompt.mjs +96 -0
  48. package/src/orchestrate.mjs +29 -0
  49. package/src/paged-read.mjs +7 -1
  50. package/src/projects.mjs +27 -2
  51. package/src/prompt-command.mjs +25 -0
  52. package/src/prompt.mjs +25 -8
  53. package/src/remote-client-info.mjs +16 -0
  54. package/src/remote-control-auth.mjs +109 -0
  55. package/src/remote-control-socket.mjs +137 -0
  56. package/src/remote-control.mjs +134 -0
  57. package/src/remote-device-key.mjs +41 -0
  58. package/src/remote-device-login.mjs +76 -0
  59. package/src/remote-device-protocol.mjs +72 -0
  60. package/src/remote-device-store.mjs +71 -0
  61. package/src/remote-device-worker.mjs +48 -0
  62. package/src/remote-device.mjs +202 -0
  63. package/src/remote-io.mjs +163 -0
  64. package/src/remote-key-access.mjs +76 -0
  65. package/src/remote-key-worker.mjs +38 -0
  66. package/src/remote-keychain.mjs +72 -0
  67. package/src/remote-management.mjs +234 -0
  68. package/src/remote.mjs +132 -0
  69. package/src/resource.mjs +8 -6
  70. package/src/runner.mjs +6 -1
  71. package/src/standalone-create.mjs +4 -3
  72. package/src/store.mjs +6 -3
  73. package/src/supervise.mjs +25 -53
  74. package/src/supervision.mjs +41 -19
  75. package/src/target.mjs +25 -0
  76. package/src/task-id.mjs +23 -0
  77. package/src/thread-create.mjs +4 -3
  78. package/src/thread-id.mjs +21 -6
  79. package/src/thread-verify.mjs +108 -0
  80. package/src/turn-repair.mjs +228 -0
  81. package/src/update.mjs +158 -0
  82. package/docs/thread-create-native-investigation.md +0 -136
@@ -0,0 +1,319 @@
1
+ import { desktopCloudProjectDoctor, assignDesktopCloudProject, desktopCloudProjectReceipt } from "./desktop-cloud-project.mjs";
2
+ import { listProjects as listDesktopProjects } from "./projects.mjs";
3
+ import { isDesktopCloudTask, desktopCloudId, desktopCloudLink } from "./desktop-cloud-id.mjs";
4
+ import { desktopCloudConnection, desktopCloudEnvironments } from "./desktop-cloud-api.mjs";
5
+ import { createDesktopCloudTask, listDesktopCloudTasks, desktopCloudReceipt } from "./desktop-cloud.mjs";
6
+ import { isCloudTask } from "./task-id.mjs";
7
+ import { cloudTaskId } from "./cloud-id.mjs";
8
+ import { cloudConnection } from "./cloud-connection.mjs";
9
+ import { observeThread, printObservation } from "./observe.mjs";
10
+ import { monitorCommand } from "./monitor.mjs";
11
+ import { notificationOptions } from "./notify.mjs";
12
+ import { prepareSupervisorPrompt, superviseAgent } from "./supervise.mjs";
13
+ import { supervisorId, registerSupervisor, getSupervisor, assertSupervisor, controlSupervisor, supervisorStatus, listSupervisors } from "./supervision.mjs";
14
+ import { createFinding, findingStatus, listFindings, resolveFinding, dismissFinding, reopenFinding, evaluateFinding, findingStats } from "./findings.mjs";
15
+ import { sendTrackedMessage, listMessages, getMessage, messageSummary, reconcileMessages, markMessage, listInstructions } from "./journal.mjs";
16
+ import { createCloudTask, cloudTaskDiff, listCloudTasks, cloudCreationReceipt } from "./cloud.mjs";
17
+ import { cloudSendReceipt, reconcileCloudSend } from "./cloud-observe.mjs";
18
+ import { cloudEnvironments } from "./cloud-api.mjs";
19
+ import { readFileSync } from "node:fs";
20
+ import { spawnSync } from "node:child_process";
21
+ import { playSendSound } from "./sound.mjs";
22
+ import { VERSION } from "./version.mjs";
23
+ import { createCheckpoint, listCheckpoints, getCheckpoint, checkpointSummary, verifyCheckpoint, runCheckpoint, attachArtifacts } from "./checkpoint.mjs";
24
+ import { acquireResource, resourceStatus, renewResource, releaseResource, listResources, runWithResource } from "./resource.mjs";
25
+
26
+ const failure = (message, code = "CLOUD_OPTIONS_INVALID") => Object.assign(new Error(message), { code, execution_environment: "cloud" });
27
+ const values = new Set("--environment --connection --supervisor --require-node-version --host --remote-transport --context-thread --thread --backend --source --kind --evidence --based-on --finding --expires-at --supersedes --checkpoint --limit --max-chars --since --poll-ms --timeout-ms --until --notify --settle-ms --max-hold-ms --owner --key --condition --note --rating --reason --root --path --exclude --command --cwd --artifact --status --by --token --ttl-ms --agent --project --env --branch --title --model --thinking --cursor --attempt".split(" "));
28
+ function option(args, name, fallback) {
29
+ const index = args.indexOf(name), stop = args.indexOf("--");
30
+ if (index < 0 || (stop >= 0 && index > stop)) return fallback;
31
+ const value = args[index + 1]; if (value == null || value.startsWith("--")) throw failure(`${name} requires a value.`);
32
+ args.splice(index, 2); return value;
33
+ }
34
+ function flag(args, name) {
35
+ const index = args.indexOf(name), stop = args.indexOf("--");
36
+ if (index < 0 || (stop >= 0 && index > stop)) return false;
37
+ args.splice(index, 1); return true;
38
+ }
39
+ const repeated = (args, name) => { const result = []; let value; while ((value = option(args, name)) !== undefined) result.push(value); return result; };
40
+ const message = args => { if (args[0] === "--") args.shift(); return args.length === 1 && args[0] === "-" ? readFileSync(0, "utf8") : args.join(" "); };
41
+ function exact(args, count) { if (args.length !== count || args.some(arg => arg.startsWith("--"))) throw failure("Unexpected arguments. See the command's help."); }
42
+
43
+ function selectedCloudInvocation(argv) {
44
+ const end = argv.indexOf("--"), own = end < 0 ? argv : argv.slice(0, end);
45
+ const connection = own.indexOf("--connection");
46
+ const positional = [];
47
+ for (let i = 0; i < own.length; i++) {
48
+ if (values.has(own[i])) { i++; continue; }
49
+ if (!own[i].startsWith("--")) positional.push(own[i]);
50
+ }
51
+ const [command, action] = positional;
52
+ if (["help", "--help", "--version"].includes(command) || own.includes("--help") || own.includes("--version")) return false;
53
+ if (connection >= 0 && ["cloud", "desktop-cloud"].includes(own[connection + 1])) return own[connection + 1];
54
+ const thread = own.indexOf("--thread");
55
+ if (["doctor", "resource"].includes(command) && thread >= 0) return isDesktopCloudTask(own[thread + 1]) ? "desktop-cloud" : isCloudTask(own[thread + 1]) ? "cloud" : false;
56
+ if (command === "thread" && action === "create") return false;
57
+ const nested = ["history", "instructions", "findings", "checkpoint", "thread"].includes(command)
58
+ || (command === "supervise" && ["prompt", "register", "status", "pause", "resume", "stop", "list"].includes(action));
59
+ const target = isCloudTask(command) || isDesktopCloudTask(command) ? command : positional[nested ? 2 : 1];
60
+ return isDesktopCloudTask(target) ? "desktop-cloud" : isCloudTask(target) ? "cloud" : false;
61
+ }
62
+
63
+ export const cloudInvocation = argv => !!selectedCloudInvocation(argv);
64
+
65
+ // Only parsing/routing is backend-specific. Supervision, monitoring, directives,
66
+ // findings, journaling and saved-agent launch use the same local implementation.
67
+ export async function mainCloud(argv) {
68
+ const args = [...argv], json = flag(args, "--json"), connection = option(args, "--connection", selectedCloudInvocation(argv) || "cloud");
69
+ const desktopCloud = connection === "desktop-cloud";
70
+ const targetId = desktopCloud ? desktopCloudId : cloudTaskId;
71
+ if (!["cloud", "desktop-cloud"].includes(connection)) throw failure("A cloud task requires the cloud connection.", "CLOUD_TARGET_CONFLICT");
72
+ const own = args.slice(0, args.includes("--") ? args.indexOf("--") : args.length);
73
+ if (own.some(value => ["--host", "--remote-transport", "--context-thread"].includes(value))) throw failure("Desktop host routes cannot select a cloud task.", "CLOUD_TARGET_CONFLICT");
74
+ const requiredNode = option(args, "--require-node-version");
75
+ if (requiredNode !== undefined && requiredNode !== process.version) throw failure("The saved supervisor requires its original Node version. Regenerate its prompt.", "NODE_VERSION_MISMATCH");
76
+ const session = option(args, "--supervisor"), supervisor = session === undefined ? undefined : supervisorId(session);
77
+ let command = args.shift();
78
+ if (isCloudTask(command) || isDesktopCloudTask(command)) { args.unshift(command); command = "send"; }
79
+ const output = (name, data) => {
80
+ if (json) console.log(JSON.stringify({ ok: true, command: name, data }));
81
+ else console.log(JSON.stringify(data, null, 2));
82
+ return data;
83
+ };
84
+ const bound = async (target, { allowUnregistered = false } = {}) => {
85
+ const id = targetId(target);
86
+ if (supervisor) {
87
+ const record = allowUnregistered ? await getSupervisor(id) : null;
88
+ if (!allowUnregistered || record?.session_id === supervisor) await assertSupervisor(id, supervisor, { connection });
89
+ else if (record && record.state !== "stopped") throw failure("Another supervisor owns this cloud task.", "SUPERVISOR_CONFLICT");
90
+ }
91
+ return id;
92
+ };
93
+ const dependencies = desktopCloud ? desktopCloudConnection() : cloudConnection();
94
+ if (desktopCloud && await desktopCloudCommands(command, args, { supervisor, output, bound, dependencies })) return;
95
+
96
+ if (["read", "status", "watch"].includes(command)) {
97
+ const options = { supervisor, connection, fullHistory: flag(args, "--full-history"), since: option(args, "--since"),
98
+ limit: Number(option(args, "--limit", "50")), maxChars: Number(option(args, "--max-chars", "2000")), includeOutput: flag(args, "--include-output") };
99
+ const stream = command === "watch" && flag(args, "--stream");
100
+ if (command === "watch") {
101
+ const until = option(args, "--until"), timeout = option(args, "--timeout-ms");
102
+ if (stream && (until !== undefined || timeout !== undefined)) throw failure("--stream runs until cancelled; do not combine it with --until or --timeout-ms.");
103
+ Object.assign(options, { watch: true, until: until ?? "change", timeoutMs: Number(timeout ?? "30000"), pollMs: Number(option(args, "--poll-ms", "5000")) });
104
+ const notify = option(args, "--notify", stream ? "digest" : "all"), settle = option(args, "--settle-ms"), maxHold = option(args, "--max-hold-ms");
105
+ if (!stream && (notify === "digest" || settle !== undefined || maxHold !== undefined)) throw failure("Digest options require --stream.");
106
+ Object.assign(options, notificationOptions({ notify, ...(settle === undefined ? {} : { settleMs: Number(settle) }), ...(maxHold === undefined ? {} : { maxHoldMs: Number(maxHold) }) }));
107
+ }
108
+ exact(args, 1); const id = await bound(args[0]);
109
+ if (stream) return monitorCommand(id, options, dependencies);
110
+ const data = await observeThread(id, options, dependencies); data.connection_mode = connection;
111
+ if (command === "status") delete data.events;
112
+ if (json) output(command, data); else printObservation(data, command === "status");
113
+ return;
114
+ }
115
+ if (command === "supervise") {
116
+ const action = args[0];
117
+ if (["register", "status", "list", "pause", "resume", "stop"].includes(action)) {
118
+ args.shift(); const owner = action === "register" ? option(args, "--owner", "external") : undefined;
119
+ exact(args, action === "list" ? 0 : 1);
120
+ const id = action === "list" ? null : targetId(args[0]); let data;
121
+ if (action === "register") {
122
+ if (!supervisor) throw failure("supervise register requires the generated --supervisor ID.");
123
+ data = await registerSupervisor(id, supervisor, { owner, connection });
124
+ } else {
125
+ if (supervisor && action !== "status") throw failure("A supervisor cannot change its own control state.", "SUPERVISOR_CONTROL_FORBIDDEN");
126
+ if (supervisor && id && (await getSupervisor(id))?.session_id !== supervisor) throw failure("This command belongs to another supervisor.", "SUPERVISOR_MISMATCH");
127
+ data = action === "list" ? (await listSupervisors()).filter(record => record.connection === connection) : action === "status" ? await supervisorStatus(id) : await controlSupervisor(id, action);
128
+ if (action === "status") { const pending = await listFindings(id, { limit: 20 }); data.unresolved_findings = { total: pending.total, has_more: pending.has_more, findings: pending.findings.map(f => ({ id: f.id, title: f.title, status: f.status, revision: f.revision })) }; }
129
+ }
130
+ output(`supervise.${action}`, data); return;
131
+ }
132
+ if (supervisor) throw failure("A supervisor cannot start a replacement session.", "SUPERVISOR_CONTROL_FORBIDDEN");
133
+ const promptOnly = action === "prompt"; if (promptOnly) args.shift();
134
+ const separator = args.indexOf("--"), ownArgs = separator < 0 ? args : args.slice(0, separator), forwarded = separator < 0 ? [] : args.slice(separator + 1);
135
+ const agent = option(ownArgs, "--agent", "claude"), usePathCli = flag(ownArgs, "--use-path-cli");
136
+ if (ownArgs.length < 1 || ownArgs.length > 2 || ownArgs.some(a => a.startsWith("--"))) throw failure("Expected: supervise [prompt] <TASK> [POLICY] [--agent claude|codex].");
137
+ const id = targetId(ownArgs[0]);
138
+ if (promptOnly) { const data = await prepareSupervisorPrompt(id, ownArgs[1], { agent, usePathCli, connection }); if (json) output("supervise.prompt", data); else console.log(data.prompt); }
139
+ else { if (json) throw failure("supervise inherits agent output and does not support --json."); process.exitCode = await superviseAgent(id, { policy: ownArgs[1], agent, usePathCli, agentArgs: forwarded, connection }); }
140
+ return;
141
+ }
142
+ if (command === "findings") {
143
+ const action = args.shift(), options = {};
144
+ const names = { key: "--key", title: "--title", condition: "--condition", basedOn: "--based-on", checkpoint: "--checkpoint", note: "--note", reason: "--reason", rating: "--rating" };
145
+ const allowed = { create: ["key", "title", "condition", "basedOn"], resolve: ["checkpoint", "note"], dismiss: ["reason"], reopen: ["reason", "condition", "basedOn"], evaluate: ["rating", "reason"] }[action] ?? [];
146
+ for (const key of allowed) { const value = option(args, names[key]); if (value !== undefined) options[key] = value; }
147
+ if (["create", "reopen"].includes(action)) { const evidence = repeated(args, "--evidence"); if (evidence.length) options.evidence = evidence; }
148
+ if (action === "list") { options.all = flag(args, "--all"); options.limit = Number(option(args, "--limit", "50")); }
149
+ const handlers = { create: createFinding, show: findingStatus, list: listFindings, resolve: resolveFinding, dismiss: dismissFinding, reopen: reopenFinding, evaluate: evaluateFinding, stats: findingStats };
150
+ if (!Object.hasOwn(handlers, action)) throw failure("Unknown findings operation.");
151
+ const single = ["create", "list", "stats"].includes(action); exact(args, single ? 1 : 2); const id = await bound(args[0]);
152
+ output(`findings.${action}`, single ? await handlers[action](id, options) : await handlers[action](id, args[1], options)); return;
153
+ }
154
+ if (command === "history") {
155
+ const action = args.shift(), attempt = option(args, "--attempt");
156
+ if (attempt !== undefined) {
157
+ if (desktopCloud) throw failure("Use the common history check with a message ID for Desktop Cloud.");
158
+ if (!["show", "check"].includes(action)) throw failure("--attempt requires history show or history check.");
159
+ exact(args, 1); const id = await bound(args[0]);
160
+ const settings = { taskId: id };
161
+ output(`history.${action}`, action === "show" ? await cloudSendReceipt(attempt, settings) : await reconcileCloudSend(attempt, settings)); return;
162
+ }
163
+ const includeText = flag(args, "--include-text"), pending = flag(args, "--pending");
164
+ const report = action === "mark" ? { status: option(args, "--status"), note: option(args, "--note"), evidence: repeated(args, "--evidence"), by: option(args, "--by", "local") } : {};
165
+ if (!args[0] || args.length > 2 || !["list", "show", "check", "mark"].includes(action) || (["show", "mark"].includes(action) && !args[1]) || (action === "list" && args[1])) throw failure("Invalid history arguments.");
166
+ const id = await bound(args[0]); const data = action === "list" ? await listMessages(id, { includeText, pending }) : action === "show" ? messageSummary(await getMessage(id, args[1]), includeText)
167
+ : action === "check" ? await reconcileMessages(id, args[1], { includeText }, dependencies) : await markMessage(id, args[1], report);
168
+ output(`history.${action}`, data); return;
169
+ }
170
+ if (command === "instructions") {
171
+ const action = args.shift();
172
+ if (action === "list") { const all = flag(args, "--all"); exact(args, 1); output("instructions.list", await listInstructions(await bound(args[0]), { all })); return; }
173
+ if (action !== "retract") throw failure("Expected instructions list|retract.");
174
+ const source = option(args, "--source", "external"), reason = option(args, "--reason"), basedOn = option(args, "--based-on"), dryRun = flag(args, "--dry-run"), newTurn = flag(args, "--new-turn");
175
+ exact(args, 2); if (!reason) throw failure("Retraction requires --reason.");
176
+ output("instructions.retract", await sendTrackedMessage(await bound(args[0]), reason, { source, retracts: args[1], basedOn, dryRun, newTurn, supervisor, connection })); return;
177
+ }
178
+ if (command === "checkpoint") {
179
+ const action = args.shift(), includeOutput = flag(args, "--include-output"); let data;
180
+ if (action === "capture") {
181
+ const paths = repeated(args, "--path"), excludes = repeated(args, "--exclude"); exact(args, 2);
182
+ data = await createCheckpoint(await bound(args[0]), args[1], { paths, excludes });
183
+ } else if (action === "run") {
184
+ const timeoutMs = Number(option(args, "--timeout-ms", "0"));
185
+ if (args[2] !== "--" || args.length < 4) throw failure("Expected checkpoint run <TASK> <ID> -- <COMMAND> [ARGS...].");
186
+ data = await runCheckpoint(await bound(args[0]), args[1], args.slice(3), { timeoutMs });
187
+ if (!includeOutput) delete data.output_tail; if (!data.valid) process.exitCode = 1;
188
+ } else if (action === "attach") { const refs = repeated(args, "--artifact"); exact(args, 2); data = await attachArtifacts(await bound(args[0]), args[1], refs); }
189
+ else if (action === "list") { exact(args, 1); data = await listCheckpoints(await bound(args[0])); }
190
+ else if (["show", "verify"].includes(action)) { exact(args, 2); const id = await bound(args[0]); data = action === "verify" ? await verifyCheckpoint(id, args[1]) : checkpointSummary(await getCheckpoint(id, args[1]), includeOutput); if (action === "verify" && !data.valid) process.exitCode = 1; }
191
+ else throw failure("Unknown checkpoint operation.");
192
+ output(`checkpoint.${action}`, Array.isArray(data) ? data.map(item => ({ ...item, execution_environment: "local" })) : { ...data, execution_environment: "local", verification_target: "explicit local input/artifact paths; cloud files are not mounted" }); return;
193
+ }
194
+ if (command === "resource") {
195
+ const action = args.shift(), threadInput = option(args, "--thread");
196
+ const threadId = threadInput === undefined ? undefined : await bound(threadInput);
197
+ if (supervisor && !threadId) throw failure("A supervisor resource command requires its --thread target.", "SUPERVISOR_CONTROL_FORBIDDEN");
198
+ let data;
199
+ if (["acquire", "run"].includes(action)) {
200
+ const settings = { owner: option(args, "--owner"), ttlMs: Number(option(args, "--ttl-ms", "600000")), reason: option(args, "--reason", ""), condition: option(args, "--condition", ""), threadId };
201
+ if (action === "run") {
202
+ const timeoutMs = Number(option(args, "--timeout-ms", "0")), includeOutput = flag(args, "--include-output");
203
+ if (args[1] !== "--" || args.length < 3) throw failure("Expected resource run <NAME> --owner NAME -- <COMMAND> [ARGS...].");
204
+ data = await runWithResource(args[0], args.slice(2), { ...settings, timeoutMs }); if (!includeOutput) delete data.output_tail; if (!data.valid) process.exitCode = 1;
205
+ } else { exact(args, 1); data = await acquireResource(args[0], settings); }
206
+ } else if (["renew", "release"].includes(action)) {
207
+ const token = option(args, "--token"), ttlMs = Number(option(args, "--ttl-ms", "600000")); exact(args, 1);
208
+ data = action === "renew" ? await renewResource(args[0], token, { ttlMs }) : await releaseResource(args[0], token);
209
+ } else if (action === "status") { exact(args, 1); data = await resourceStatus(args[0]); }
210
+ else if (action === "list") { exact(args, 0); data = await listResources(); }
211
+ else throw failure("Unknown resource operation.");
212
+ output(`resource.${action}`, Array.isArray(data) ? data.map(item => ({ ...item, execution_environment: "local" })) : { ...data, execution_environment: "local", scope: "local coordination and explicitly supplied local commands" }); return;
213
+ }
214
+ if (command === "threads") {
215
+ if (args.shift() !== "list") throw failure("Expected threads list.");
216
+ const limit = Number(option(args, "--limit", "20")), cursor = option(args, "--cursor"), environment = option(args, "--env"); exact(args, 0);
217
+ if (supervisor) throw failure("A bound supervisor cannot enumerate unrelated cloud tasks.", "SUPERVISOR_CONTROL_FORBIDDEN");
218
+ output("threads.list", await listCloudTasks({ limit, cursor, environment })); return;
219
+ }
220
+ if (command === "thread" && args[0] === "receipt") {
221
+ args.shift(); exact(args, 1);
222
+ if (supervisor) throw failure("A bound supervisor cannot inspect unrelated creation attempts.", "SUPERVISOR_CONTROL_FORBIDDEN");
223
+ output("thread.receipt", await cloudCreationReceipt(args[0])); return;
224
+ }
225
+ if (command === "thread" && args[0] === "create") {
226
+ args.shift(); if (supervisor) throw failure("A bound supervisor cannot create another task.", "SUPERVISOR_CONTROL_FORBIDDEN");
227
+ const project = option(args, "--project"), requestedBranch = option(args, "--branch"), dryRun = flag(args, "--dry-run");
228
+ if (project === undefined) throw failure("Cloud creation requires --project with an environment ID or exact name.");
229
+ const end = args.includes("--") ? args.indexOf("--") : args.length; if (args.slice(0, end).some(arg => arg.startsWith("--"))) throw failure("Unknown cloud creation option; explicit model/title/permission overrides are not implemented yet.");
230
+ const body = message(args); if (!body.trim()) throw failure("A non-empty initial instruction is required.");
231
+ const rows = (await cloudEnvironments()).environments, selector = project, matches = rows.filter(e => e.id === selector || e.name === selector);
232
+ if (matches.length !== 1) throw failure("The cloud environment selector must match one accessible ID or exact name.");
233
+ const selected = matches[0], branch = requestedBranch ?? (selected.repositories.length === 1 ? selected.repositories[0].default_branch : null);
234
+ if (!branch) throw failure("Specify --branch: this environment does not identify one repository default branch.");
235
+ const data = await createCloudTask(selected.id, branch, body, { dryRun });
236
+ output("thread.create", { ...data, thread_id: data.task_id ?? null, project_id: selected.id, project_name: selected.name, deep_link: data.url ?? null,
237
+ branch_source: requestedBranch === undefined ? "environment-default" : "explicit" }); return;
238
+ }
239
+ if (command === "projects") { exact(args, 1); if (args[0] !== "list") throw failure("Expected projects list."); if (supervisor) throw failure("A bound supervisor cannot enumerate unrelated cloud environments.", "SUPERVISOR_CONTROL_FORBIDDEN"); const data = await cloudEnvironments(); output("projects.list", { projects: data.environments.map(e => ({ ...e, project_id: e.id, execution_environment: "cloud" })) }); return; }
240
+ if (command === "doctor") {
241
+ const target = option(args, "--thread"), backend = option(args, "--backend", "app-server"); exact(args, 0);
242
+ if (backend !== "app-server") throw failure("Cloud cannot use Desktop UI.", "CLOUD_TARGET_CONFLICT");
243
+ if (target) {
244
+ const id = await bound(target, { allowUnregistered: true });
245
+ await observeThread(id, { connection, limit: 1 }, dependencies);
246
+ } else { if (supervisor) throw failure("A bound supervisor doctor requires its target task.", "SUPERVISOR_CONTROL_FORBIDDEN"); await cloudEnvironments(); }
247
+ output("doctor", { ready: true, backend: "codex-api", execution_environment: "cloud", version: VERSION,
248
+ codex_steer_compatibility: { status: "compatible" }, connection: { status: "connected", mode: "cloud" },
249
+ observation_compatibility: { status: target ? "verified" : "unverified" },
250
+ runtime_compatibility: { operations: Object.fromEntries(["read", "watch", "monitor", "history_check", "send", "send_new_turn"].map(name => [name, { status: "supported" }])) },
251
+ active_send_semantics: "interrupt-and-follow-up; same task, new turn", unverified_features: ["approval_roundtrip", "atomic_server_freshness"] }); return;
252
+ }
253
+ if (command === "thread" && args[0] === "resolve") { args.shift(); exact(args, 1); const id = targetId(args[0]); output("thread.resolve", { thread_id: id, task_id: id, deep_link: `https://chatgpt.com/codex/tasks/${id}`, execution_environment: "cloud" }); return; }
254
+ if (command === "open") {
255
+ exact(args, 1); const id = await bound(args[0]), url = `https://chatgpt.com/codex/tasks/${id}`;
256
+ if (process.platform !== "darwin") throw failure("Open this task URL in your browser; native browser launch is only implemented on macOS.", "CLOUD_CAPABILITY_UNSUPPORTED");
257
+ const result = spawnSync("/usr/bin/open", [url], { encoding: "utf8", timeout: 5000 }); if (result.status !== 0) throw failure("Could not open the cloud task in the browser.", "CLOUD_OPEN_FAILED");
258
+ output("open", { thread_id: id, deep_link: url, execution_environment: "cloud" }); return;
259
+ }
260
+ if (command === "diff") {
261
+ const attempt = option(args, "--attempt"), timeout = option(args, "--timeout-ms"); exact(args, 1);
262
+ const data = await cloudTaskDiff(await bound(args[0]), { ...(attempt === undefined ? {} : { attempt: Number(attempt) }), ...(timeout === undefined ? {} : { timeoutMs: Number(timeout) }) });
263
+ if (json) output("diff", data);
264
+ else if (data.diff === null) console.error("No server-recorded patch is available. This does not prove the cloud working copy has no changes; inspect read command evidence.");
265
+ else process.stdout.write(data.diff);
266
+ return;
267
+ }
268
+ if (command !== "send") throw failure("This cloud operation is not implemented yet. No Desktop fallback was attempted.", "CLOUD_CAPABILITY_UNSUPPORTED");
269
+ const dryRun = flag(args, "--dry-run"), newTurn = flag(args, "--new-turn"), backend = option(args, "--backend", "app-server");
270
+ const noSound = flag(args, "--no-sound"), explicitSound = flag(args, "--sound"); if (noSound && explicitSound) throw failure("--sound and --no-sound cannot be combined.");
271
+ if (backend !== "app-server") throw failure("Cloud cannot send through Desktop UI.", "CLOUD_TARGET_CONFLICT");
272
+ const options = { dryRun, newTurn, supervisor, connection, evidence: repeated(args, "--evidence") };
273
+ for (const [key, name] of Object.entries({ source: "--source", kind: "--kind", basedOn: "--based-on", supersedes: "--supersedes", expiresAt: "--expires-at", checkpoint: "--checkpoint", finding: "--finding" })) { const value = option(args, name); if (value !== undefined) options[key] = value; }
274
+ const id = targetId(args.shift()), end = args.includes("--") ? args.indexOf("--") : args.length;
275
+ if (args.slice(0, end).some(arg => arg.startsWith("--"))) throw failure("Unknown send option. Use -- before instruction text starting with --.");
276
+ const body = message(args); if (!body.trim()) throw failure("send requires a non-empty message.");
277
+ const receipt = await sendTrackedMessage(id, body, options);
278
+ receipt.sound = noSound ? { played: false, reason: "disabled" } : playSendSound(receipt);
279
+ output("send", receipt);
280
+ }
281
+
282
+ async function desktopCloudCommands(command, args, { supervisor, output, bound, dependencies }) {
283
+ if (!["projects", "environments", "threads", "thread", "doctor", "diff", "open"].includes(command)) return false;
284
+ if (supervisor && ["projects", "environments", "threads", "thread"].includes(command)) throw failure("A supervisor cannot enumerate or create other tasks.", "SUPERVISOR_CONTROL_FORBIDDEN");
285
+ if (["projects", "environments"].includes(command)) {
286
+ exact(args, 1); if (args[0] !== "list") throw failure("Expected list.");
287
+ if (command === "projects") { output("projects.list", { projects: (await listDesktopProjects()).flatMap(row => row.desktop_project_ids.map(id => ({ project_id: id, canonical_project_id: row.project_id, name: row.name, roots: row.roots }))), catalog_source: "desktop-app-server", assignment_requires_exact_desktop_executor: true }); return true; }
288
+ output("environments.list", await desktopCloudEnvironments()); return true;
289
+ }
290
+ if (command === "threads") {
291
+ if (args.shift() !== "list") throw failure("Expected threads list.");
292
+ const limit = Number(option(args, "--limit", "20")), cursor = option(args, "--cursor"); exact(args, 0);
293
+ output("threads.list", await listDesktopCloudTasks({ limit, cursor })); return true;
294
+ }
295
+ if (command === "thread") {
296
+ const action = args.shift();
297
+ if (action === "create") {
298
+ const environment = option(args, "--environment"), project = option(args, "--project"), title = option(args, "--title"), dryRun = flag(args, "--dry-run");
299
+ const end = args.includes("--") ? args.indexOf("--") : args.length;
300
+ if (args.slice(0, end).some(arg => arg.startsWith("--"))) throw failure("Unknown Desktop Cloud creation option.");
301
+ output("thread.create", await createDesktopCloudTask(environment, message(args), { project, title, dryRun })); return true;
302
+ }
303
+ if (action === "assign") { const project = option(args, "--project"); exact(args, 1); const data = await assignDesktopCloudProject(await bound(args[0]), project); output("thread.assign", data); if (data.verification_status !== "verified") process.exitCode = 1; return true; }
304
+ exact(args, 1);
305
+ if (action === "receipt") { let data; try { data = await desktopCloudReceipt(args[0]); } catch (error) { if (error.code !== "DESKTOP_CLOUD_CREATION_RECEIPT_NOT_FOUND") throw error; data = await desktopCloudProjectReceipt(args[0]); } output("thread.receipt", data); return true; }
306
+ if (action === "resolve") { const id = await bound(args[0]); output("thread.resolve", { thread_id: id, deep_link: desktopCloudLink(id) }); return true; }
307
+ throw failure("Expected thread create|assign|receipt|resolve.");
308
+ }
309
+ if (command === "doctor") {
310
+ const target = option(args, "--thread"), project = option(args, "--project"); exact(args, 0);
311
+ if (project !== undefined && !target) throw failure("--project diagnostics require --thread.");
312
+ const id = target ? await bound(target, { allowUnregistered: true }) : null;
313
+ if (id) await observeThread(id, { connection: "desktop-cloud", limit: 1 }, dependencies);
314
+ else { if (supervisor) throw failure("A supervisor doctor needs its target."); await desktopCloudEnvironments(); }
315
+ output("doctor", { ready: true, connection_mode: "desktop-cloud", observation: target ? "verified" : "unverified", project_membership: id ? await desktopCloudProjectDoctor(id, project) : { membership: { status: "unverified" }, assignment_api: { status: "unverified" } }, approval_roundtrip: "unverified", atomic_server_freshness: "unverified" }); return true;
316
+ }
317
+ if (command === "open") { exact(args, 1); output("open", { deep_link: desktopCloudLink(await bound(args[0])) }); return true; }
318
+ throw failure("Desktop Cloud diff retrieval is not verified. Inspect file-change events with read --include-output.", "CAPABILITY_UNVERIFIED");
319
+ }
@@ -0,0 +1,64 @@
1
+ import { cloudTaskId } from "./cloud-id.mjs";
2
+ import { readCloudTask } from "./cloud-observe.mjs";
3
+ import { codexHome } from "./runtime.mjs";
4
+
5
+ const status = value => ["in_progress", "pending", "running", "queued"].includes(value) ? "inProgress"
6
+ : value === "cancelled" ? "interrupted" : value === "failed" || value === "error" ? "failed" : value === "completed" ? "completed" : "unknown";
7
+
8
+ // Adapt public cloud history into the same observation model used by the local
9
+ // monitor, freshness checks and directive journal. Keep backend IDs unchanged.
10
+ export function cloudThread(snapshot) {
11
+ const taskId = cloudTaskId(snapshot.task_id);
12
+ const turns = snapshot.turns.map(turn => ({ id: turn.turn_id, status: turn.role === "user" ? "completed" : status(turn.status), items: [
13
+ ...turn.commands.map(command => ({ id: command.id, type: "commandExecution", command: command.command, cwd: null,
14
+ status: status(command.status === "inProgress" ? "in_progress" : command.status), exitCode: command.exit_code, aggregatedOutput: command.output ?? "" })),
15
+ ...(turn.progress ?? []).map((message, index) => ({ id: `${turn.turn_id}:progress:${message.id === undefined ? `index:${index}` : `id:${message.id}`}`, type: "agentMessage",
16
+ phase: "commentary", status: status(turn.status), text: message.text })),
17
+ ...turn.messages.map((message, index) => turn.role === "user"
18
+ ? { id: `${turn.turn_id}:message:${index}`, type: "userMessage", clientId: null, cloudParentTurnId: turn.parent_turn_id,
19
+ content: [{ type: "text", text: message.text }] }
20
+ : { id: `${turn.turn_id}:message:${index}`, type: "agentMessage", phase: "final_answer", status: status(turn.status), text: message.text }),
21
+ ] }));
22
+ const active = turns.filter(turn => turn.status === "inProgress");
23
+ return { id: taskId, name: snapshot.title ?? null, cwd: null, historyMode: "aggregate", canAcceptDirectInput: true,
24
+ status: { type: active.length ? "active" : status(snapshot.status) === "unknown" ? "systemError" : "idle", activeFlags: [] },
25
+ turns, cloud_snapshot: snapshot };
26
+ }
27
+
28
+ export class CloudReadClient {
29
+ constructor({ read = readCloudTask, timeoutMs = 30000, signal } = {}) {
30
+ this.read = read; this.timeoutMs = timeoutMs; this.controller = new AbortController();
31
+ this.cancel = () => this.close(); signal?.addEventListener("abort", this.cancel, { once: true });
32
+ this.signal = signal; if (signal?.aborted) this.close();
33
+ }
34
+ async request(method, params = {}, options = {}) {
35
+ if (this.controller.signal.aborted) throw Object.assign(new Error("Cloud observation was cancelled."), { code: "CANCELLED" });
36
+ if (method !== "thread/read" || options.mutation) throw Object.assign(new Error("Unsupported cloud observation method."), { code: "CAPABILITY_UNSUPPORTED" });
37
+ const id = cloudTaskId(params.threadId);
38
+ try {
39
+ if (!params.includeTurns || this.cached?.task_id !== id) this.cached = await this.read(id, { timeoutMs: this.timeoutMs, signal: this.controller.signal });
40
+ const thread = cloudThread(this.cached);
41
+ if (params.includeTurns) this.cached = null;
42
+ return { thread };
43
+ } catch (error) {
44
+ const mapped = { CLOUD_CONNECTION_FAILED: "CONNECTION_FAILED", CLOUD_RATE_LIMITED: "CONNECTION_FAILED", CLOUD_TIMEOUT: "TIMEOUT", CLOUD_AUTH_TIMEOUT: "TIMEOUT", CLOUD_STATE_CHANGED: "OBSERVATION_CHANGED", CLOUD_CANCELLED: "CANCELLED" }[error.code];
45
+ if (mapped) throw Object.assign(new Error(error.message), { code: mapped, cause_code: error.code,
46
+ ...(error.retry_after_ms === undefined ? {} : { retry_after_ms: error.retry_after_ms }) });
47
+ throw error;
48
+ }
49
+ }
50
+ close() { this.controller.abort(); this.signal?.removeEventListener("abort", this.cancel); this.cached = null; }
51
+ }
52
+
53
+ export function cloudConnection(dependencies = {}) {
54
+ return {
55
+ async discover(home = codexHome()) {
56
+ return { paths: { home, socket: "codex-cloud" }, state: { codex_home: home, cloud_connection: true,
57
+ codex_steer_protocol: 1, codex_steer_capabilities: { thread_read: [1], history_pagination: [], turn_steer: [], turn_start: [], desktop_subscribe: [] } } };
58
+ },
59
+ async connect(endpoint, options = {}) {
60
+ if (endpoint !== "codex-cloud") throw Object.assign(new Error("Invalid cloud observation endpoint."), { code: "CLOUD_TARGET_CONFLICT" });
61
+ return new CloudReadClient({ ...dependencies, ...options });
62
+ },
63
+ };
64
+ }
@@ -0,0 +1,16 @@
1
+ const pattern = /^task_[A-Za-z0-9][A-Za-z0-9_-]{0,199}$/;
2
+ const failure = () => Object.assign(new Error("Invalid Codex Cloud task. Use a task ID or an official chatgpt.com/codex/tasks link."), { code: "CLOUD_OPTIONS_INVALID" });
3
+
4
+ export function isCloudTask(value) {
5
+ return typeof value === "string" && (value.startsWith("task_") || /^https:\/\/chatgpt\.com\/codex\/tasks\//.test(value));
6
+ }
7
+
8
+ export function cloudTaskId(value) {
9
+ if (typeof value !== "string") throw failure();
10
+ if (pattern.test(value)) return value;
11
+ try {
12
+ const url = new URL(value), id = url.pathname.slice("/codex/tasks/".length);
13
+ if (url.origin === "https://chatgpt.com" && !url.username && !url.password && url.pathname.startsWith("/codex/tasks/") && pattern.test(id)) return id;
14
+ } catch {}
15
+ throw failure();
16
+ }
@@ -0,0 +1,178 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { cloudApi, cloudCredentials } from "./cloud-api.mjs";
3
+ import { cloudTaskId } from "./cloud-id.mjs";
4
+ import { readRecord, storePath, withStoreLock, writeRecord } from "./store.mjs";
5
+
6
+ const failure = (code, message) => Object.assign(new Error(message), { code, execution_environment: "cloud" });
7
+ const digest = value => createHash("sha256").update(JSON.stringify(value)).digest("hex");
8
+ const validTurn = (task, value) => typeof value === "string" && value.startsWith(`${task}~`) && /^(?:usertrn|assttrn)_[A-Za-z0-9_-]{1,200}$/.test(value.slice(task.length + 1));
9
+ export const cloudUserRevision = snapshot => digest(snapshot.turns.filter(turn => turn.role === "user").map(turn => ({ id: turn.turn_id, parent: turn.parent_turn_id, messages: turn.messages })));
10
+
11
+ function texts(items) {
12
+ return (Array.isArray(items) ? items : []).filter(item => item?.type === "message").map(item => ({
13
+ role: typeof item.role === "string" ? item.role : null,
14
+ text: (Array.isArray(item.content) ? item.content : []).flatMap(part => typeof part === "string" ? [part]
15
+ : part?.content_type === "text" && typeof part.text === "string" ? [part.text] : []).join("\n"),
16
+ })).filter(item => item.text);
17
+ }
18
+
19
+ function publicProgress(items) {
20
+ return (Array.isArray(items) ? items : []).flatMap(item => {
21
+ const role = item?.role ?? item?.author?.role;
22
+ if (role !== "assistant" || ![undefined, null, "commentary"].includes(item.channel)
23
+ || item.type === "reasoning" || (item.recipient != null && item.recipient !== "all") || item.metadata?.is_visually_hidden_from_conversation === true
24
+ || (item.metadata?.reasoning_status != null && item.metadata?.is_thinking_preamble_message !== true)) return [];
25
+ let text;
26
+ if (Array.isArray(item.content)) text = texts([{ ...item, type: "message" }])[0]?.text;
27
+ else if (["text", "multimodal_text"].includes(item.content?.content_type) && Array.isArray(item.content.parts)) text = item.content.parts.filter(part => typeof part === "string").join("");
28
+ if (!text) return [];
29
+ return [{ ...(typeof item.id === "string" && item.id.length <= 512 && !/[\x00-\x1f]/u.test(item.id) ? { id: item.id } : {}), role: "assistant", text }];
30
+ });
31
+ }
32
+
33
+ function progress(turn) {
34
+ const items = publicProgress(turn.worklog?.messages), seen = new Set(items.flatMap(item => item.id === undefined ? [] : [item.id]));
35
+ const latest = new Map();
36
+ for (const event of [...(turn.thread_events?.events ?? []), ...(turn.app_server_events?.events ?? [])]) {
37
+ const item = event?.params?.item;
38
+ if (!["item/started", "item/completed"].includes(event?.method) || item?.type !== "agentMessage" || item.phase !== "commentary"
39
+ || typeof item.id !== "string" || !item.id || item.id.length > 512 || /[\x00-\x1f]/u.test(item.id) || typeof item.text !== "string" || !item.text) continue;
40
+ latest.set(item.id, { id: item.id, role: "assistant", text: item.text });
41
+ }
42
+ return [...items, ...[...latest.values()].filter(item => !seen.has(item.id))];
43
+ }
44
+
45
+ function commands(turn) {
46
+ const latest = new Map();
47
+ for (const event of [...(turn.thread_events?.events ?? []), ...(turn.app_server_events?.events ?? [])]) {
48
+ const item = event?.params?.item;
49
+ if (!["item/started", "item/completed"].includes(event?.method) || item?.type !== "commandExecution" || typeof item.id !== "string") continue;
50
+ latest.set(item.id, { id: item.id, status: typeof item.status === "string" ? item.status : "unknown",
51
+ command: typeof item.command === "string" ? item.command : null, output: typeof item.aggregatedOutput === "string" ? item.aggregatedOutput : null,
52
+ exit_code: Number.isInteger(item.exitCode) ? item.exitCode : null });
53
+ }
54
+ return [...latest.values()];
55
+ }
56
+
57
+ export async function readCloudTask(target, settings = {}, { request = cloudApi } = {}) {
58
+ const taskId = cloudTaskId(target);
59
+ if (settings.timeoutMs !== undefined && (!Number.isSafeInteger(settings.timeoutMs) || settings.timeoutMs < 1000 || settings.timeoutMs > 600000)) throw failure("CLOUD_OPTIONS_INVALID", "--timeout-ms must be 1000..600000.");
60
+ const timeoutMs = settings.timeoutMs ?? 120000, deadline = Date.now() + timeoutMs;
61
+ const details = await request(`tasks/${taskId}`, { timeoutMs, ...(settings.signal ? { signal: settings.signal } : {}) });
62
+ if (deadline - Date.now() < 1) throw failure("CLOUD_TIMEOUT", "Cloud history deadline expired.");
63
+ const history = await request(`tasks/${taskId}/turns`, { timeoutMs: deadline - Date.now(), ...(settings.signal ? { signal: settings.signal } : {}) });
64
+ const current = details.current_assistant_turn;
65
+ if (details.task?.id !== taskId || current?.role !== "assistant" || !validTurn(taskId, current?.id) || !current.id.startsWith(`${taskId}~assttrn_`) || !history.turn_mapping || typeof history.turn_mapping !== "object" || Array.isArray(history.turn_mapping)) {
66
+ throw failure("CLOUD_PROTOCOL_UNVERIFIED", "Cloud did not confirm the requested task and its turn history.");
67
+ }
68
+ if (details.task.current_turn_id !== history.current_turn_id || !history.turn_mapping[current.id]) throw failure("CLOUD_STATE_CHANGED", "Cloud changed while reading. Read again before sending.");
69
+ const nodes = Object.entries(history.turn_mapping);
70
+ if (nodes.length > 10000 || nodes.some(([id, node]) => !validTurn(taskId, id) || node?.turn?.id !== id || (node.parent != null && !validTurn(taskId, node.parent)))) {
71
+ throw failure("CLOUD_PROTOCOL_UNVERIFIED", "Cloud history contains an unrecognized turn identity.");
72
+ }
73
+ const turns = nodes.map(([id, node]) => ({ turn_id: id, parent_turn_id: node.parent ?? null,
74
+ role: node.turn.role, status: node.turn.turn_status ?? null, created_at: node.turn.created_at ?? null,
75
+ messages: texts(node.turn.input_items ?? node.turn.output_items),
76
+ progress: progress(node.turn),
77
+ commands: commands(node.turn),
78
+ })).sort((a, b) => (a.created_at ?? 0) - (b.created_at ?? 0) || a.turn_id.localeCompare(b.turn_id));
79
+ const snapshot = { task_id: taskId, turn_id: current.id, current_turn_id: history.current_turn_id, status: current.turn_status ?? "unknown",
80
+ title: typeof details.task.title === "string" && details.task.title.length <= 2000 ? details.task.title : null, turns };
81
+ return { backend: "codex-api", execution_environment: "cloud", ...snapshot,
82
+ history_scope: "all-returned-turns", url: `https://chatgpt.com/codex/tasks/${taskId}` };
83
+ }
84
+
85
+ export async function sendCloudTask(target, expectedTurn, prompt, settings = {}, dependencies = {}) {
86
+ const taskId = cloudTaskId(target);
87
+ if (settings.timeoutMs !== undefined && (!Number.isSafeInteger(settings.timeoutMs) || settings.timeoutMs < 1000 || settings.timeoutMs > 600000)) throw failure("CLOUD_OPTIONS_INVALID", "--timeout-ms must be 1000..600000.");
88
+ if (!validTurn(taskId, expectedTurn) || typeof settings.expectedUserRevision !== "string" || !/^[a-f0-9]{64}$/.test(settings.expectedUserRevision) || !["steer", "new-turn"].includes(settings.deliveryMode)) throw failure("CLOUD_OPTIONS_INVALID", "Cloud delivery requires the exact parent turn, user-input revision and delivery mode.");
89
+ if (typeof prompt !== "string" || !prompt.trim() || prompt.includes("\0") || Buffer.byteLength(prompt) > 1024 * 1024) throw failure("CLOUD_OPTIONS_INVALID", "A non-empty instruction of at most 1 MiB is required.");
90
+ const request = dependencies.request ?? cloudApi, read = dependencies.read ?? readCloudTask;
91
+ const write = dependencies.write ?? writeRecord, load = dependencies.load ?? readRecord, lock = dependencies.lock ?? withStoreLock;
92
+ // Pin one official account for the read/compare/send sequence. Never refresh
93
+ // or automatically retry an unknown submission against a different account.
94
+ const auth = dependencies.request ? null : await cloudCredentials();
95
+ const api = (route, options = {}) => request(route, { ...options, ...(auth ? { credentials: async () => auth } : {}) });
96
+ try { return await lock(`cloud-send-${taskId}`, async () => {
97
+ const observation = await read(taskId, { timeoutMs: settings.timeoutMs }, { request: api });
98
+ if (observation.turn_id !== expectedTurn || cloudUserRevision(observation) !== settings.expectedUserRevision) throw failure("CLOUD_STATE_CHANGED", "Cloud task or user input changed after observation. Read again; no message was sent.");
99
+ const active = ["in_progress", "pending", "queued", "running"].includes(observation.status);
100
+ if (settings.deliveryMode === "steer" ? !active : !["completed", "failed", "cancelled"].includes(observation.status)) {
101
+ throw failure(active ? "CLOUD_TASK_BUSY" : "CLOUD_TASK_IDLE", settings.deliveryMode === "steer"
102
+ ? "The task has no active turn. Use --new-turn to continue an idle cloud task."
103
+ : "A new turn requires a finished cloud turn. Nothing was sent.");
104
+ }
105
+ const previous = await load("cloud-send-heads", taskId);
106
+ const safelyUnsent = previous?.submission_status === "not_sent" && previous.dispatch_started === false;
107
+ if (previous && !safelyUnsent && (previous.submission_status !== "accepted" || previous.parent_turn_id === expectedTurn)) throw Object.assign(failure("CLOUD_DUPLICATE_SEND", "An accepted or uncertain send needs inspection before another send. Use history check <TASK> --attempt <ATTEMPT_ID>; do not resend."), { existing_attempt_id: previous.attempt_id, existing_receipt_path: previous.receipt_path });
108
+ const attemptId = randomUUID();
109
+ const receipt = { execution_environment: "cloud", backend: "codex-api", task_id: taskId, parent_turn_id: expectedTurn,
110
+ attempt_id: attemptId, receipt_path: storePath("cloud-messages", attemptId), user_revision: settings.expectedUserRevision,
111
+ prompt_sha256: createHash("sha256").update(prompt).digest("hex"), prompt_hash_encoding: "utf8", message_characters: [...prompt].length,
112
+ delivery_action: active ? "interrupt-and-follow-up" : "new-turn", parent_status: observation.status, submission_status: "not_sent", dry_run: !!settings.dryRun };
113
+ await settings.beforeSend?.(observation);
114
+ if (settings.dryRun) return receipt;
115
+ const dispatched = { ...receipt, submission_status: "dispatching" };
116
+ try {
117
+ await write("cloud-messages", attemptId, receipt);
118
+ // Finish receipt persistence before reserving the head. There must be no
119
+ // fallible persistence between the reservation and the single POST.
120
+ await write("cloud-messages", attemptId, dispatched);
121
+ await write("cloud-send-heads", taskId, dispatched);
122
+ } catch {
123
+ const unsent = { ...receipt, dispatch_started: false, error_code: "CLOUD_PERSISTENCE_FAILED" };
124
+ try { await write("cloud-send-heads", taskId, previous ?? unsent); }
125
+ catch { unsent.reservation_cleanup_required = true; unsent.receipt_update_required = true; }
126
+ try { await write("cloud-messages", attemptId, unsent); }
127
+ catch { unsent.receipt_update_required = true; }
128
+ throw Object.assign(failure("CLOUD_PERSISTENCE_FAILED", "Cloud send records could not be prepared. No request was sent. Inspect the receipt and reservation before an explicit retry."), unsent, { uncertain: false });
129
+ }
130
+ let response;
131
+ try {
132
+ response = await api("tasks", { timeoutMs: settings.timeoutMs, body: {
133
+ follow_up: { task_id: taskId, turn_id: expectedTurn, environment_mode: "code" },
134
+ input_items: [{ type: "message", role: "user", content: [{ content_type: "text", text: prompt }] }],
135
+ } });
136
+ if ((response.task?.id ?? response.id) !== taskId) throw failure("CLOUD_PROTOCOL_UNVERIFIED", "Cloud did not confirm the same task after follow-up.");
137
+ receipt.submission_status = "accepted";
138
+ } catch (error) {
139
+ receipt.submission_status = "unknown";
140
+ receipt.error_code = error.code ?? "CLOUD_SEND_FAILED";
141
+ try { await write("cloud-messages", attemptId, receipt); } catch { receipt.receipt_update_required = true; }
142
+ throw Object.assign(failure(receipt.error_code, "Cloud follow-up outcome is unknown. Read this task and inspect the receipt before retrying; no automatic retry was made."), receipt, { uncertain: true });
143
+ }
144
+ try { await write("cloud-messages", attemptId, receipt); await write("cloud-send-heads", taskId, receipt); } catch { receipt.receipt_update_required = true; }
145
+ return receipt;
146
+ }); } finally { if (auth) auth.token = ""; }
147
+ }
148
+
149
+ export async function cloudSendReceipt(attemptId, { load = readRecord, taskId } = {}) {
150
+ if (typeof attemptId !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(attemptId)) throw failure("CLOUD_OPTIONS_INVALID", "A cloud send attempt UUID is required.");
151
+ const receipt = await load("cloud-messages", attemptId);
152
+ if (!receipt) throw failure("CLOUD_RECEIPT_NOT_FOUND", "Cloud send receipt was not found.");
153
+ if (taskId !== undefined && cloudTaskId(receipt.task_id) !== cloudTaskId(taskId)) throw failure("CLOUD_TARGET_CONFLICT", "The send attempt belongs to another cloud task.");
154
+ return receipt;
155
+ }
156
+
157
+ export async function reconcileCloudSend(attemptId, { read = readCloudTask, load = readRecord, write = writeRecord, lock = withStoreLock, taskId: expectedTask, expectedPromptSha256, expectedParentTurnId } = {}) {
158
+ const receipt = await cloudSendReceipt(attemptId, { load, taskId: expectedTask });
159
+ const taskId = cloudTaskId(receipt.task_id);
160
+ if ((expectedPromptSha256 !== undefined && receipt.prompt_sha256 !== expectedPromptSha256)
161
+ || (expectedParentTurnId !== undefined && receipt.parent_turn_id !== expectedParentTurnId)) {
162
+ return { ...receipt, delivery_verification: "conflict", retry_safe: false };
163
+ }
164
+ return lock(`cloud-send-${taskId}`, async () => {
165
+ if (receipt.submission_status === "not_sent" && receipt.dispatch_started === false) {
166
+ if ((await load("cloud-send-heads", taskId))?.attempt_id === attemptId) await write("cloud-send-heads", taskId, receipt);
167
+ return { ...receipt, delivery_verification: "not_sent", retry_safe: true, reservation_cleanup_required: false };
168
+ }
169
+ const snapshot = await read(taskId);
170
+ const matches = snapshot.turns.filter(turn => turn.role === "user" && turn.parent_turn_id === receipt.parent_turn_id
171
+ && turn.messages.some(message => createHash("sha256").update(message.text).digest("hex") === receipt.prompt_sha256));
172
+ if (matches.length !== 1) return { ...receipt, delivery_verification: "not_observed", retry_safe: false };
173
+ Object.assign(receipt, { submission_status: "accepted", delivery_verification: "stored", user_turn_id: matches[0].turn_id });
174
+ await write("cloud-messages", attemptId, receipt);
175
+ if ((await load("cloud-send-heads", taskId))?.attempt_id === attemptId) await write("cloud-send-heads", taskId, receipt);
176
+ return receipt;
177
+ });
178
+ }