@awak-app/simy-cli 0.5.71 → 0.5.73
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/README.md +101 -3
- package/package.json +2 -2
- package/src/agent.js +205 -12
- package/src/backend-executable.js +45 -2
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/hooks/README.md +22 -0
- package/src/codex-plugins/simy/hooks/continuation-policy.mjs +27 -0
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +180 -31
- package/src/codex-plugins/simy/hooks/outcome-progress.mjs +129 -0
- package/src/coding-run-recovery.js +88 -0
- package/src/coding-run-store.js +2 -2
- package/src/continuation-policy.js +2 -0
- package/src/delivery-hooks/command.js +11 -5
- package/src/delivery-hooks/desktop.js +12 -3
- package/src/delivery-hooks/supervisor.js +17 -12
- package/src/desktop-host.js +19 -8
- package/src/desktop-sqm-settings.js +24 -7
- package/src/device-signing-identity.js +65 -0
- package/src/diagnostic-log.js +68 -0
- package/src/doctor.js +55 -0
- package/src/index.js +18 -1
- package/src/local-task.js +68 -3
- package/src/orchestrator/budget.js +55 -4
- package/src/orchestrator/charter-validation.js +33 -0
- package/src/orchestrator/completion-contract.js +1 -1
- package/src/orchestrator/contract.js +2 -0
- package/src/orchestrator/delivery/references/state-and-receipts.md +21 -3
- package/src/orchestrator/evidence.js +20 -19
- package/src/orchestrator/independent-audit.js +9 -0
- package/src/orchestrator/instruction.js +6 -1
- package/src/orchestrator/loop.js +66 -41
- package/src/orchestrator/outcome-monitor.js +95 -44
- package/src/orchestrator/outcome-progress.js +2 -0
- package/src/orchestrator/recovery.js +3 -0
- package/src/purpose-local-proof.js +380 -0
- package/src/runner.js +227 -19
- package/src/runtime-diagnostics.js +77 -0
- package/src/session-supervision/codex-app-server.js +3 -2
- package/src/session-supervision/index.js +16 -4
- package/src/sqm/bundle-store.js +14 -7
- package/src/sqm/claude-plugin.js +2 -0
- package/src/sqm/command.js +42 -4
- package/src/sqm/diagnostics.js +23 -0
- package/src/sqm/evidence-client.js +24 -7
- package/src/sqm/history-store.js +21 -5
- package/src/sqm/index.js +116 -17
- package/src/sqm/proof.js +4 -23
- package/src/sqm/report.js +9 -2
- package/src/sqm/request-recovery.js +82 -0
- package/src/sqm/review-worker.js +97 -15
- package/src/sqm/session.js +33 -3
- package/src/startup-preflight.js +44 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const TRANSIENT_STATUSES = new Set([502, 503, 504]);
|
|
2
|
+
const NETWORK_CODES = new Set(["ECONNRESET", "ECONNREFUSED", "ENETUNREACH", "EAI_AGAIN", "ETIMEDOUT", "UND_ERR_CONNECT_TIMEOUT", "UND_ERR_SOCKET"]);
|
|
3
|
+
|
|
4
|
+
export function sqmDiagnostic(error, stage = "unknown") {
|
|
5
|
+
const status = error?.http_status ?? error?.status;
|
|
6
|
+
const timeout = error?.name === "TimeoutError" || error?.code === "ETIMEDOUT";
|
|
7
|
+
const network = (error instanceof TypeError && /fetch failed|failed to fetch|network request failed/i.test(error.message)) ||
|
|
8
|
+
NETWORK_CODES.has(error?.code) || NETWORK_CODES.has(error?.cause?.code);
|
|
9
|
+
const safeCode = typeof error?.code === "string" && /^[a-z][a-z0-9_]{0,79}$/.test(error.code) ? error.code : null;
|
|
10
|
+
return {
|
|
11
|
+
stage: typeof error?.stage === "string" && /^[a-z][a-z0-9_]{0,79}$/.test(error.stage) ? error.stage : stage,
|
|
12
|
+
code: safeCode || (status === 401 ? "sign_in_required" : status === 403 ? "access_denied" :
|
|
13
|
+
status === 409 ? "scope_unresolved" : TRANSIENT_STATUSES.has(status) ? "gateway_unavailable" :
|
|
14
|
+
timeout ? "request_timeout" : network ? "network_unavailable" : "check_failed"),
|
|
15
|
+
...(Number.isInteger(status) && status >= 400 && status <= 599 ? { http_status: status } : {}),
|
|
16
|
+
retryable: error?.retryable !== false && (TRANSIENT_STATUSES.has(status) || timeout || network),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// This deliberately exposes only GET recovery. Uncertain POST success must be
|
|
21
|
+
// reconciled with a server idempotency contract before it may be replayed.
|
|
22
|
+
export async function fetchSqmRead(url, {
|
|
23
|
+
headers, signal, fetchImpl = globalThis.fetch,
|
|
24
|
+
timeoutMs = 45_000, requestTimeoutMs = 15_000, maxAttempts = 3,
|
|
25
|
+
sleep = abortableSleep, now = Date.now, onRecoveryEvent = async () => {},
|
|
26
|
+
} = {}) {
|
|
27
|
+
const totalLimit = boundedInteger(timeoutMs, 45_000);
|
|
28
|
+
const requestLimit = boundedInteger(requestTimeoutMs, 15_000);
|
|
29
|
+
const deadline = now() + totalLimit;
|
|
30
|
+
const deadlineSignal = AbortSignal.timeout(totalLimit);
|
|
31
|
+
const attempts = boundedInteger(maxAttempts, 3);
|
|
32
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
33
|
+
signal?.throwIfAborted();
|
|
34
|
+
deadlineSignal.throwIfAborted();
|
|
35
|
+
const remaining = deadline - now();
|
|
36
|
+
if (remaining <= 0) throw new DOMException("SQM request recovery deadline exceeded.", "TimeoutError");
|
|
37
|
+
const attemptSignal = AbortSignal.any([
|
|
38
|
+
deadlineSignal, AbortSignal.timeout(Math.min(requestLimit, remaining)),
|
|
39
|
+
...(signal ? [signal] : []),
|
|
40
|
+
]);
|
|
41
|
+
try {
|
|
42
|
+
const response = await withAbort(fetchImpl(url, { method: "GET", headers, signal: attemptSignal }), attemptSignal);
|
|
43
|
+
if (!TRANSIENT_STATUSES.has(response.status)) return response;
|
|
44
|
+
const error = new Error(`SQM Cloud returned HTTP ${response.status}.`);
|
|
45
|
+
error.status = response.status;
|
|
46
|
+
if (attempt === attempts) return response;
|
|
47
|
+
await response.body?.cancel().catch(() => {});
|
|
48
|
+
throw error;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (signal?.aborted) throw signal.reason;
|
|
51
|
+
const diagnostic = sqmDiagnostic(error, "knowledge_loading");
|
|
52
|
+
if (!diagnostic.retryable || attempt === attempts || deadlineSignal.aborted) throw error;
|
|
53
|
+
await onRecoveryEvent({ ...diagnostic, attempt, recovery: "retrying" });
|
|
54
|
+
const delay = Math.min(250 * 2 ** (attempt - 1), Math.max(0, deadline - now()));
|
|
55
|
+
if (delay <= 0) throw new DOMException("SQM request recovery deadline exceeded.", "TimeoutError");
|
|
56
|
+
await sleep(delay, AbortSignal.any([deadlineSignal, ...(signal ? [signal] : [])]));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function boundedInteger(value, maximum) {
|
|
62
|
+
return Number.isFinite(value) && value > 0 ? Math.min(maximum, Math.max(1, Math.floor(value))) : maximum;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function withAbort(promise, signal) {
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const abort = () => { signal.removeEventListener("abort", abort); reject(signal.reason); };
|
|
68
|
+
if (signal.aborted) abort();
|
|
69
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
70
|
+
Promise.resolve(promise).then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function abortableSleep(milliseconds, signal) {
|
|
75
|
+
signal.throwIfAborted();
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const finish = () => { signal.removeEventListener("abort", abort); resolve(); };
|
|
78
|
+
const timer = setTimeout(finish, milliseconds);
|
|
79
|
+
const abort = () => { clearTimeout(timer); signal.removeEventListener("abort", abort); reject(signal.reason); };
|
|
80
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
81
|
+
});
|
|
82
|
+
}
|
package/src/sqm/review-worker.js
CHANGED
|
@@ -3,6 +3,7 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
6
|
+
import { inspectProviderStatus } from "../desktop-status.js";
|
|
6
7
|
import { inspectDesktopExecutor } from "../desktop-executor.js";
|
|
7
8
|
import {
|
|
8
9
|
webApiErrorMessage,
|
|
@@ -88,7 +89,8 @@ export function createSqmLocalReviewWorker({
|
|
|
88
89
|
leaseSeconds = DEFAULT_LEASE_SECONDS,
|
|
89
90
|
heartbeatIntervalMs = Math.max(10_000, Math.floor(leaseSeconds * 500)),
|
|
90
91
|
getProvider = () => "codex",
|
|
91
|
-
runReview = (request, provider) => runStructuredProviderReview(provider, request),
|
|
92
|
+
runReview = (request, provider, options) => runStructuredProviderReview(provider, request, { ...options, beforeDispatch: options?.beforeLeaseDispatch }),
|
|
93
|
+
getEnvironmentKey = () => null,
|
|
92
94
|
beginWork = () => () => {},
|
|
93
95
|
onError = () => {},
|
|
94
96
|
onChange = () => {},
|
|
@@ -96,12 +98,30 @@ export function createSqmLocalReviewWorker({
|
|
|
96
98
|
getWorkspaceUrl = () => null,
|
|
97
99
|
recordHistory = async () => {},
|
|
98
100
|
onHistoryError = () => {},
|
|
101
|
+
now = () => Date.now(),
|
|
102
|
+
providerFailureCooldownMs = 5 * 60_000,
|
|
103
|
+
getRecoveryKey = () => null,
|
|
99
104
|
} = {}) {
|
|
100
105
|
if (!api || typeof api.request !== "function") throw new TypeError("api.request is required");
|
|
101
106
|
let activeJobId = null;
|
|
102
107
|
let pollPromise = null;
|
|
103
108
|
let closed = false;
|
|
104
|
-
let issue = null;
|
|
109
|
+
let issue = null, pausedEnvironment = null, pauseReason = null;
|
|
110
|
+
let issueContext = null;
|
|
111
|
+
const failureDetails = new WeakMap();
|
|
112
|
+
const pauses = new Map();
|
|
113
|
+
const contextKey = (provider, scope) => JSON.stringify([scope?.webOrigin, scope?.accountId, scope?.organizationId, provider]);
|
|
114
|
+
const currentPause = () => pauses.get(contextKey(getProvider(), getHistoryScope()));
|
|
115
|
+
const pauseForFailure = (error, provider, scope, recoveryKey) => {
|
|
116
|
+
const failure = failureDetails.get(error) || reviewFailure(error, "claim", provider);
|
|
117
|
+
if (!["provider_sign_in_required", "provider_model_unsupported", "codex_local_unavailable", "claude_local_unavailable", "desktop_access_required"].includes(failure.code) && ![401, 403].includes(failure.status)) return;
|
|
118
|
+
const key = contextKey(provider, scope);
|
|
119
|
+
const failures = Math.min((pauses.get(key)?.failures || 0) + 1, 4);
|
|
120
|
+
const delay = Math.min(30 * 60_000, Math.max(1000, providerFailureCooldownMs) * 2 ** (failures - 1));
|
|
121
|
+
pauses.set(key, { code: failure.code, stage: failure.stage, status: failure.status, failures, recoveryKey,
|
|
122
|
+
retryAt: now() + delay, message: `${formatFailure(failure)}\nNew SQM claims are paused. After fixing this cause, refresh the local AI connection or wait for the next recovery attempt.` });
|
|
123
|
+
if (pauses.size > 32) pauses.delete(pauses.keys().next().value);
|
|
124
|
+
};
|
|
105
125
|
const history = [];
|
|
106
126
|
const historyScopes = new WeakMap();
|
|
107
127
|
const visibleHistory = () => history.filter(item => historyScopes.get(item) === JSON.stringify(getHistoryScope()));
|
|
@@ -114,7 +134,7 @@ export function createSqmLocalReviewWorker({
|
|
|
114
134
|
item.state = state; item.finishedAt = new Date().toISOString();
|
|
115
135
|
item.result = state === "completed" ? "SQM review submitted." : "";
|
|
116
136
|
item.error = state === "failed" ? formatFailure(failure, failureReported) : "";
|
|
117
|
-
if (failure) { item.phase = failure.stage; item.errorCode = failure.code; }
|
|
137
|
+
if (failure) { item.phase = failure.stage; item.errorStage = failure.stage; item.errorCode = failure.code; item.httpStatus = failure.status; }
|
|
118
138
|
await persist(scope, item);
|
|
119
139
|
}
|
|
120
140
|
onChange();
|
|
@@ -130,11 +150,11 @@ export function createSqmLocalReviewWorker({
|
|
|
130
150
|
|
|
131
151
|
// Repair only a rejected model result, under the same live lease. Network,
|
|
132
152
|
// authorization, and persistence failures must not repeat provider execution.
|
|
133
|
-
const validatedReview = async (request, provider, code, submit, setStage) => {
|
|
153
|
+
const validatedReview = async (request, provider, code, submit, setStage, reviewOptions) => {
|
|
134
154
|
let nextRequest = request;
|
|
135
155
|
for (let attempt = 0; ; attempt += 1) {
|
|
136
156
|
setStage(false);
|
|
137
|
-
const review = await runReview(nextRequest, provider);
|
|
157
|
+
const review = await runReview(nextRequest, provider, reviewOptions);
|
|
138
158
|
setStage(true);
|
|
139
159
|
try {
|
|
140
160
|
return { review, response: await submit(review) };
|
|
@@ -162,26 +182,38 @@ export function createSqmLocalReviewWorker({
|
|
|
162
182
|
heartbeatTimer = setInterval(heartbeat, heartbeatIntervalMs);
|
|
163
183
|
heartbeatTimer.unref?.();
|
|
164
184
|
let stage = "analysis";
|
|
185
|
+
const reviewOptions = { providerReportingSupported: claim.capabilities?.reported_executor_provider === true,
|
|
186
|
+
beforeLeaseDispatch: () => api.request({ action: "heartbeat", ...leaseBody(claim, provider) }) };
|
|
187
|
+
const providerReport = (review, selectedProvider = provider) => claim.capabilities?.reported_executor_provider === true
|
|
188
|
+
? { reported_executor_provider: review.provider || selectedProvider } : {};
|
|
165
189
|
try {
|
|
166
190
|
const { review: analysis, response: advanced } = await validatedReview(claim.request, provider, "invalid_sqm_analysis_output", analysis => api.request({
|
|
167
191
|
action: "advance",
|
|
168
192
|
...leaseBody(claim, provider),
|
|
169
193
|
analysis_output: analysis.output,
|
|
170
194
|
executor_model: analysis.model,
|
|
171
|
-
|
|
195
|
+
...providerReport(analysis),
|
|
196
|
+
}), submitting => { stage = submitting ? "advance" : "analysis"; if (submitting) pauses.delete(contextKey(provider, historyScope)); }, reviewOptions);
|
|
197
|
+
const analysisProvider = analysis.provider || provider;
|
|
198
|
+
const item = history.find(item => item.id === claim.job_id);
|
|
199
|
+
if (item) item.provider = analysisProvider;
|
|
172
200
|
if (advanced.completed) { await updateTask(claim.job_id, "completed", historyScope); return advanced; }
|
|
173
201
|
stage = "adjudication";
|
|
174
|
-
const { response: completed } = await validatedReview(advanced.request,
|
|
202
|
+
const { review: adjudication, response: completed } = await validatedReview(advanced.request, analysisProvider, "invalid_sqm_adjudication_output", adjudication => api.request({
|
|
175
203
|
action: "complete",
|
|
176
204
|
...leaseBody(claim, provider),
|
|
177
205
|
analysis_output: analysis.output,
|
|
178
206
|
adjudication_output: adjudication.output,
|
|
179
207
|
executor_model: adjudication.model || analysis.model,
|
|
180
|
-
|
|
208
|
+
...providerReport(adjudication, analysisProvider),
|
|
209
|
+
...(claim.capabilities?.reported_executor_provider === true ? { analysis_executor_provider: analysisProvider, analysis_executor_model: analysis.model } : {}),
|
|
210
|
+
}), submitting => { stage = submitting ? "complete" : "adjudication"; if (submitting) pauses.delete(contextKey(provider, historyScope)); }, reviewOptions);
|
|
211
|
+
if (item) item.provider = adjudication.provider || analysisProvider;
|
|
181
212
|
await updateTask(claim.job_id, "completed", historyScope);
|
|
182
213
|
return completed;
|
|
183
214
|
} catch (error) {
|
|
184
215
|
const failure = reviewFailure(error, stage, provider);
|
|
216
|
+
if (error && typeof error === "object") failureDetails.set(error, failure);
|
|
185
217
|
let failureReported = error.failureRecorded === true;
|
|
186
218
|
if (!failureReported) {
|
|
187
219
|
try {
|
|
@@ -201,22 +233,33 @@ export function createSqmLocalReviewWorker({
|
|
|
201
233
|
|
|
202
234
|
return {
|
|
203
235
|
snapshot() {
|
|
204
|
-
|
|
236
|
+
const pause = currentPause();
|
|
237
|
+
return { active_job_id: activeJobId, polling: Boolean(pollPromise), closed, paused: pausedEnvironment !== null, pause_reason: pauseReason, issue: issueContext === contextKey(getProvider(), getHistoryScope()) ? issue : null,
|
|
238
|
+
...(pause ? { provider_pause: { code: pause.code, status: pause.status, retry_at: new Date(pause.retryAt).toISOString(), message: pause.message } } : {}) };
|
|
205
239
|
},
|
|
206
240
|
desktopTasks() { return { activeCount: activeJobId ? 1 : 0, items: visibleHistory().map(item => ({ ...item })) }; },
|
|
207
241
|
desktopTaskDetail(key) {
|
|
208
242
|
const item = visibleHistory().find(item => item.key === key);
|
|
209
243
|
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;
|
|
210
244
|
},
|
|
211
|
-
|
|
245
|
+
retry() { if (closed || pollPromise) return false; pausedEnvironment = null; pauseReason = null; pauses.delete(contextKey(getProvider(), getHistoryScope())); issue = null; onChange(); return true; },
|
|
246
|
+
resetHistory() { if (!pollPromise) { history.splice(0); issue = null; pausedEnvironment = null; pauseReason = null; } },
|
|
212
247
|
poll() {
|
|
213
248
|
if (closed || pollPromise) return pollPromise || Promise.resolve(null);
|
|
249
|
+
const environmentKey = getEnvironmentKey();
|
|
250
|
+
if (pausedEnvironment !== null && pausedEnvironment === environmentKey) return Promise.resolve(null);
|
|
251
|
+
if (pausedEnvironment !== null) { pausedEnvironment = null; pauseReason = null; pauses.delete(contextKey(getProvider(), getHistoryScope())); issue = null; }
|
|
214
252
|
const provider = getProvider();
|
|
215
253
|
if (provider == null) return Promise.resolve(null);
|
|
216
254
|
assertProvider(provider);
|
|
255
|
+
const historyScope = getHistoryScope();
|
|
256
|
+
const key = contextKey(provider, historyScope);
|
|
257
|
+
const recoveryKey = getRecoveryKey(provider);
|
|
258
|
+
const pause = pauses.get(key);
|
|
259
|
+
if (pause && pause.recoveryKey === recoveryKey && now() < pause.retryAt) return Promise.resolve(null);
|
|
260
|
+
if (pause && pause.recoveryKey !== recoveryKey) pauses.delete(key);
|
|
217
261
|
const releaseWork = beginWork?.();
|
|
218
262
|
if (!releaseWork) return Promise.resolve(null);
|
|
219
|
-
const historyScope = getHistoryScope();
|
|
220
263
|
const workspaceUrl = getWorkspaceUrl();
|
|
221
264
|
pollPromise = (async () => {
|
|
222
265
|
const claim = await api.request({
|
|
@@ -226,10 +269,18 @@ export function createSqmLocalReviewWorker({
|
|
|
226
269
|
lease_seconds: leaseSeconds,
|
|
227
270
|
});
|
|
228
271
|
if (issue) { issue = null; onChange(); }
|
|
229
|
-
if (!claim?.claimed)
|
|
230
|
-
|
|
272
|
+
if (!claim?.claimed) {
|
|
273
|
+
// A successful empty claim proves SIMY access recovered, but it says
|
|
274
|
+
// nothing about a local provider that has not executed again yet.
|
|
275
|
+
if (pause?.stage === "claim") pauses.delete(key);
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
const result = await processClaim(claim, provider, historyScope, workspaceUrl);
|
|
279
|
+
pauses.delete(key);
|
|
280
|
+
return result;
|
|
231
281
|
})()
|
|
232
282
|
.catch(async (error) => {
|
|
283
|
+
pauseForFailure(error, provider, historyScope, recoveryKey);
|
|
233
284
|
if (error.jobId && /^[a-f0-9-]{36}$/i.test(error.jobId) && !history.some(item => item.id === error.jobId)) {
|
|
234
285
|
const scope = historyScope;
|
|
235
286
|
const failure = reviewFailure(error, "claim", provider);
|
|
@@ -239,6 +290,10 @@ export function createSqmLocalReviewWorker({
|
|
|
239
290
|
history.unshift(item); historyScopes.set(item, JSON.stringify(scope)); history.splice(50);
|
|
240
291
|
await persist(scope, item);
|
|
241
292
|
}
|
|
293
|
+
if (["provider_model_unsupported", "codex_local_unavailable", "claude_local_unavailable", "provider_sign_in_required", "provider_status_unknown"].includes(error.code)) {
|
|
294
|
+
pausedEnvironment = environmentKey; pauseReason = error.code;
|
|
295
|
+
}
|
|
296
|
+
issueContext = key;
|
|
242
297
|
issue = error.status === 403 ? "forbidden" : error.status === 401 ? "signed_out" : "unavailable";
|
|
243
298
|
onError(error);
|
|
244
299
|
return null;
|
|
@@ -261,7 +316,33 @@ export function runStructuredCodexReview(request, options) {
|
|
|
261
316
|
return runStructuredProviderReview("codex", request, options);
|
|
262
317
|
}
|
|
263
318
|
|
|
264
|
-
export async function runStructuredProviderReview(
|
|
319
|
+
export async function runStructuredProviderReview(provider, request, options = {}) {
|
|
320
|
+
const fallbackProviders = options.enableEngineFallback === true && options.providerReportingSupported === true && options.readOnlyAnalysis !== false && Array.isArray(options.allowedFallbackProviders)
|
|
321
|
+
? [...new Set(options.allowedFallbackProviders.filter(candidate => ["codex", "claude"].includes(candidate) && candidate !== provider))] : [];
|
|
322
|
+
if (!fallbackProviders.length) return runSingleProviderReview(provider, request, options);
|
|
323
|
+
const deadlineAt = Date.now() + (options.executionTimeoutMs ?? DEFAULT_EXECUTION_TIMEOUT_MS);
|
|
324
|
+
const attempts = [];
|
|
325
|
+
for (const candidate of [provider, ...fallbackProviders]) {
|
|
326
|
+
if (options.signal?.aborted) throw new SqmLocalReviewError("Local analysis was cancelled.", { code: "analysis_cancelled" });
|
|
327
|
+
if (Date.now() >= deadlineAt) throw new SqmLocalReviewError("Local analysis timed out.", { code: "analysis_timeout" });
|
|
328
|
+
if (candidate !== provider) {
|
|
329
|
+
await options.beforeDispatch?.(candidate);
|
|
330
|
+
const readiness = await (options.inspectProviderStatus || inspectProviderStatus)(candidate);
|
|
331
|
+
if (readiness.state !== "ready") throw new SqmLocalReviewError("Fallback provider is not signed in and ready.", {
|
|
332
|
+
code: readiness.state === "signed_out" ? "provider_sign_in_required" : "provider_status_unknown" });
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
const result = await runSingleProviderReview(candidate, request, { ...options, deadlineAt,
|
|
336
|
+
beforeDispatch: () => options.beforeDispatch?.(candidate) });
|
|
337
|
+
return { ...result, provider: candidate, provider_attempts: [...attempts, { provider: candidate, code: "completed" }] };
|
|
338
|
+
} catch (error) {
|
|
339
|
+
attempts.push({ provider: candidate, code: error.code });
|
|
340
|
+
if (!["provider_model_unsupported", "codex_local_unavailable", "claude_local_unavailable"].includes(error.code) || candidate === fallbackProviders.at(-1)) throw error;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function runSingleProviderReview(
|
|
265
346
|
provider,
|
|
266
347
|
request,
|
|
267
348
|
{
|
|
@@ -273,6 +354,7 @@ export async function runStructuredProviderReview(
|
|
|
273
354
|
readOnlyAnalysis = true,
|
|
274
355
|
terminationGraceMs = 1_000,
|
|
275
356
|
beforeDispatch = async () => {},
|
|
357
|
+
deadlineAt,
|
|
276
358
|
} = {},
|
|
277
359
|
) {
|
|
278
360
|
assertProvider(provider);
|
|
@@ -295,7 +377,7 @@ export async function runStructuredProviderReview(
|
|
|
295
377
|
"Return only JSON that strictly matches the provided output schema.",
|
|
296
378
|
request.prompt,
|
|
297
379
|
].join("\n\n");
|
|
298
|
-
const deadline = Date.now() + executionTimeoutMs;
|
|
380
|
+
const deadline = deadlineAt ?? Date.now() + executionTimeoutMs;
|
|
299
381
|
const models = provider === "codex" ? SQM_CODEX_MODELS : [null];
|
|
300
382
|
let stdout;
|
|
301
383
|
let selectedModel;
|
package/src/sqm/session.js
CHANGED
|
@@ -18,10 +18,18 @@ export async function ensureSqmSession(
|
|
|
18
18
|
startAgentImpl = startAgent,
|
|
19
19
|
openAuthorizationUrlImpl = openAuthorizationUrl,
|
|
20
20
|
createAgentShutdownImpl = createAgentShutdown,
|
|
21
|
+
expectedScope,
|
|
22
|
+
signal,
|
|
23
|
+
rejectedToken,
|
|
21
24
|
} = {},
|
|
22
25
|
) {
|
|
23
26
|
let session = await readSessionImpl(webOrigin);
|
|
24
|
-
|
|
27
|
+
const pinnedScope = expectedScope || sqmSessionScope(session, webOrigin);
|
|
28
|
+
signal?.throwIfAborted();
|
|
29
|
+
if (session?.token !== rejectedToken && sqmSessionReady(session, webOrigin, now(), isSessionValidImpl)) {
|
|
30
|
+
assertSqmSessionScope(session, webOrigin, pinnedScope);
|
|
31
|
+
return session;
|
|
32
|
+
}
|
|
25
33
|
|
|
26
34
|
const agent = await startAgentImpl({ requestedPort: 0, webOrigin, quiet: true });
|
|
27
35
|
const shutdown = createAgentShutdownImpl(agent);
|
|
@@ -35,19 +43,41 @@ export async function ensureSqmSession(
|
|
|
35
43
|
|
|
36
44
|
const deadline = now() + timeoutMs;
|
|
37
45
|
while (now() < deadline) {
|
|
46
|
+
signal?.throwIfAborted();
|
|
38
47
|
await sleep(Math.min(500, Math.max(1, deadline - now())));
|
|
39
48
|
session = await readSessionImpl(webOrigin);
|
|
40
|
-
if (sqmSessionReady(session, webOrigin, now(), isSessionValidImpl)) {
|
|
49
|
+
if (session?.token !== rejectedToken && sqmSessionReady(session, webOrigin, now(), isSessionValidImpl)) {
|
|
50
|
+
assertSqmSessionScope(session, webOrigin, pinnedScope);
|
|
41
51
|
logger.log("SIMY CLI authorization completed. Continuing SQM check.");
|
|
42
52
|
return session;
|
|
43
53
|
}
|
|
44
54
|
}
|
|
45
|
-
|
|
55
|
+
const error = new Error("SIMY authorization timed out. Complete the browser sign-in and rerun the SQM command.");
|
|
56
|
+
error.code = "authorization_timeout";
|
|
57
|
+
throw error;
|
|
46
58
|
} finally {
|
|
47
59
|
await shutdown();
|
|
48
60
|
}
|
|
49
61
|
}
|
|
50
62
|
|
|
63
|
+
export function sqmSessionScope(session, webOrigin) {
|
|
64
|
+
return { account_id: session?.account_id || session?.auth_user_id || null,
|
|
65
|
+
organization_id: session?.organization_id || session?.org_id || null,
|
|
66
|
+
device_id: session?.device_id || null, api_base_url: session?.api_base_url || null,
|
|
67
|
+
web_origin: new URL(webOrigin).origin };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function assertSqmSessionScope(session, webOrigin, expected) {
|
|
71
|
+
const current = sqmSessionScope(session, webOrigin);
|
|
72
|
+
for (const field of ["account_id", "organization_id", "device_id", "api_base_url", "web_origin"]) {
|
|
73
|
+
if (expected?.[field] && expected[field] !== current[field]) {
|
|
74
|
+
const error = new Error(`SQM authorization changed its ${field}; resume the original authorized scope.`);
|
|
75
|
+
error.code = "authorization_scope_changed";
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
51
81
|
function sqmSessionReady(session, webOrigin, now, isSessionValidImpl) {
|
|
52
82
|
return Boolean(
|
|
53
83
|
isSessionValidImpl(session, now, webOrigin) &&
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createCodingLoopSnapshot } from "./orchestrator/contract.js";
|
|
2
|
+
import { validateCharter } from "./orchestrator/charter-validation.js";
|
|
3
|
+
|
|
4
|
+
// Reuse the execution charter rather than inventing a second approval policy.
|
|
5
|
+
// These checks inspect local metadata only; model access requires real dispatch.
|
|
6
|
+
export function startupRequirementChecks(request = {}) {
|
|
7
|
+
const checks = [{ key: "model_access", status: "unknown", blocking: false,
|
|
8
|
+
summary: "Signing in does not prove access to the requested model. Access is checked on dispatch without a paid preflight request.",
|
|
9
|
+
action: { id: "review_model_access", type: "inspect_provider", label: "Review provider and model settings" } }];
|
|
10
|
+
if (!String(request.requirement || "").trim()) {
|
|
11
|
+
checks.push({ key: "task_requirements", status: "unknown", blocking: false,
|
|
12
|
+
summary: "Environment check only. Supply the task requirement to check acceptance criteria and design approval.",
|
|
13
|
+
action: { id: "provide_requirement", type: "edit_requirements", label: "Provide task requirements" } });
|
|
14
|
+
return checks;
|
|
15
|
+
}
|
|
16
|
+
const { charter } = createCodingLoopSnapshot({ runId: "preflight", request });
|
|
17
|
+
const findings = validateCharter(charter);
|
|
18
|
+
for (const [key, code, passed, actionId, label] of [
|
|
19
|
+
["acceptance_criteria", "EXPLICIT_ACCEPTANCE_CRITERIA_REQUIRED", "Acceptance criteria satisfy the task risk policy.", "provide_acceptance_criteria", "Provide explicit acceptance criteria"],
|
|
20
|
+
["design_review", "DESIGN_REVIEW_REQUIRED", "Design approval satisfies the task risk policy.", "approve_design", "Provide design review and approval evidence"],
|
|
21
|
+
["required_checks", "REQUIRED_CI_CHECKS_MISSING", "Required checks satisfy the task risk policy.", "provide_required_checks", "Provide CI and security check names"],
|
|
22
|
+
]) {
|
|
23
|
+
const finding = findings.find(item => item.code === code);
|
|
24
|
+
checks.push({ key, status: finding ? "failed" : "passed", blocking: Boolean(finding),
|
|
25
|
+
summary: finding?.explanation || passed,
|
|
26
|
+
...(finding ? { code, action: { id: actionId, type: "edit_requirements", label } } : {}) });
|
|
27
|
+
}
|
|
28
|
+
checks.push({ key: "assurance_selection", status: "unknown", blocking: false,
|
|
29
|
+
summary: "The executor must verify the five-level comparison and user selection in existing decision evidence before material implementation.",
|
|
30
|
+
action: { id: "review_assurance_selection", type: "review_decision_evidence", label: "Review assurance level and selection evidence" } });
|
|
31
|
+
checks.push({ key: "external_dependencies", status: "unknown", blocking: false,
|
|
32
|
+
summary: "Confirm test accounts, external resources and permissions required by the acceptance criteria. Local readiness does not certify Cloud entitlement.",
|
|
33
|
+
action: { id: "confirm_external_dependencies", type: "edit_requirements", label: "Confirm external inputs and permissions" } });
|
|
34
|
+
return checks;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function summarizeStartupChecks(checks) {
|
|
38
|
+
const blockers = checks.filter(check => check.status === "failed" && check.blocking !== false);
|
|
39
|
+
const unknown = checks.filter(check => check.status === "unknown");
|
|
40
|
+
return { status: blockers.length ? "blocked" : unknown.length ? "needs_confirmation" : "ready",
|
|
41
|
+
blocker_keys: blockers.map(check => check.key), unknown_keys: unknown.map(check => check.key),
|
|
42
|
+
actions: checks.flatMap(check => check.action ? [{ check_key: check.key, ...check.action }]
|
|
43
|
+
: (check.recovery?.actions || []).map(action => ({ check_key: check.key, ...action }))) };
|
|
44
|
+
}
|