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.
- package/CHANGELOG.md +16 -0
- package/README.md +6 -0
- package/docs/distribution.md +4 -0
- package/package.json +1 -1
- package/src/app-server.mjs +4 -4
- package/src/checkpoint.mjs +10 -10
- package/src/cli.mjs +79 -75
- package/src/cloud-cli.mjs +25 -12
- package/src/connection-error.mjs +7 -0
- package/src/desktop-cloud-id.mjs +9 -1
- package/src/desktop-cloud-project.mjs +37 -4
- package/src/desktop-cloud.mjs +10 -2
- package/src/desktop.mjs +9 -9
- package/src/directive.mjs +4 -4
- package/src/evidence.mjs +3 -3
- package/src/findings.mjs +6 -3
- package/src/help-cloud-remote.mjs +399 -0
- package/src/help-errors.mjs +10 -0
- package/src/help-intervention.mjs +492 -0
- package/src/help-local.mjs +508 -0
- package/src/help-observation.mjs +200 -0
- package/src/help-supervision.mjs +253 -0
- package/src/help.mjs +58 -304
- package/src/journal.mjs +22 -7
- package/src/launcher.mjs +6 -6
- package/src/monitor.mjs +2 -2
- package/src/notify.mjs +3 -3
- package/src/observe.mjs +4 -4
- package/src/paged-read.mjs +2 -2
- package/src/resource.mjs +6 -6
- package/src/runner.mjs +5 -5
- package/src/store.mjs +2 -2
- package/src/supervise.mjs +1 -1
- package/src/supervision.mjs +58 -16
- package/src/task-id.mjs +5 -4
- package/src/thread-id.mjs +5 -5
- package/src/wrapper.mjs +2 -2
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|
package/src/desktop-cloud.mjs
CHANGED
|
@@ -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(
|
|
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 }) {
|