@awak-app/simy-cli 0.5.47 → 0.5.48
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
CHANGED
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.48";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function withCliContract(
|
|
@@ -315,13 +315,16 @@ export async function readCodexConversation(candidate, { openClient = openCodexA
|
|
|
315
315
|
for (const [index, turn] of settled.toReversed().entries()) {
|
|
316
316
|
if (turn.itemsView === 'notLoaded') { truncated = true; if (turn === boundary) incomplete = true; continue; }
|
|
317
317
|
if (turn.itemsView !== 'summary') throw new CodexAppServerError('context_unavailable', 'Codex did not return the requested conversation summaries.');
|
|
318
|
-
if (!Array.isArray(turn?.items)) { incomplete = true; continue; }
|
|
318
|
+
if (!Array.isArray(turn?.items)) { if (turn === boundary) incomplete = true; continue; }
|
|
319
319
|
if (turn === boundary && !turn.items.length) incomplete = true;
|
|
320
320
|
for (const [itemIndex, item] of turn.items.entries()) {
|
|
321
321
|
const role = item?.type === 'userMessage' ? 'user' : item?.type === 'agentMessage' ? 'assistant' : null;
|
|
322
322
|
if (!role) continue;
|
|
323
323
|
const text = role === 'user' ? textContent(item.content) : item.text;
|
|
324
|
-
|
|
324
|
+
// An older interrupted/empty answer is an omission in the display
|
|
325
|
+
// history, not a missing latest exchange. Retain human corrections;
|
|
326
|
+
// the classifier must still establish a self-contained current goal.
|
|
327
|
+
if (typeof text !== 'string' || !text.trim()) { if (turn === boundary) incomplete = true; continue; }
|
|
325
328
|
messages.push({ role, turnRef: `${turn.id || index}:${item.id || itemIndex}`, text });
|
|
326
329
|
}
|
|
327
330
|
}
|
|
@@ -2,9 +2,10 @@ import { readCodexConversation } from './codex-app-server.js';
|
|
|
2
2
|
import { readClaudeConversation } from './claude-sessions.js';
|
|
3
3
|
import { boundConversation, boundProviderGoal, sanitizedText } from './conversation-context.js';
|
|
4
4
|
import { runStructuredProviderReview } from '../sqm/review-worker.js';
|
|
5
|
+
import { inspectProviderStatus } from '../desktop-status.js';
|
|
5
6
|
import { createHash } from 'node:crypto';
|
|
6
7
|
|
|
7
|
-
export const CONTINUATION_ANALYSIS_VERSION =
|
|
8
|
+
export const CONTINUATION_ANALYSIS_VERSION = 3;
|
|
8
9
|
const BLOCKER_TYPES = ['approval', 'missing_information', 'external_dependency', 'execution_failure'];
|
|
9
10
|
const STATES = ['continue', 'no_action', 'needs_user', 'waiting', 'uncertain'];
|
|
10
11
|
const string = maximum => ({ type: 'string', maxLength: maximum });
|
|
@@ -50,8 +51,22 @@ Older history may be omitted. Set assessment.contextSufficient=true only if the
|
|
|
50
51
|
Return blocker=null unless concrete unfinished original work is currently prevented by a specific unresolved approval, missing_information, external_dependency or execution_failure. State and needsUser/externalWait flags alone are not blockers. Running/status-only waiting, future reminders, voluntary pauses, budget limits, uncertainty, optional follow-ups and resolved historical failures must have blocker=null. A recoverable failure with a feasible next step is not a blocker. A blocker never changes continuation authority. Its reason must describe the actual obstacle concisely; basis must quote exact original conversation excerpts that establish the obstacle, checking all later supplied messages to establish that it is still current. Evidence may originate in an earlier turn when no later message resolves the obstacle. Goal metadata alone cannot establish a blocker. Later authorization, supplied information, resolution or completion clears older blockers. For execution_failure, require explicit evidence that the failure prevents further useful progress, not simply that a command failed.
|
|
51
52
|
Return concise reasons and exact short supporting excerpts, each tied to the role and turnRef provided. Excerpts must occur verbatim in the supplied message. Do not reproduce secrets, credentials or large private passages. Set nextAction=null except for continue. Populate assessment honestly; nextActionKind names the concrete action, not merely a plan to act.`;
|
|
52
53
|
|
|
53
|
-
function uncertain(reason) {
|
|
54
|
-
return { state: 'uncertain', reason, objective: '', remaining: [], nextAction: null, basis: [], blocker: null, analysisVersion: CONTINUATION_ANALYSIS_VERSION };
|
|
54
|
+
function uncertain(reason, reasonCode) {
|
|
55
|
+
return { state: 'uncertain', reason, ...(reasonCode ? { reasonCode } : {}), objective: '', remaining: [], nextAction: null, basis: [], blocker: null, analysisVersion: CONTINUATION_ANALYSIS_VERSION };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function unavailableAnalysis(candidate, error, inspectProvider, signal) {
|
|
59
|
+
if (error?.code === 'analysis_cancelled') return uncertain('Conversation analysis was cancelled.', 'cancelled');
|
|
60
|
+
if (error?.code === 'analysis_timeout') return uncertain('The local AI app did not finish checking this turn in time. Retry when it is available.', 'timeout');
|
|
61
|
+
if (error?.code === `${candidate.provider}_local_unavailable`) return uncertain('The local AI app is unavailable. Open or update it, then retry.', 'unavailable');
|
|
62
|
+
let status;
|
|
63
|
+
try { status = await inspectProvider(candidate.provider); } catch { /* Unknown is never proof of sign-out. */ }
|
|
64
|
+
if (signal?.aborted) return uncertain('Conversation analysis was cancelled.', 'cancelled');
|
|
65
|
+
if (error?.code === 'provider_sign_in_required' || status?.state === 'signed_out') {
|
|
66
|
+
const label = candidate.provider === 'claude' ? 'Claude Code' : 'Codex';
|
|
67
|
+
return uncertain(`Sign in to ${label} to check this conversation and continue its unfinished work. After signing in, retry this check.`, 'provider_sign_in_required');
|
|
68
|
+
}
|
|
69
|
+
return uncertain('The local AI app could not check this turn. Check its connection, then retry.', 'analysis_unavailable');
|
|
55
70
|
}
|
|
56
71
|
|
|
57
72
|
function unreadableContext(error) {
|
|
@@ -143,7 +158,7 @@ export async function readContinuationContext(candidate, { signal, readContext,
|
|
|
143
158
|
|
|
144
159
|
export async function analyzeContinuation(candidate, {
|
|
145
160
|
signal, readContext, openCodexClient, claudeHome, previousAnalysis,
|
|
146
|
-
runProvider = runStructuredProviderReview, providerOptions = {},
|
|
161
|
+
runProvider = runStructuredProviderReview, providerOptions = {}, inspectProvider = inspectProviderStatus,
|
|
147
162
|
} = {}) {
|
|
148
163
|
if (signal?.aborted) return uncertain('Conversation analysis was cancelled.');
|
|
149
164
|
if (!['codex', 'claude'].includes(candidate?.provider) || typeof candidate?.session !== 'string' || typeof candidate?.cwd !== 'string') {
|
|
@@ -172,18 +187,26 @@ export async function analyzeContinuation(candidate, {
|
|
|
172
187
|
if (previousAnalysis?.analysisVersion === CONTINUATION_ANALYSIS_VERSION && previousAnalysis?.contextDigest === context.contextDigest && STATES.includes(previousAnalysis.state)) {
|
|
173
188
|
return { ...snapshot, ...previousAnalysis };
|
|
174
189
|
}
|
|
175
|
-
if (context.incomplete || !context.messages.length) return uncertain('The latest
|
|
190
|
+
if (context.incomplete || !context.messages.length) return uncertain('The latest request or reply could not be read completely. Open the original conversation to review it, then retry.', 'context_incomplete');
|
|
191
|
+
let result;
|
|
176
192
|
try {
|
|
177
|
-
|
|
193
|
+
result = await runProvider(candidate.provider, {
|
|
178
194
|
system: SYSTEM, prompt: `Classify this conversation evidence only (older history omitted: ${context.truncated}):\n${JSON.stringify(context.providerGoal ? { originalProviderGoal: context.providerGoal, messages: context.messages } : context.messages)}`, output_schema: OUTPUT_SCHEMA,
|
|
179
195
|
}, { ...providerOptions, signal, readOnlyAnalysis: true });
|
|
180
|
-
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (signal?.aborted) return uncertain('Conversation analysis was cancelled.', 'cancelled');
|
|
198
|
+
// Only a known machine code or the provider's own login probe can
|
|
199
|
+
// establish sign-out. SIMY account login is unrelated to local analysis.
|
|
200
|
+
return unavailableAnalysis(candidate, error, inspectProvider, signal);
|
|
201
|
+
}
|
|
202
|
+
if (signal?.aborted) return uncertain('Conversation analysis was cancelled.');
|
|
203
|
+
try {
|
|
181
204
|
const analysis = validateContinuationAnalysis(result?.output, context);
|
|
182
205
|
const model = typeof result?.model === 'string' && /^[a-zA-Z0-9._/-]{1,120}$/.test(result.model) ? result.model : null;
|
|
183
206
|
return { ...snapshot, ...analysis, ...(model ? { model } : {}) };
|
|
184
207
|
} catch {
|
|
185
208
|
// Provider stderr/errors may contain transcript text or credentials. Never
|
|
186
209
|
// expose them to the desktop, cache, diagnostics or control plane.
|
|
187
|
-
return uncertain('
|
|
210
|
+
return uncertain('The AI app returned a check that could not be verified. Retry this check or review the original conversation.', 'analysis_invalid_result');
|
|
188
211
|
}
|
|
189
212
|
}
|
package/src/sqm/review-worker.js
CHANGED
|
@@ -260,6 +260,9 @@ export async function runStructuredProviderReview(
|
|
|
260
260
|
);
|
|
261
261
|
const envelope = provider === "claude" ? JSON.parse(stdout) : null;
|
|
262
262
|
if (envelope?.is_error || (envelope && envelope.subtype !== "success")) {
|
|
263
|
+
if (readOnlyAnalysis && envelope?.is_error === true && providerSignInRequired(envelope.result)) {
|
|
264
|
+
throw new SqmLocalReviewError('Sign in to the local AI app again.', { code: 'provider_sign_in_required' });
|
|
265
|
+
}
|
|
263
266
|
throw new Error("Claude Code did not complete a structured review");
|
|
264
267
|
}
|
|
265
268
|
const output = provider === "claude"
|
|
@@ -322,7 +325,7 @@ function spawnReview(command, args, prompt, { spawnImpl, executionTimeoutMs, lab
|
|
|
322
325
|
};
|
|
323
326
|
const cancelled = () => halt(new Error('Local analysis was cancelled'));
|
|
324
327
|
const timer = setTimeout(() => {
|
|
325
|
-
halt(new Error(`${label} timed out after ${executionTimeoutMs}ms`));
|
|
328
|
+
halt(readOnlyAnalysis ? new SqmLocalReviewError('Local analysis timed out.', { code: 'analysis_timeout' }) : new Error(`${label} timed out after ${executionTimeoutMs}ms`));
|
|
326
329
|
}, executionTimeoutMs);
|
|
327
330
|
timer.unref?.();
|
|
328
331
|
const finish = (error = null) => {
|
|
@@ -341,6 +344,10 @@ function spawnReview(command, args, prompt, { spawnImpl, executionTimeoutMs, lab
|
|
|
341
344
|
for (const line of stdout.split('\n').filter(Boolean)) {
|
|
342
345
|
let event;
|
|
343
346
|
try { event = JSON.parse(line); } catch { continue; }
|
|
347
|
+
if (['error', 'turn.failed'].includes(event.type) && providerSignInRequired(event.message ?? event.error?.message)) {
|
|
348
|
+
halt(new SqmLocalReviewError('Sign in to the local AI app again.', { code: 'provider_sign_in_required' }));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
344
351
|
if (/command_execution|mcp_tool_call|web_search|file_change|tool_call/.test(event.item?.type || '')) {
|
|
345
352
|
halt(new Error('Read-only analysis attempted to use a tool'));
|
|
346
353
|
return;
|
|
@@ -358,6 +365,14 @@ function spawnReview(command, args, prompt, { spawnImpl, executionTimeoutMs, lab
|
|
|
358
365
|
});
|
|
359
366
|
child.on("error", finish);
|
|
360
367
|
child.on("close", (code) => {
|
|
368
|
+
if (readOnlyAnalysis && provider === 'claude' && !terminalError) {
|
|
369
|
+
let envelope;
|
|
370
|
+
try { envelope = JSON.parse(stdout); } catch { /* Non-JSON failure remains unavailable. */ }
|
|
371
|
+
if (envelope?.is_error === true && providerSignInRequired(envelope.result)) {
|
|
372
|
+
finish(new SqmLocalReviewError('Sign in to the local AI app again.', { code: 'provider_sign_in_required' }));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
361
376
|
finish(terminalError || (code === 0 ? null : new Error(stderr.trim() || `${label} exited with code ${code}`)));
|
|
362
377
|
});
|
|
363
378
|
signal?.addEventListener('abort', cancelled, { once: true });
|
|
@@ -366,6 +381,12 @@ function spawnReview(command, args, prompt, { spawnImpl, executionTimeoutMs, lab
|
|
|
366
381
|
});
|
|
367
382
|
}
|
|
368
383
|
|
|
384
|
+
function providerSignInRequired(message) {
|
|
385
|
+
// Match explicit provider authentication failures only, never general 403s
|
|
386
|
+
// (which can be permissions/quotas) or model-generated prose/tool output.
|
|
387
|
+
return typeof message === 'string' && /(?:not logged in|please (?:log|sign) in|invalid api key|authentication token (?:is )?(?:invalid|expired)|refresh token (?:has )?expired)/i.test(message.slice(0, 4000));
|
|
388
|
+
}
|
|
389
|
+
|
|
369
390
|
function safeReviewEnvironment(environment, provider) {
|
|
370
391
|
const allowed = [
|
|
371
392
|
"HOME", "PATH", "CODEX_HOME", "CLAUDE_CONFIG_DIR",
|