codexteer 0.18.2 → 0.20.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 (45) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +13 -5
  3. package/bin/codexteer.mjs +1 -1
  4. package/docs/distribution.md +23 -6
  5. package/docs/orchestration.md +4 -4
  6. package/package.json +1 -1
  7. package/src/app-server.mjs +4 -4
  8. package/src/checkpoint.mjs +10 -10
  9. package/src/cli.mjs +90 -79
  10. package/src/cloud-cli.mjs +29 -33
  11. package/src/connection-error.mjs +7 -0
  12. package/src/cursor.mjs +370 -0
  13. package/src/desktop-cloud-id.mjs +9 -1
  14. package/src/desktop-cloud-project.mjs +37 -4
  15. package/src/desktop-cloud.mjs +10 -2
  16. package/src/desktop.mjs +9 -9
  17. package/src/directive.mjs +7 -6
  18. package/src/distribution.mjs +58 -33
  19. package/src/evidence.mjs +3 -3
  20. package/src/findings.mjs +13 -8
  21. package/src/help-cloud-remote.mjs +399 -0
  22. package/src/help-errors.mjs +10 -0
  23. package/src/help-intervention.mjs +492 -0
  24. package/src/help-local.mjs +510 -0
  25. package/src/help-observation.mjs +200 -0
  26. package/src/help-supervision.mjs +257 -0
  27. package/src/help.mjs +126 -304
  28. package/src/journal.mjs +33 -12
  29. package/src/launcher.mjs +6 -6
  30. package/src/monitor.mjs +5 -4
  31. package/src/notify.mjs +3 -3
  32. package/src/observe.mjs +11 -40
  33. package/src/orchestrate-prompt.mjs +14 -12
  34. package/src/orchestrate.mjs +4 -7
  35. package/src/paged-read.mjs +10 -17
  36. package/src/prompt-command.mjs +45 -7
  37. package/src/prompt.mjs +15 -12
  38. package/src/resource.mjs +6 -6
  39. package/src/runner.mjs +5 -5
  40. package/src/store.mjs +2 -2
  41. package/src/supervise.mjs +5 -7
  42. package/src/task-id.mjs +29 -4
  43. package/src/thread-id.mjs +5 -5
  44. package/src/unknown-options.mjs +17 -0
  45. package/src/wrapper.mjs +2 -2
@@ -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
@@ -1,5 +1,6 @@
1
1
  import { collectEvidence, verifyEvidence } from "./evidence.mjs";
2
- import { decodeCursor, threadSnapshot, fetchThread } from "./observe.mjs";
2
+ import { threadSnapshot, fetchThread } from "./observe.mjs";
3
+ import { parseCursor } from "./cursor.mjs";
3
4
  import { getCheckpoint, verifyCheckpoint } from "./checkpoint.mjs";
4
5
  import { verifyPagedFreshness } from "./paged-read.mjs";
5
6
  import { assertFindingSend, getFinding } from "./findings.mjs";
@@ -11,11 +12,11 @@ export async function prepareDirective(threadId, body, options, id, storage = {}
11
12
  if (!source && !kind && !evidence.length && !basedOn && !supersedes && !retracts && !expiresAt && !checkpoint && !finding && !supervisor) return { wireText: body, metadata: null };
12
13
  const concern = finding ? await assertFindingSend(threadId, finding, storage) : null;
13
14
  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).");
18
- const observed = basedOn ? decodeCursor(basedOn, threadId) : null;
15
+ if (supersedes && retracts) throw Object.assign(new Error("Use either supersedes or retracts, not both."), { code: "OPTIONS_INVALID" });
16
+ 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" });
17
+ if (kind && !Object.hasOwn(kindLabels, kind)) throw Object.assign(new Error("--kind must be decision, review, hypothesis, or suggestion."), { code: "OPTIONS_INVALID" });
18
+ 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" });
19
+ const observed = basedOn ? parseCursor(basedOn, threadId) : null;
19
20
  const refs = await collectEvidence(evidence);
20
21
  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,
21
22
  ...(concern ? { finding_id: concern.id, finding_revision: concern.revision } : {}), ...(supervisor ? { supervisor_id: supervisor } : {}) };
@@ -1,6 +1,6 @@
1
1
  import { constants } from "node:fs";
2
2
  import { lstat, mkdir, mkdtemp, open, readdir, realpath, rename, rm } from "node:fs/promises";
3
- import { createHash } from "node:crypto";
3
+ import { createHash, randomBytes } from "node:crypto";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import path from "node:path";
6
6
  import { setTimeout as delay } from "node:timers/promises";
@@ -77,6 +77,25 @@ async function readSafe(file, privateMode) {
77
77
  } finally { await handle.close(); }
78
78
  }
79
79
 
80
+ const present = file => lstat(file).then(() => true, e => { if (e.code !== "ENOENT") throw e; return false; });
81
+
82
+ // An abandoned lock is never stolen. A concurrent caller can reuse the
83
+ // completed placement after its owner releases this lock.
84
+ async function exclusively(lock, operation) {
85
+ let lockInfo;
86
+ for (let attempt = 0; attempt < 100; attempt++) {
87
+ try { await mkdir(lock, { mode: 0o700 }); lockInfo = await ownedDirectory(lock); break; }
88
+ catch (error) {
89
+ if (error.code !== "EEXIST") throw error;
90
+ await ownedDirectory(lock).catch(e => { if (e.code !== "ENOENT") throw e; });
91
+ await delay(50);
92
+ }
93
+ }
94
+ if (!lockInfo) throw failure(`Placement is busy or an earlier placement was interrupted. Inspect ${lock} before retrying.`, "DEPLOYMENT_BUSY");
95
+ try { return await operation(); }
96
+ finally { if (sameFile(lockInfo, await lstat(lock).catch(() => ({})))) await rm(lock, { recursive: true }); }
97
+ }
98
+
80
99
  async function writeExclusive(file, bytes, mode) {
81
100
  const handle = await open(file, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, mode);
82
101
  try { await handle.writeFile(bytes); await handle.sync(); } finally { await handle.close(); }
@@ -155,38 +174,44 @@ export async function prepareDeployment(home, { sourceRoot = PACKAGE_ROOT } = {}
155
174
  const canonicalHome = await prepareCodexHome(home);
156
175
  const stateRoot = path.join(canonicalHome, "codex-steer"), root = path.join(stateRoot, "runtimes");
157
176
  await ownedDirectory(stateRoot, true); await ownedDirectory(root, true);
158
- const target = path.join(root, distribution.id), lock = path.join(root, `.install-${distribution.id}`);
159
- let lockInfo;
160
- // An abandoned lock is never stolen. A concurrent caller can reuse the
161
- // completed distribution after its installer releases this lock.
162
- for (let attempt = 0; attempt < 100; attempt++) {
163
- try { await mkdir(lock, { mode: 0o700 }); lockInfo = await ownedDirectory(lock); break; }
164
- catch (error) {
165
- if (error.code !== "EEXIST") throw error;
166
- await ownedDirectory(lock).catch(e => { if (e.code !== "ENOENT") throw e; });
167
- await delay(50);
177
+ const target = path.join(root, distribution.id);
178
+ return exclusively(path.join(root, `.install-${distribution.id}`), async () => {
179
+ let stage, stageInfo;
180
+ try {
181
+ if (await present(target)) return { ...await verifyDeployment(target, distribution), codex_home: canonicalHome, reused: true };
182
+ stage = await mkdtemp(path.join(root, ".stage-")); stageInfo = await ownedDirectory(stage);
183
+ for (const file of distribution.manifest.files) {
184
+ const parts = file.path.split("/"); parts.pop(); let parent = stage;
185
+ for (const part of parts) { parent = path.join(parent, part); await ownedDirectory(parent, true); }
186
+ await writeExclusive(path.join(stage, file.path), distribution.contents.get(file.path), file.mode);
187
+ }
188
+ await writeExclusive(path.join(stage, RECORD), JSON.stringify({ manifest: distribution.manifest, sha256: distribution.sha256 }) + "\n", 0o600);
189
+ await verifyDeployment(stage, distribution);
190
+ // All cooperating installers hold the exclusive lock, and the parent is
191
+ // private. Refuse even an empty existing directory rather than replacing it.
192
+ if (await present(target)) throw failure("Deployment destination appeared during placement.");
193
+ await rename(stage, target); stage = null;
194
+ return { ...await verifyDeployment(target, distribution), codex_home: canonicalHome, reused: false };
195
+ } finally {
196
+ if (stage && sameFile(stageInfo, await lstat(stage).catch(() => ({})))) await rm(stage, { recursive: true });
168
197
  }
169
- }
170
- if (!lockInfo) throw failure("Runtime placement is busy or an earlier installation was interrupted. Inspect the installation lock before retrying.", "DEPLOYMENT_BUSY");
171
- let stage, stageInfo;
172
- try {
173
- const existing = await lstat(target).catch(e => { if (e.code !== "ENOENT") throw e; return null; });
174
- if (existing) return { ...await verifyDeployment(target, distribution), codex_home: canonicalHome, reused: true };
175
- stage = await mkdtemp(path.join(root, ".stage-")); stageInfo = await ownedDirectory(stage);
176
- for (const file of distribution.manifest.files) {
177
- const parts = file.path.split("/"); parts.pop(); let parent = stage;
178
- for (const part of parts) { parent = path.join(parent, part); await ownedDirectory(parent, true); }
179
- await writeExclusive(path.join(stage, file.path), distribution.contents.get(file.path), file.mode);
198
+ });
199
+ }
200
+
201
+ // A launcher is named by its own content, so reuse and tamper detection are
202
+ // one comparison. An existing file is verified, never rewritten.
203
+ export async function placeLauncher(deployment, kind, script) {
204
+ const bytes = Buffer.from(script), stateRoot = path.join(deployment.codex_home, "codex-steer"), bin = path.join(stateRoot, "bin");
205
+ await ownedDirectory(stateRoot); await ownedDirectory(bin, true);
206
+ const name = `codexteer-${kind}-${hash(bytes).slice(0, 12)}`, file = path.join(bin, name);
207
+ return exclusively(path.join(bin, `.install-${name}`), async () => {
208
+ if (!await present(file)) {
209
+ const stage = path.join(bin, `.stage-${randomBytes(8).toString("hex")}`);
210
+ try { await writeExclusive(stage, bytes, 0o700); await rename(stage, file); }
211
+ finally { await rm(stage, { force: true }); }
180
212
  }
181
- await writeExclusive(path.join(stage, RECORD), JSON.stringify({ manifest: distribution.manifest, sha256: distribution.sha256 }) + "\n", 0o600);
182
- await verifyDeployment(stage, distribution);
183
- // All cooperating installers hold the exclusive lock, and the parent is
184
- // private. Refuse even an empty existing directory rather than replacing it.
185
- if (await lstat(target).catch(e => { if (e.code !== "ENOENT") throw e; return null; })) throw failure("Deployment destination appeared during placement.");
186
- await rename(stage, target); stage = null;
187
- return { ...await verifyDeployment(target, distribution), codex_home: canonicalHome, reused: false };
188
- } finally {
189
- if (stage && sameFile(stageInfo, await lstat(stage).catch(() => ({})))) await rm(stage, { recursive: true });
190
- if (sameFile(lockInfo, await lstat(lock).catch(() => ({})))) await rm(lock, { recursive: true });
191
- }
213
+ const { bytes: found, info } = await readSafe(file, true);
214
+ if ((info.mode & 0o777) !== 0o700 || !found.equals(bytes)) throw failure(`The launcher ${file} differs from the one this CLI generates. It was left unchanged; ask the user to inspect or remove it before generating again.`, "LAUNCHER_MODIFIED");
215
+ return file;
216
+ });
192
217
  }
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
@@ -2,7 +2,8 @@ import { randomUUID } from "node:crypto";
2
2
  import { normalizeTaskId } from "./task-id.mjs";
3
3
  import { readRecord, writeRecord, listRecords, withStoreLock } from "./store.mjs";
4
4
  import { collectEvidence } from "./evidence.mjs";
5
- import { decodeCursor, digest } from "./observe.mjs";
5
+ import { digest } from "./observe.mjs";
6
+ import { parseCursor, withCursorReference } from "./cursor.mjs";
6
7
  import { verifyCheckpoint } from "./checkpoint.mjs";
7
8
 
8
9
  const failure = (message, code) => Object.assign(new Error(message), { code });
@@ -14,6 +15,7 @@ const text = (value, label, max = 8000) => {
14
15
  // A cursor can advance because our own intervention appeared as a userMessage.
15
16
  // It is observation context, never sufficient evidence for another intervention.
16
17
  const contextHash = record => digest({ condition: record.condition, evidence: record.evidence });
18
+ const presentFinding = record => ({ ...withCursorReference(record), ...(Array.isArray(record.revisions) ? { revisions: record.revisions.map(withCursorReference) } : {}) });
17
19
 
18
20
  export async function getFinding(threadInput, id, storage = {}) {
19
21
  const threadId = normalizeTaskId(threadInput), record = await readRecord("findings", id, storage);
@@ -33,7 +35,7 @@ export async function createFinding(threadInput, { key, title, condition, eviden
33
35
  const threadId = normalizeTaskId(threadInput);
34
36
  if (typeof key !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,150}$/.test(key)) throw failure("--key requires a stable short identifier for this concern.", "FINDING_INVALID");
35
37
  text(title, "--title", 240); text(condition, "--condition");
36
- const refs = await collectEvidence(evidence), observed = basedOn ? decodeCursor(basedOn, threadId) : null;
38
+ const refs = await collectEvidence(evidence), observed = basedOn ? parseCursor(basedOn, threadId) : null;
37
39
  return withStoreLock(`message-${threadId}`, async () => {
38
40
  const existing = (await listRecords("findings", storage)).find(record => record?.thread_id === threadId && record.key === key);
39
41
  if (existing) return { ...await findingStatus(threadId, existing.id, storage), reused: true };
@@ -42,16 +44,19 @@ export async function createFinding(threadInput, { key, title, condition, eviden
42
44
  evidence: refs, based_on: basedOn ?? null, user_revision: observed?.user_revision ?? null, disposition: "open", resolution: null,
43
45
  evaluation: { rating: "unrated" }, revisions: [], created_at: now, updated_at: now };
44
46
  await writeRecord("findings", record.id, record, storage);
45
- return { ...record, status: "open", messages: [], verification: null, reused: false };
47
+ return presentFinding({ ...record, status: "open", messages: [], verification: null, reused: false });
46
48
  }, storage);
47
49
  }
48
50
 
51
+ // The user's noted resolution of an unknown delivery releases it; absence alone never does.
52
+ export const releasedDelivery = entry => entry.delivery_status === "unknown" && Boolean(entry.resolution);
53
+
49
54
  export async function assertFindingSend(threadInput, id, storage = {}) {
50
55
  const record = await getFinding(threadInput, id, storage);
51
56
  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
57
  // The journal is the reservation: even a crash before writing a receipt leaves
53
58
  // 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");
59
+ const previous = (await findingMessages(record, storage)).find(message => message.delivery_status !== "not_sent" && !releasedDelivery(message));
55
60
  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
61
  return record;
57
62
  }
@@ -60,7 +65,7 @@ export async function findingStatus(threadInput, id, storage = {}) {
60
65
  const record = await getFinding(threadInput, id, storage), entries = await findingMessages(record, storage);
61
66
  const messages = entries.map(entry => ({ id: entry.id, delivery_status: entry.delivery_status, attempt_state: entry.attempt_state,
62
67
  response: entry.response, stored: entry.verification?.status ?? "unchecked" }));
63
- const latest = entries.filter(entry => entry.delivery_status !== "not_sent").at(-1);
68
+ const latest = entries.filter(entry => entry.delivery_status !== "not_sent" && !releasedDelivery(entry)).at(-1);
64
69
  let status = record.disposition, verification = null;
65
70
  if (record.disposition === "open" && latest) status = latest.delivery_status === "unknown" ? "delivery_unknown"
66
71
  : latest.response?.status === "applied" ? "awaiting_verification" : "awaiting_response";
@@ -73,7 +78,7 @@ export async function findingStatus(threadInput, id, storage = {}) {
73
78
  } catch (error) { verification = { valid: false, checkpoint_id: record.resolution.checkpoint_id, error_code: error.code ?? "CHECKPOINT_UNAVAILABLE", condition_assessment: "explicit_report" }; }
74
79
  if (!verification.valid && record.disposition === "resolved") status = "needs_review";
75
80
  }
76
- return { ...record, status, messages, verification };
81
+ return presentFinding({ ...record, status, messages, verification });
77
82
  }
78
83
 
79
84
  export async function listFindings(threadInput, { all = false, limit = 50, ...storage } = {}) {
@@ -100,7 +105,7 @@ async function changeFinding(threadInput, id, update, storage) {
100
105
  }
101
106
 
102
107
  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");
108
+ 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
109
  }
105
110
 
106
111
  export async function resolveFinding(threadInput, id, { checkpoint, note, ...storage }) {
@@ -127,7 +132,7 @@ export async function reopenFinding(threadInput, id, { reason, evidence, basedOn
127
132
  const threadId = normalizeTaskId(threadInput);
128
133
  text(reason, "--reason"); if (condition !== undefined) text(condition, "--condition");
129
134
  const refs = evidence === undefined ? undefined : await collectEvidence(evidence);
130
- const observed = basedOn === undefined ? undefined : decodeCursor(basedOn, threadId);
135
+ const observed = basedOn === undefined ? undefined : parseCursor(basedOn, threadId);
131
136
  return changeFinding(threadId, id, async record => {
132
137
  await knownDelivery(record, storage);
133
138
  const updated = { ...record, condition: condition ?? record.condition, evidence: refs ?? record.evidence, based_on: basedOn ?? record.based_on, user_revision: observed?.user_revision ?? record.user_revision };