@awak-app/simy-cli 0.5.67 → 0.5.69
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/package.json +1 -1
- package/src/agent.js +4 -0
- package/src/cli-contract.js +1 -1
- package/src/sqm/history-store.js +2 -2
- package/src/sqm/review-worker.js +72 -10
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -878,6 +878,10 @@ export async function startAgent({
|
|
|
878
878
|
return (dependencies.runSqmLocalReview || ((value, selected, options) => runStructuredProviderReview(selected, value, options)))(request, provider, { beforeDispatch });
|
|
879
879
|
},
|
|
880
880
|
onChange: notifyDesktopStatusChange,
|
|
881
|
+
getWorkspaceUrl: () => {
|
|
882
|
+
const slug = desktopAccess.snapshot().organizationSlug;
|
|
883
|
+
return slug ? new URL(`/org/${encodeURIComponent(slug)}/quality-monitor/`, apiOrigin).toString() : null;
|
|
884
|
+
},
|
|
881
885
|
getHistoryScope: () => sqmHistoryScope(apiOrigin, session),
|
|
882
886
|
recordHistory: (scope, item) => sqmHistory.record(scope, item),
|
|
883
887
|
onHistoryError: () => { if (!quiet) console.error("Could not save the local SQM analysis history."); },
|
package/src/cli-contract.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "./durable-local-task-steps-contract.js";
|
|
14
14
|
import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
|
|
15
15
|
|
|
16
|
-
export const CLI_VERSION = "0.5.
|
|
16
|
+
export const CLI_VERSION = "0.5.69";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function withCliContract(
|
package/src/sqm/history-store.js
CHANGED
|
@@ -60,7 +60,7 @@ export function createSqmHistoryStore({ root = defaultSessionRoot(), now = () =>
|
|
|
60
60
|
const item = (await this.items(scope)).find(item => item.key === key);
|
|
61
61
|
if (!item) return null;
|
|
62
62
|
return { key, instruction: item.instruction || `SQM local review · ${item.provider || "local"}`,
|
|
63
|
-
result: item.result || "", error: item.state === "interrupted" ? "This client stopped before the local run finished. Check the web workspace for its current retry status." : item.error || "", links: [] };
|
|
63
|
+
result: item.result || "", error: item.state === "interrupted" ? "This client stopped before the local run finished. Check the web workspace for its current retry status." : item.error || "", links: item.webUrl ? [{ label: "Analysis details", url: item.webUrl }] : [] };
|
|
64
64
|
},
|
|
65
65
|
};
|
|
66
66
|
}
|
|
@@ -69,7 +69,7 @@ function metadata(entry) {
|
|
|
69
69
|
if (!entry || !["analysis", "check"].includes(entry.kind) || typeof entry.key !== "string" || !entry.key.startsWith(entry.kind === "analysis" ? "sqm:" : "sqm-check:")) return null;
|
|
70
70
|
if (!["running", "completed", "failed", "unavailable", "interrupted", "verification_pending"].includes(entry.state) || !Number.isFinite(Date.parse(entry.startedAt))) return null;
|
|
71
71
|
const item = {};
|
|
72
|
-
for (const name of ["key", "id", "kind", "label", "state", "provider", "phase", "repository", "startedAt", "finishedAt", "instruction", "result", "error"]) {
|
|
72
|
+
for (const name of ["key", "id", "kind", "label", "state", "provider", "phase", "repository", "startedAt", "finishedAt", "instruction", "result", "error", "errorCode", "webUrl"]) {
|
|
73
73
|
if (typeof entry[name] === "string") item[name] = entry[name].slice(0, ["instruction", "result", "error"].includes(name) ? 4096 : 512);
|
|
74
74
|
}
|
|
75
75
|
return item;
|
package/src/sqm/review-worker.js
CHANGED
|
@@ -18,11 +18,12 @@ const DEFAULT_EXECUTION_TIMEOUT_MS = 240_000;
|
|
|
18
18
|
export const SQM_CODEX_MODEL = "gpt-6.1-sol";
|
|
19
19
|
|
|
20
20
|
export class SqmLocalReviewError extends Error {
|
|
21
|
-
constructor(message, { status = null, code = "sqm_local_review_error" } = {}) {
|
|
21
|
+
constructor(message, { status = null, code = "sqm_local_review_error", stage = null, jobId = null, failureRecorded = false } = {}) {
|
|
22
22
|
super(message);
|
|
23
23
|
this.name = "SqmLocalReviewError";
|
|
24
24
|
this.status = status;
|
|
25
25
|
this.code = code;
|
|
26
|
+
this.stage = stage; this.jobId = jobId; this.failureRecorded = failureRecorded;
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
|
|
@@ -69,6 +70,7 @@ export function createSqmLocalReviewApi({
|
|
|
69
70
|
throw new SqmLocalReviewError(webApiErrorMessage(payload, response.status), {
|
|
70
71
|
status: response.status,
|
|
71
72
|
code: machineCode(payload?.code || payload?.detail?.code),
|
|
73
|
+
stage: payload?.detail?.stage, jobId: payload?.detail?.job_id, failureRecorded: payload?.detail?.failure_recorded === true,
|
|
72
74
|
});
|
|
73
75
|
}
|
|
74
76
|
return payload;
|
|
@@ -87,6 +89,7 @@ export function createSqmLocalReviewWorker({
|
|
|
87
89
|
onError = () => {},
|
|
88
90
|
onChange = () => {},
|
|
89
91
|
getHistoryScope = () => null,
|
|
92
|
+
getWorkspaceUrl = () => null,
|
|
90
93
|
recordHistory = async () => {},
|
|
91
94
|
onHistoryError = () => {},
|
|
92
95
|
} = {}) {
|
|
@@ -101,12 +104,13 @@ export function createSqmLocalReviewWorker({
|
|
|
101
104
|
const persist = async (scope, item) => {
|
|
102
105
|
try { await recordHistory(scope, item); } catch { onHistoryError(); /* History failure must not fail a leased review. */ }
|
|
103
106
|
};
|
|
104
|
-
const updateTask = async (id, state, scope) => {
|
|
107
|
+
const updateTask = async (id, state, scope, failure = null, failureReported = null) => {
|
|
105
108
|
const item = history.find(item => item.id === id);
|
|
106
109
|
if (item) {
|
|
107
110
|
item.state = state; item.finishedAt = new Date().toISOString();
|
|
108
111
|
item.result = state === "completed" ? "SQM review submitted." : "";
|
|
109
|
-
item.error = state === "failed" ?
|
|
112
|
+
item.error = state === "failed" ? formatFailure(failure, failureReported) : "";
|
|
113
|
+
if (failure) { item.phase = failure.stage; item.errorCode = failure.code; }
|
|
110
114
|
await persist(scope, item);
|
|
111
115
|
}
|
|
112
116
|
onChange();
|
|
@@ -120,13 +124,12 @@ export function createSqmLocalReviewWorker({
|
|
|
120
124
|
lease_token: claim.lease_token,
|
|
121
125
|
});
|
|
122
126
|
|
|
123
|
-
const processClaim = async (claim, provider) => {
|
|
124
|
-
const historyScope = getHistoryScope();
|
|
127
|
+
const processClaim = async (claim, provider, historyScope, workspaceUrl) => {
|
|
125
128
|
activeJobId = claim.job_id;
|
|
126
129
|
const priorIndex = history.findIndex(item => item.id === claim.job_id);
|
|
127
130
|
if (priorIndex >= 0) history.splice(priorIndex, 1);
|
|
128
131
|
history.unshift({ id: claim.job_id, key: `sqm:${claim.job_id}`, label: `SQM · ${String(claim.job_id).slice(0, 8)}`,
|
|
129
|
-
kind: "analysis", state: "running", provider, startedAt: new Date().toISOString() });
|
|
132
|
+
kind: "analysis", state: "running", provider, startedAt: new Date().toISOString(), webUrl: analysisTaskUrl(workspaceUrl, claim.job_id) });
|
|
130
133
|
historyScopes.set(history[0], JSON.stringify(historyScope));
|
|
131
134
|
history.splice(50);
|
|
132
135
|
await persist(historyScope, history[0]);
|
|
@@ -137,8 +140,10 @@ export function createSqmLocalReviewWorker({
|
|
|
137
140
|
};
|
|
138
141
|
heartbeatTimer = setInterval(heartbeat, heartbeatIntervalMs);
|
|
139
142
|
heartbeatTimer.unref?.();
|
|
143
|
+
let stage = "analysis";
|
|
140
144
|
try {
|
|
141
145
|
const analysis = await runReview(claim.request, provider);
|
|
146
|
+
stage = "advance";
|
|
142
147
|
const advanced = await api.request({
|
|
143
148
|
action: "advance",
|
|
144
149
|
...leaseBody(claim, provider),
|
|
@@ -146,7 +151,9 @@ export function createSqmLocalReviewWorker({
|
|
|
146
151
|
executor_model: analysis.model,
|
|
147
152
|
});
|
|
148
153
|
if (advanced.completed) { await updateTask(claim.job_id, "completed", historyScope); return advanced; }
|
|
154
|
+
stage = "adjudication";
|
|
149
155
|
const adjudication = await runReview(advanced.request, provider);
|
|
156
|
+
stage = "complete";
|
|
150
157
|
const completed = await api.request({
|
|
151
158
|
action: "complete",
|
|
152
159
|
...leaseBody(claim, provider),
|
|
@@ -157,7 +164,16 @@ export function createSqmLocalReviewWorker({
|
|
|
157
164
|
await updateTask(claim.job_id, "completed", historyScope);
|
|
158
165
|
return completed;
|
|
159
166
|
} catch (error) {
|
|
160
|
-
|
|
167
|
+
const failure = reviewFailure(error, stage, provider);
|
|
168
|
+
let failureReported = error.failureRecorded === true;
|
|
169
|
+
if (!failureReported) {
|
|
170
|
+
try {
|
|
171
|
+
const report = await api.request({ action: "fail", ...leaseBody(claim, provider),
|
|
172
|
+
error_stage: failure.stage, error_code: failure.code, ...(failure.status ? { error_status: failure.status } : {}) });
|
|
173
|
+
failureReported = report?.recorded === true;
|
|
174
|
+
} catch { /* Keep the original failure even when reporting fails. */ }
|
|
175
|
+
}
|
|
176
|
+
await updateTask(claim.job_id, "failed", historyScope, failure, failureReported);
|
|
161
177
|
throw error;
|
|
162
178
|
} finally {
|
|
163
179
|
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
@@ -173,7 +189,7 @@ export function createSqmLocalReviewWorker({
|
|
|
173
189
|
desktopTasks() { return { activeCount: activeJobId ? 1 : 0, items: visibleHistory().map(item => ({ ...item })) }; },
|
|
174
190
|
desktopTaskDetail(key) {
|
|
175
191
|
const item = visibleHistory().find(item => item.key === key);
|
|
176
|
-
return item ? { key, instruction: `SQM local review · ${item.provider}`, result: item.state === "completed" ? "SQM review submitted." : "", error: item.
|
|
192
|
+
return item ? { key, instruction: `SQM local review · ${item.provider}`, result: item.state === "completed" ? "SQM review submitted." : "", error: item.error || "", links: item.webUrl ? [{ label: "Analysis details", url: item.webUrl }] : [] } : null;
|
|
177
193
|
},
|
|
178
194
|
resetHistory() { if (!pollPromise) { history.splice(0); issue = null; } },
|
|
179
195
|
poll() {
|
|
@@ -183,6 +199,8 @@ export function createSqmLocalReviewWorker({
|
|
|
183
199
|
assertProvider(provider);
|
|
184
200
|
const releaseWork = beginWork?.();
|
|
185
201
|
if (!releaseWork) return Promise.resolve(null);
|
|
202
|
+
const historyScope = getHistoryScope();
|
|
203
|
+
const workspaceUrl = getWorkspaceUrl();
|
|
186
204
|
pollPromise = (async () => {
|
|
187
205
|
const claim = await api.request({
|
|
188
206
|
action: "claim",
|
|
@@ -192,9 +210,18 @@ export function createSqmLocalReviewWorker({
|
|
|
192
210
|
});
|
|
193
211
|
if (issue) { issue = null; onChange(); }
|
|
194
212
|
if (!claim?.claimed) return null;
|
|
195
|
-
return processClaim(claim, provider);
|
|
213
|
+
return processClaim(claim, provider, historyScope, workspaceUrl);
|
|
196
214
|
})()
|
|
197
|
-
.catch((error) => {
|
|
215
|
+
.catch(async (error) => {
|
|
216
|
+
if (error.jobId && /^[a-f0-9-]{36}$/i.test(error.jobId) && !history.some(item => item.id === error.jobId)) {
|
|
217
|
+
const scope = historyScope;
|
|
218
|
+
const failure = reviewFailure(error, "claim", provider);
|
|
219
|
+
const item = { id: error.jobId, key: `sqm:${error.jobId}`, label: `SQM · ${error.jobId.slice(0, 8)}`,
|
|
220
|
+
kind: "analysis", state: "failed", provider, startedAt: new Date().toISOString(), finishedAt: new Date().toISOString(),
|
|
221
|
+
phase: failure.stage, errorCode: failure.code, error: formatFailure(failure, error.failureRecorded === true), webUrl: analysisTaskUrl(workspaceUrl, error.jobId) };
|
|
222
|
+
history.unshift(item); historyScopes.set(item, JSON.stringify(scope)); history.splice(50);
|
|
223
|
+
await persist(scope, item);
|
|
224
|
+
}
|
|
198
225
|
issue = error.status === 403 ? "forbidden" : error.status === 401 ? "signed_out" : "unavailable";
|
|
199
226
|
onError(error);
|
|
200
227
|
return null;
|
|
@@ -453,3 +480,38 @@ function assertProvider(provider) {
|
|
|
453
480
|
throw new TypeError("Unsupported SQM local review provider");
|
|
454
481
|
}
|
|
455
482
|
}
|
|
483
|
+
|
|
484
|
+
function reviewFailure(error, stage, provider) {
|
|
485
|
+
let code = machineCode(error?.code);
|
|
486
|
+
const message = errorMessage(error);
|
|
487
|
+
if (code === "sqm_local_review_error" || code === `${provider}_local_review_failed`) {
|
|
488
|
+
if (/timed out|timeout/i.test(message)) code = "analysis_timeout";
|
|
489
|
+
else if (providerSignInRequired(message)) code = "provider_sign_in_required";
|
|
490
|
+
else if (stage === "analysis" || stage === "adjudication") code = `${provider}_local_review_failed`;
|
|
491
|
+
}
|
|
492
|
+
return { stage: /^[a-z_]{1,40}$/.test(error?.stage || "") ? error.stage : stage, code,
|
|
493
|
+
status: Number.isInteger(error?.status) && error.status >= 400 && error.status <= 599 ? error.status : null };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function formatFailure(failure, failureReported = null) {
|
|
497
|
+
if (!failure) return "SQM analysis failed. No failure diagnostics were recorded.";
|
|
498
|
+
const hint = failure.code === "provider_sign_in_required" ? "Sign in to the local AI app again."
|
|
499
|
+
: failure.code === "analysis_timeout" ? "The local AI analysis exceeded its time limit."
|
|
500
|
+
: failure.code === "sqm_source_loading_failed" ? "SIMY could not load evidence for this analysis."
|
|
501
|
+
: failure.status === 403 || failure.code === "desktop_access_required" ? "Check SQM and Developer access for the selected organization."
|
|
502
|
+
: failure.status === 401 ? "Reconnect your SIMY session."
|
|
503
|
+
: failure.code.endsWith("_local_unavailable") ? "Install or update the selected local AI app."
|
|
504
|
+
: failure.code.endsWith("_local_review_failed") ? "The local AI app did not complete a valid structured review."
|
|
505
|
+
: "Open analysis details to inspect the task state.";
|
|
506
|
+
return `${failure.stage} · ${failure.code}${failure.status ? ` · HTTP ${failure.status}` : ""}\n${hint}${failureReported === false ? "\nThe failure could not be reported to SIMY Web. The web task may still show as analyzing until its lease expires." : ""}`;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function analysisTaskUrl(workspaceUrl, id) {
|
|
510
|
+
if (!workspaceUrl || !/^[a-f0-9-]{36}$/i.test(id)) return "";
|
|
511
|
+
try {
|
|
512
|
+
const url = new URL(workspaceUrl);
|
|
513
|
+
if (!/^https?:$/.test(url.protocol)) return "";
|
|
514
|
+
url.searchParams.set("qualityTab", "analysis"); url.searchParams.set("qualityAnalysis", id);
|
|
515
|
+
return url.toString();
|
|
516
|
+
} catch { return ""; }
|
|
517
|
+
}
|