codexteer 0.18.1 → 0.19.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.
@@ -13,6 +13,7 @@ import { listProjects } from "./projects.mjs";
13
13
  import { listRecords, readRecord, writeRecord, withStoreLock, storePath } from "./store.mjs";
14
14
  const failure = (message, code = "DESKTOP_CLOUD_PROJECT_UNAVAILABLE", uncertain = false) => Object.assign(new Error(message), { code, uncertain });
15
15
  const sha = value => createHash("sha256").update(value).digest("hex");
16
+ const shows = (state, projectId) => state?.assignment?.projectKind === "local" && state.assignment.projectId === projectId && state.host_id === "durable";
16
17
 
17
18
  export async function discoverProjectApi({ discover = discoverRuntime, endpoint = discoverNativeEndpoint, run = spawnSync } = {}) {
18
19
  const selected = await discover(), { state } = selected;
@@ -100,7 +101,7 @@ export async function desktopCloudProjectDoctor(target, selector, { connect = co
100
101
  const state = await readState(api.paths.home, id);
101
102
  report.membership = { status: state.assignment?.projectKind === "local" ? (state.host_id === "durable" ? "registered" : "recorded_host_unverified") : state.projectless ? "projectless" : "source_unknown",
102
103
  project_id: state.assignment?.projectId ?? null, host_id: state.host_id, source: "read-only-desktop-profile-state" };
103
- if (selected) report.membership.matches_selected_project = state.assignment?.projectKind === "local" && state.assignment.projectId === selected.project_id && state.host_id === "durable";
104
+ if (selected) report.membership.matches_selected_project = shows(state, selected.project_id);
104
105
  return report;
105
106
  } finally { try { client?.close(); } catch { /* Preserve diagnostics. */ } }
106
107
  }
@@ -109,7 +110,9 @@ export async function assignDesktopCloudProject(target, selector, { connect = co
109
110
  const id = desktopCloudId(target);
110
111
  if (typeof selector !== "string" || !selector.trim()) throw failure("--project is required.", "PROJECT_REQUIRED");
111
112
  return withStoreLock(`desktop-cloud-project-${id}`, async () => {
112
- if ((await listRecords("desktop-cloud-projects", storage)).some(row => row.thread_id === id && row.delivery_status === "unknown")) throw failure("An earlier assignment is unknown. Inspect its receipt; do not replay it.", "DESKTOP_CLOUD_PROJECT_PENDING");
113
+ const pending = (await confirmFromState(id, (await listRecords("desktop-cloud-projects", storage)).filter(row => row.thread_id === id && row.delivery_status === "unknown"), readState, storage)).find(row => !row.resolution);
114
+ if (pending) throw Object.assign(failure(`An earlier assignment is unknown and is never replayed. Run thread receipt ${pending.attempt_id} --connection desktop-cloud to recheck Desktop's profile state; if it stays unknown, the user (not a supervisor) can release it with --resolve-unknown --note TEXT.`, "DESKTOP_CLOUD_PROJECT_PENDING"),
115
+ { existing_attempt_id: pending.attempt_id, existing_receipt_path: pending.receipt_path });
113
116
  let client, receipt, api, dispatching = false;
114
117
  try {
115
118
  client = await connect(); const environmentId = await targetEnvironment(client, id); api = await discoverApi();
@@ -129,7 +132,7 @@ export async function assignDesktopCloudProject(target, selector, { connect = co
129
132
  receipt.delivery_status = "accepted"; receipt.proof_sha256 = sha(response.payload);
130
133
  await writeRecord("desktop-cloud-projects", attempt, receipt, storage); await check(api);
131
134
  const stored = await readState(api.paths.home, id);
132
- receipt.verification_status = stored.assignment?.projectKind === "local" && stored.assignment.projectId === selected.project_id && stored.host_id === "durable" ? "verified" : "not_observed";
135
+ receipt.verification_status = shows(stored, selected.project_id) ? "verified" : "not_observed";
133
136
  await writeRecord("desktop-cloud-projects", attempt, receipt, storage); return receipt;
134
137
  } catch (error) {
135
138
  if (receipt) {
@@ -141,8 +144,38 @@ export async function assignDesktopCloudProject(target, selector, { connect = co
141
144
  } finally { try { client?.close(); } catch { /* Preserve diagnostics. */ } }
142
145
  }, storage);
143
146
  }
144
- export async function desktopCloudProjectReceipt(attempt, storage = {}) {
147
+ // Absence from Desktop state never proves the assignment was not applied, so
148
+ // only a positive observation rewrites an unknown attempt.
149
+ async function confirmFromState(id, rows, readState, storage) {
150
+ if (!rows.length) return rows;
151
+ let state; try { state = await readState(storage.home ?? codexHome(), id); } catch { return rows; }
152
+ const remaining = [];
153
+ for (const row of rows) {
154
+ if (!shows(state, row.project_id)) { remaining.push(row); continue; }
155
+ await writeRecord("desktop-cloud-projects", row.attempt_id, { ...row, delivery_status: "accepted", verification_status: "verified", delivery_verification: "desktop-profile-state", verified_at: new Date().toISOString() }, storage);
156
+ }
157
+ return remaining;
158
+ }
159
+ async function readAttempt(attempt, storage) {
145
160
  if (typeof attempt !== "string" || !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(attempt)) throw failure("Invalid assignment attempt ID.");
146
161
  const row = await readRecord("desktop-cloud-projects", attempt, storage);
147
162
  if (!row || row.type !== "desktop-cloud-project" || row.attempt_id !== attempt) throw failure("Assignment receipt not found."); return row;
148
163
  }
164
+ export async function desktopCloudProjectReceipt(attempt, { readState = readDesktopProjectState, ...storage } = {}) {
165
+ const row = await readAttempt(attempt, storage);
166
+ if (row.delivery_status !== "unknown") return row;
167
+ return withStoreLock(`desktop-cloud-project-${row.thread_id}`, async () => {
168
+ await confirmFromState(row.thread_id, [await readAttempt(attempt, storage)].filter(current => current.delivery_status === "unknown"), readState, storage);
169
+ return readAttempt(attempt, storage);
170
+ }, storage);
171
+ }
172
+ export async function resolveDesktopCloudProjectAttempt(attempt, { note, ...storage } = {}) {
173
+ if (typeof note !== "string" || !note.trim()) throw failure("--resolve-unknown requires a non-empty --note.", "DESKTOP_CLOUD_OPTIONS_INVALID");
174
+ const { thread_id: id } = await readAttempt(attempt, storage);
175
+ return withStoreLock(`desktop-cloud-project-${id}`, async () => {
176
+ const row = await readAttempt(attempt, storage);
177
+ if (row.delivery_status !== "unknown" || row.resolution) throw failure("This assignment attempt has no unresolved unknown delivery.", "DESKTOP_CLOUD_OPTIONS_INVALID");
178
+ const updated = { ...row, resolution: { by: "user", at: new Date().toISOString(), note } };
179
+ await writeRecord("desktop-cloud-projects", attempt, updated, storage); return updated;
180
+ }, storage);
181
+ }
@@ -52,8 +52,8 @@ export async function sendDesktopCloudInput(target, text, { dryRun = false, newT
52
52
  const plan = { thread_id: id, deep_link: desktopCloudLink(id), client_message_id: clientMessageId, backend: "desktop-cloud", connection_mode: "desktop-cloud", delivery_action: newTurn ? "new-turn" : "steer", dry_run: dryRun, message_characters: [...text].length };
53
53
  if (dryRun) return { ...plan, sent: false, delivery_status: "not_sent" };
54
54
  return withStoreLock(`desktop-cloud-send-${id}`, async () => {
55
- const pending = (await listRecords("desktop-cloud-sends", storage)).find(row => row.thread_id === id && row.delivery_status === "unknown");
56
- if (pending) throw failure("An earlier Desktop Cloud delivery is unknown. Inspect history; it must not be replayed automatically.", "DESKTOP_CLOUD_DELIVERY_PENDING");
55
+ const pending = (await listRecords("desktop-cloud-sends", storage)).find(row => row.thread_id === id && row.delivery_status === "unknown" && !row.resolution);
56
+ if (pending) throw Object.assign(failure(`An earlier Desktop Cloud delivery is unknown and is never replayed automatically. Run history check ${id} ${pending.client_message_id}; if it stays unconfirmed, the user (not a supervisor) can release it with history mark ${id} ${pending.client_message_id} --resolve-unknown --note TEXT.`, "DESKTOP_CLOUD_DELIVERY_PENDING"), { existing_message_id: pending.client_message_id });
57
57
  let client, dispatching = false, receipt = { ...plan, prompt_sha256: hash(text), sent: false, delivery_status: "not_sent" };
58
58
  const save = () => writeRecord("desktop-cloud-sends", clientMessageId, receipt, storage);
59
59
  try {
@@ -96,3 +96,11 @@ export async function confirmDesktopCloudDelivery(entry, verification, storage =
96
96
  await writeRecord("desktop-cloud-sends", entry.client_message_id, { ...receipt, delivery_status: "accepted", sent: true, turn_id: verification.turn_id, delivery_verification: "stored" }, storage);
97
97
  }, storage);
98
98
  }
99
+ export async function resolveDesktopCloudDelivery(target, clientMessageId, resolution, storage = {}) {
100
+ const id = desktopCloudId(target);
101
+ return withStoreLock(`desktop-cloud-send-${id}`, async () => {
102
+ const receipt = await readRecord("desktop-cloud-sends", clientMessageId, storage);
103
+ if (receipt?.thread_id !== id || receipt.delivery_status !== "unknown" || receipt.resolution) throw failure("This message has no unresolved unknown Desktop Cloud delivery.");
104
+ const updated = { ...receipt, resolution }; await writeRecord("desktop-cloud-sends", clientMessageId, updated, storage); return updated;
105
+ }, storage);
106
+ }
package/src/desktop.mjs CHANGED
@@ -61,7 +61,7 @@ export function openDesktopThread(threadId, { focusComposer = false, prompt, bac
61
61
  const deepLink = focusComposer ? threadComposerDeepLink(threadId, prompt) : threadDeepLink(threadId);
62
62
  const result = run(OPEN, buildOpenArgs(deepLink, { background }));
63
63
  if (result.status !== 0) {
64
- throw new Error(result.stderr.trim() || `Failed to open ${deepLink}`);
64
+ throw Object.assign(new Error(result.stderr.trim() || `Failed to open ${deepLink}`), { code: "DESKTOP_UI_FAILED" });
65
65
  }
66
66
  return {
67
67
  thread_id: threadId,
@@ -74,7 +74,7 @@ export function openDesktopThread(threadId, { focusComposer = false, prompt, bac
74
74
 
75
75
  function validateWaitMs(waitMs) {
76
76
  if (!Number.isInteger(waitMs) || waitMs < 0 || waitMs > 30000) {
77
- throw new Error("--wait-ms must be an integer between 0 and 30000.");
77
+ throw Object.assign(new Error("--wait-ms must be an integer between 0 and 30000."), { code: "OPTIONS_INVALID" });
78
78
  }
79
79
  }
80
80
 
@@ -85,7 +85,7 @@ function requireReadyDesktop() {
85
85
  .filter(([, ok]) => !ok)
86
86
  .map(([name]) => name)
87
87
  .join(", ");
88
- throw new Error(`Desktop backend is not ready: ${missing}. ${doctor.remediation ?? ""}`.trim());
88
+ throw Object.assign(new Error(`Desktop backend is not ready: ${missing}. ${doctor.remediation ?? ""}`.trim()), { code: "DESKTOP_UI_NOT_READY" });
89
89
  }
90
90
 
91
91
  function frontmostProcessId() {
@@ -96,7 +96,7 @@ function frontmostProcessId() {
96
96
  if (result.error) throw result.error;
97
97
  const processId = Number.parseInt(result.stdout.trim(), 10);
98
98
  if (result.status !== 0 || !Number.isInteger(processId) || processId < 1) {
99
- throw new Error(result.stderr.trim() || "Could not identify the previously focused application.");
99
+ throw Object.assign(new Error(result.stderr.trim() || "Could not identify the previously focused application."), { code: "DESKTOP_UI_FAILED" });
100
100
  }
101
101
  return processId;
102
102
  }
@@ -105,7 +105,7 @@ function enableDesktopAccessibility() {
105
105
  const result = run(OSASCRIPT, [ACCESSIBILITY_SCRIPT, "enable"], { timeout: 5000 });
106
106
  if (result.error) throw result.error;
107
107
  if (result.status !== 0) {
108
- throw new Error(result.stderr.trim() || "Could not enable the Codex accessibility tree.");
108
+ throw Object.assign(new Error(result.stderr.trim() || "Could not enable the Codex accessibility tree."), { code: "DESKTOP_UI_FAILED" });
109
109
  }
110
110
  return result.stdout.trim() || "unchanged";
111
111
  }
@@ -115,7 +115,7 @@ function restoreDesktopAccessibility(mode) {
115
115
  const result = run(OSASCRIPT, [ACCESSIBILITY_SCRIPT, "restore", mode], { timeout: 5000 });
116
116
  if (result.error) throw result.error;
117
117
  if (result.status !== 0) {
118
- throw new Error(result.stderr.trim() || "Could not restore the Codex accessibility state.");
118
+ throw Object.assign(new Error(result.stderr.trim() || "Could not restore the Codex accessibility state."), { code: "DESKTOP_UI_FAILED" });
119
119
  }
120
120
  }
121
121
 
@@ -134,7 +134,7 @@ export function inspectDesktopUi(threadId, { waitMs = 1500 } = {}) {
134
134
  }
135
135
  if (result.error) throw result.error;
136
136
  if (result.status !== 0) {
137
- throw new Error(result.stderr.trim() || "Could not inspect the Codex Desktop UI.");
137
+ throw Object.assign(new Error(result.stderr.trim() || "Could not inspect the Codex Desktop UI."), { code: "DESKTOP_UI_FAILED" });
138
138
  }
139
139
  return {
140
140
  thread_id: threadId,
@@ -181,7 +181,7 @@ export function sendDesktopMessage(
181
181
  { dryRun = false, waitMs = 1500, newTurn = false, keepFocus = false } = {},
182
182
  ) {
183
183
  if ((parseThreadTarget(threadId).hostId ?? targetHost()) !== "local") throw remoteReadOnly();
184
- if (message.trim() === "") throw new Error("Message must not be empty.");
184
+ if (message.trim() === "") throw Object.assign(new Error("Message must not be empty."), { code: "OPTIONS_INVALID" });
185
185
  validateWaitMs(waitMs);
186
186
 
187
187
  const desktopSettings = readDesktopSettings();
@@ -202,7 +202,7 @@ export function sendDesktopMessage(
202
202
  });
203
203
  if (result.error) throw result.error;
204
204
  if (result.status !== 0) {
205
- throw new Error(result.stderr.trim() || "Codex Desktop did not accept the steering message.");
205
+ throw Object.assign(new Error(result.stderr.trim() || "Codex Desktop did not accept the steering message."), { code: "DESKTOP_UI_FAILED" });
206
206
  }
207
207
 
208
208
  return { ...plan, dry_run: false, sent: null, delivery_status: "submitted_unverified" };
package/src/directive.mjs CHANGED
@@ -11,10 +11,10 @@ export async function prepareDirective(threadId, body, options, id, storage = {}
11
11
  if (!source && !kind && !evidence.length && !basedOn && !supersedes && !retracts && !expiresAt && !checkpoint && !finding && !supervisor) return { wireText: body, metadata: null };
12
12
  const concern = finding ? await assertFindingSend(threadId, finding, storage) : null;
13
13
  if (checkpoint) await getCheckpoint(threadId, checkpoint, storage);
14
- if (supersedes && retracts) throw new Error("Use either supersedes or retracts, not both.");
15
- if (expiresAt && (!/(Z|[+-]\d{2}:\d{2})$/.test(expiresAt) || !Number.isFinite(Date.parse(expiresAt)) || Date.parse(expiresAt) <= Date.now())) throw new Error("--expires-at must be a future ISO timestamp with timezone.");
16
- if (kind && !Object.hasOwn(kindLabels, kind)) throw new Error("--kind must be decision, review, hypothesis, or suggestion.");
17
- if (source != null && (!source.trim() || source.length > 80 || /[\r\n\x00-\x1f]/.test(source))) throw new Error("--source must be a non-empty single line (up to 80 characters).");
14
+ if (supersedes && retracts) throw Object.assign(new Error("Use either supersedes or retracts, not both."), { code: "OPTIONS_INVALID" });
15
+ if (expiresAt && (!/(Z|[+-]\d{2}:\d{2})$/.test(expiresAt) || !Number.isFinite(Date.parse(expiresAt)) || Date.parse(expiresAt) <= Date.now())) throw Object.assign(new Error("--expires-at must be a future ISO timestamp with timezone."), { code: "OPTIONS_INVALID" });
16
+ if (kind && !Object.hasOwn(kindLabels, kind)) throw Object.assign(new Error("--kind must be decision, review, hypothesis, or suggestion."), { code: "OPTIONS_INVALID" });
17
+ if (source != null && (!source.trim() || source.length > 80 || /[\r\n\x00-\x1f]/.test(source))) throw Object.assign(new Error("--source must be a non-empty single line (up to 80 characters)."), { code: "OPTIONS_INVALID" });
18
18
  const observed = basedOn ? decodeCursor(basedOn, threadId) : null;
19
19
  const refs = await collectEvidence(evidence);
20
20
  const metadata = { source: source ?? "external", kind: kind ?? "review", evidence: refs, based_on: basedOn ?? null, supersedes: supersedes ?? null, retracts: retracts ?? null, expires_at: expiresAt ?? null, checkpoint_id: checkpoint ?? null,
package/src/evidence.mjs CHANGED
@@ -8,11 +8,11 @@ import { remoteFingerprintFile } from "./remote-io.mjs";
8
8
  export async function fingerprintFile(input) {
9
9
  if (targetHost() !== "local") return remoteFingerprintFile(input);
10
10
  const ref = path.resolve(input), canonical = await realpath(ref), before = await stat(canonical);
11
- if (!before.isFile()) throw new Error("Evidence must be a file or an http(s)/codex URL.");
11
+ if (!before.isFile()) throw Object.assign(new Error("Evidence must be a file or an http(s)/codex URL."), { code: "OPTIONS_INVALID" });
12
12
  const hash = createHash("sha256");
13
13
  for await (const data of createReadStream(canonical)) hash.update(data);
14
14
  const after = await stat(canonical);
15
- if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs || (await realpath(ref)) !== canonical) throw new Error("File changed while being fingerprinted. Try again after editing finishes.");
15
+ if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs || (await realpath(ref)) !== canonical) throw Object.assign(new Error("File changed while being fingerprinted. Try again after editing finishes."), { code: "STALE_EVIDENCE" });
16
16
  return { type: "file", ref, path: canonical, size: after.size, mode: after.mode & 0o777, sha256: hash.digest("hex") };
17
17
  }
18
18
 
@@ -20,7 +20,7 @@ export async function collectEvidence(refs = []) {
20
20
  return Promise.all(refs.map(async ref => {
21
21
  if (/^(https?|codex):\/\//i.test(ref)) {
22
22
  const url = new URL(ref);
23
- if (url.username || url.password) throw new Error("Evidence URLs must not contain credentials.");
23
+ if (url.username || url.password) throw Object.assign(new Error("Evidence URLs must not contain credentials."), { code: "OPTIONS_INVALID" });
24
24
  return { type: "url", ref: url.href, verification: "reference_only" };
25
25
  }
26
26
  return fingerprintFile(ref);
package/src/findings.mjs CHANGED
@@ -46,12 +46,15 @@ export async function createFinding(threadInput, { key, title, condition, eviden
46
46
  }, storage);
47
47
  }
48
48
 
49
+ // The user's noted resolution of an unknown delivery releases it; absence alone never does.
50
+ export const releasedDelivery = entry => entry.delivery_status === "unknown" && Boolean(entry.resolution);
51
+
49
52
  export async function assertFindingSend(threadInput, id, storage = {}) {
50
53
  const record = await getFinding(threadInput, id, storage);
51
54
  if (record.disposition !== "open") throw failure("This finding is closed. Reopen it with new evidence or a changed requirement before sending again.", "FINDING_CLOSED");
52
55
  // The journal is the reservation: even a crash before writing a receipt leaves
53
56
  // an unknown attempt here. Never infer not-sent from a missing response.
54
- const previous = (await findingMessages(record, storage)).find(message => message.delivery_status !== "not_sent");
57
+ const previous = (await findingMessages(record, storage)).find(message => message.delivery_status !== "not_sent" && !releasedDelivery(message));
55
58
  if (previous) throw Object.assign(failure("This finding revision already has a pending or accepted intervention. Check its history; do not resend it.", "DUPLICATE_INTERVENTION"), { existing_message_id: previous.id });
56
59
  return record;
57
60
  }
@@ -60,7 +63,7 @@ export async function findingStatus(threadInput, id, storage = {}) {
60
63
  const record = await getFinding(threadInput, id, storage), entries = await findingMessages(record, storage);
61
64
  const messages = entries.map(entry => ({ id: entry.id, delivery_status: entry.delivery_status, attempt_state: entry.attempt_state,
62
65
  response: entry.response, stored: entry.verification?.status ?? "unchecked" }));
63
- const latest = entries.filter(entry => entry.delivery_status !== "not_sent").at(-1);
66
+ const latest = entries.filter(entry => entry.delivery_status !== "not_sent" && !releasedDelivery(entry)).at(-1);
64
67
  let status = record.disposition, verification = null;
65
68
  if (record.disposition === "open" && latest) status = latest.delivery_status === "unknown" ? "delivery_unknown"
66
69
  : latest.response?.status === "applied" ? "awaiting_verification" : "awaiting_response";
@@ -100,7 +103,7 @@ async function changeFinding(threadInput, id, update, storage) {
100
103
  }
101
104
 
102
105
  async function knownDelivery(record, storage) {
103
- if ((await findingMessages(record, storage)).some(entry => entry.delivery_status === "unknown")) throw failure("Delivery is unknown. Reconcile the existing message before closing or revising this finding.", "FINDING_DELIVERY_UNKNOWN");
106
+ if ((await findingMessages(record, storage)).some(entry => entry.delivery_status === "unknown" && !releasedDelivery(entry))) throw failure("Delivery is unknown. Reconcile the existing message before closing or revising this finding.", "FINDING_DELIVERY_UNKNOWN");
104
107
  }
105
108
 
106
109
  export async function resolveFinding(threadInput, id, { checkpoint, note, ...storage }) {