@awak-app/simy-cli 0.6.9 → 0.6.10
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/cli-contract.js +1 -1
- package/src/session-supervision/analysis-manager.js +36 -9
- package/src/session-supervision/codex-app-server.js +1 -0
- package/src/session-supervision/continuation-analysis.js +3 -3
- package/src/session-supervision/conversation-context.js +7 -0
- package/src/session-supervision/index.js +1 -0
- package/src/sqm/provider-diagnostics.js +1 -1
- package/src/sqm/review-worker.js +2 -1
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.6.
|
|
16
|
+
export const CLI_VERSION = "0.6.10";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function withCliContract(
|
|
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { lstat, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { analyzeContinuation, CONTINUATION_ANALYSIS_VERSION } from './continuation-analysis.js';
|
|
5
|
+
import { providerGoalEvidence } from './conversation-context.js';
|
|
5
6
|
|
|
6
7
|
const BACKFILL_LIMIT = 10;
|
|
7
8
|
const MAX_CACHE_ENTRIES = 2000;
|
|
@@ -9,12 +10,17 @@ const AUTH_RECOVERY_COOLDOWN_MS = 60_000;
|
|
|
9
10
|
const RESULT_STATES = new Set(['continue', 'no_action', 'needs_user', 'waiting', 'uncertain']);
|
|
10
11
|
const keyOf = candidate => `${candidate.provider}:${candidate.session}`;
|
|
11
12
|
const activityOf = candidate => Number.isFinite(candidate.lastActivityAt) ? candidate.lastActivityAt : 0;
|
|
12
|
-
const
|
|
13
|
-
?
|
|
14
|
-
|
|
13
|
+
const turnOf = candidate => candidate.provider === 'codex' && typeof candidate.latestTurnId === 'string' && candidate.latestTurnId
|
|
14
|
+
? candidate.latestTurnId : null;
|
|
15
|
+
const goalOf = candidate => providerGoalEvidence(candidate.providerGoal);
|
|
16
|
+
const legacyRevision = candidate => createHash('sha256').update(JSON.stringify([
|
|
17
|
+
candidate.provider, candidate.session, candidate.cwd, activityOf(candidate), ...(candidate.isRunning === true ? ['running'] : []),
|
|
18
|
+
...(goalOf(candidate) === null ? [] : ['goal', [candidate.providerGoal.objective ?? null, candidate.providerGoal.status ?? null,
|
|
19
|
+
candidate.providerGoal.updatedAt ?? null, candidate.providerGoal.textTruncated === true]]),
|
|
20
|
+
])).digest('hex');
|
|
15
21
|
const goalRevisionOf = candidate => goalOf(candidate) === null ? null : createHash('sha256').update(JSON.stringify(goalOf(candidate))).digest('hex');
|
|
16
22
|
export const continuationAnalysisRevision = candidate => createHash('sha256')
|
|
17
|
-
.update(JSON.stringify([candidate.provider, candidate.session, candidate.cwd, activityOf(candidate),
|
|
23
|
+
.update(JSON.stringify([candidate.provider, candidate.session, candidate.cwd, turnOf(candidate) ? ['turn', turnOf(candidate)] : activityOf(candidate),
|
|
18
24
|
...(candidate.isRunning === true ? ['running'] : []), ...(goalOf(candidate) === null ? [] : ['goal', goalOf(candidate)])]))
|
|
19
25
|
.digest('hex');
|
|
20
26
|
const eligible = candidate => candidate.capabilities?.canAnalyze === true &&
|
|
@@ -50,6 +56,14 @@ export async function createContinuationAnalysisManager({ root, analyze = analyz
|
|
|
50
56
|
const known = new Map(document.known.slice(-MAX_CACHE_ENTRIES));
|
|
51
57
|
const entries = new Map(document.entries.slice(-MAX_CACHE_ENTRIES));
|
|
52
58
|
const candidates = new Map();
|
|
59
|
+
const observedCandidate = candidate => {
|
|
60
|
+
const previous = known.get(keyOf(candidate));
|
|
61
|
+
// A bounded discovery may skip this turn probe. Preserve its prior ID
|
|
62
|
+
// only while identity and activity still match; changed activity must be read.
|
|
63
|
+
return !turnOf(candidate) && candidate.provider === 'codex' && previous?.latestTurnId &&
|
|
64
|
+
previous.cwd === candidate.cwd && previous.activity === activityOf(candidate)
|
|
65
|
+
? { ...candidate, latestTurnId: previous.latestTurnId } : candidate;
|
|
66
|
+
};
|
|
53
67
|
const providerStatuses = new Map(), signedOutChecks = new Map();
|
|
54
68
|
for (const [key, entry] of entries) {
|
|
55
69
|
if (entry.result?.state === 'analyzing') entry.result = { state: 'queued' };
|
|
@@ -231,7 +245,7 @@ export async function createContinuationAnalysisManager({ root, analyze = analyz
|
|
|
231
245
|
await exclusive(async () => {
|
|
232
246
|
if (closed || suspended) return;
|
|
233
247
|
candidates.clear();
|
|
234
|
-
for (const candidate of sessions) candidates.set(keyOf(candidate), candidate);
|
|
248
|
+
for (const candidate of sessions) candidates.set(keyOf(candidate), observedCandidate(candidate));
|
|
235
249
|
const sorted = [...candidates.values()].sort((a, b) => activityOf(b) - activityOf(a) || keyOf(a).localeCompare(keyOf(b)));
|
|
236
250
|
const backfill = document.initialized ? new Set() : new Set(sorted.slice(0, BACKFILL_LIMIT).map(keyOf));
|
|
237
251
|
let dirty = !document.initialized;
|
|
@@ -242,10 +256,19 @@ export async function createContinuationAnalysisManager({ root, analyze = analyz
|
|
|
242
256
|
// Old cache formats did not observe goal/tracking metadata. Establish
|
|
243
257
|
// their baseline once, rather than mistaking the upgrade for activity.
|
|
244
258
|
const goalObserved = candidate.goalReadState !== 'unavailable';
|
|
245
|
-
const goalChanged = previous && Object.hasOwn(previous, 'goalRevision') && previous.goalObserved !== false && goalObserved && previous.goalRevision !== goalRevision;
|
|
259
|
+
const goalChanged = previous && previous.goalEvidenceVersion === 1 && Object.hasOwn(previous, 'goalRevision') && previous.goalObserved !== false && goalObserved && previous.goalRevision !== goalRevision;
|
|
246
260
|
const trackingEnabled = candidate.tracked === true && (!previous || Object.hasOwn(previous, 'tracked') && previous.tracked !== true);
|
|
247
|
-
|
|
261
|
+
// A known turn boundary is stronger than updated_at: tool/status
|
|
262
|
+
// events in that same turn must not enqueue or cancel paid inference.
|
|
263
|
+
const newActivity = document.initialized && (previous ?
|
|
264
|
+
(turnOf(candidate) && previous.latestTurnId ? turnOf(candidate) !== previous.latestTurnId : active > previous.activity) ||
|
|
265
|
+
(candidate.isRunning === true) !== (previous.isRunning === true) : active > document.baselineAt);
|
|
248
266
|
const entry = entries.get(key);
|
|
267
|
+
// Rebase unchanged old cache keys once without rerunning historical
|
|
268
|
+
// inference when semantic goal/turn keys become available.
|
|
269
|
+
if (entry && entry.revision !== revision && entry.cwd === candidate.cwd && entry.revision === legacyRevision(candidate)) {
|
|
270
|
+
entry.revision = revision; dirty = true;
|
|
271
|
+
}
|
|
249
272
|
// Migrate prior idle cache entries without treating historical sessions
|
|
250
273
|
// as fresh activity. Its old revision already proves this exact cwd.
|
|
251
274
|
if (entry && !entry.cwd && entry.revision === continuationAnalysisRevision({ ...candidate, isRunning: false })) {
|
|
@@ -261,9 +284,10 @@ export async function createContinuationAnalysisManager({ root, analyze = analyz
|
|
|
261
284
|
if (eligible(candidate) && (backfill.has(key) || newActivity || recheck || trackingEnabled || goalChanged || schemaRefresh)) {
|
|
262
285
|
dirty = enqueue(candidate, recheck || trackingEnabled || goalChanged || schemaRefresh, trackingEnabled || goalChanged || schemaRefresh) || dirty;
|
|
263
286
|
}
|
|
264
|
-
if (previous?.revision !== revision || previous?.goalObserved !== goalObserved || (previous?.tracked === true) !== (candidate.tracked === true) ||
|
|
287
|
+
if (previous?.revision !== revision || previous?.activity !== active || previous?.goalEvidenceVersion !== 1 || previous?.goalObserved !== goalObserved || (previous?.tracked === true) !== (candidate.tracked === true) ||
|
|
265
288
|
previous && (!Object.hasOwn(previous, 'goalRevision') || !Object.hasOwn(previous, 'tracked'))) {
|
|
266
|
-
known.delete(key); known.set(key, { revision, activity: active, isRunning: candidate.isRunning,
|
|
289
|
+
known.delete(key); known.set(key, { revision, cwd: candidate.cwd, activity: active, isRunning: candidate.isRunning, latestTurnId: turnOf(candidate),
|
|
290
|
+
tracked: candidate.tracked === true, goalRevision, goalObserved, goalEvidenceVersion: 1 }); dirty = true;
|
|
267
291
|
}
|
|
268
292
|
}
|
|
269
293
|
// Cancelling owned analysis is separate from stopping the original
|
|
@@ -280,6 +304,7 @@ export async function createContinuationAnalysisManager({ root, analyze = analyz
|
|
|
280
304
|
startDrain();
|
|
281
305
|
},
|
|
282
306
|
project(candidate) {
|
|
307
|
+
candidate = observedCandidate(candidate);
|
|
283
308
|
const revision = continuationAnalysisRevision(candidate), entry = entries.get(keyOf(candidate));
|
|
284
309
|
let analysis = entry?.revision === revision ? { ...entry.result, revision, requestId: entry.requestId } : { state: 'unanalysed', revision };
|
|
285
310
|
const previous = entry?.cwd === candidate.cwd ? entry.settledResult : null;
|
|
@@ -295,6 +320,7 @@ export async function createContinuationAnalysisManager({ root, analyze = analyz
|
|
|
295
320
|
async request(candidate, { requestId } = {}) {
|
|
296
321
|
if (requestId !== undefined && (typeof requestId !== 'string' || !requestId || requestId.length > 128)) throw new Error('Invalid analysis request identity.');
|
|
297
322
|
if (!candidate || !eligible(candidate)) throw new Error('This conversation cannot be analyzed.');
|
|
323
|
+
candidate = observedCandidate(candidate);
|
|
298
324
|
await exclusive(async () => {
|
|
299
325
|
if (closed || suspended) throw new Error('Session analysis is unavailable.');
|
|
300
326
|
candidates.set(keyOf(candidate), candidate);
|
|
@@ -306,6 +332,7 @@ export async function createContinuationAnalysisManager({ root, analyze = analyz
|
|
|
306
332
|
startDrain();
|
|
307
333
|
},
|
|
308
334
|
async invalidate(candidate) {
|
|
335
|
+
candidate = observedCandidate(candidate);
|
|
309
336
|
await exclusive(async () => {
|
|
310
337
|
if (closed || suspended) throw new Error('Session analysis is unavailable.');
|
|
311
338
|
entries.delete(keyOf(candidate));
|
|
@@ -262,6 +262,7 @@ export async function readCodexObservation(client, session, { deadline = Date.no
|
|
|
262
262
|
const page = await client.request('thread/turns/list', { threadId: session, cursor: null, limit: 1, sortDirection: 'desc', itemsView: 'notLoaded' }, timeout());
|
|
263
263
|
if (Array.isArray(page?.data) && page.data.length === 1 && ['inProgress', 'completed', 'interrupted', 'failed'].includes(page.data[0]?.status)) {
|
|
264
264
|
observation.isRunning = page.data[0].status === 'inProgress';
|
|
265
|
+
if (typeof page.data[0].id === 'string' && page.data[0].id && page.data[0].id.length <= 160) observation.latestTurnId = page.data[0].id;
|
|
265
266
|
}
|
|
266
267
|
} catch (error) {
|
|
267
268
|
if (['timeout', 'closed', 'unavailable'].includes(error?.code)) return { goalReadState: 'unavailable', providerUnavailable: true };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readCodexConversation } from './codex-app-server.js';
|
|
2
2
|
import { readClaudeConversation } from './claude-sessions.js';
|
|
3
|
-
import { boundConversation, boundProviderGoal, sanitizedText } from './conversation-context.js';
|
|
3
|
+
import { boundConversation, boundProviderGoal, providerGoalEvidence, sanitizedText } from './conversation-context.js';
|
|
4
4
|
import { runStructuredProviderReview } from '../sqm/review-worker.js';
|
|
5
5
|
import { inspectProviderStatus } from '../desktop-status.js';
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
@@ -152,7 +152,7 @@ export async function readContinuationContext(candidate, { signal, readContext,
|
|
|
152
152
|
const context = boundConversation(raw?.messages, { truncated: raw?.truncated === true, incomplete: raw?.incomplete === true });
|
|
153
153
|
const providerGoal = boundProviderGoal(raw?.providerGoal);
|
|
154
154
|
const contextDigest = createHash('sha256').update(JSON.stringify({ provider: candidate.provider, session: candidate.session, cwd: candidate.cwd, ...context,
|
|
155
|
-
...(providerGoal ? { providerGoal } : {}) })).digest('hex');
|
|
155
|
+
...(providerGoal ? { providerGoal: providerGoalEvidence(providerGoal) } : {}) })).digest('hex');
|
|
156
156
|
return { ...context, providerGoal, contextDigest, awaitingTurn: raw?.awaitingTurn === true, settledTurnId: typeof raw?.settledTurnId === 'string' ? raw.settledTurnId.slice(0, 160) : null };
|
|
157
157
|
}
|
|
158
158
|
|
|
@@ -185,7 +185,7 @@ export async function analyzeContinuation(candidate, {
|
|
|
185
185
|
basis: [{ role: 'goal', turnRef: 'provider-goal', snippet: context.providerGoal.objective.slice(0, 280) }],
|
|
186
186
|
};
|
|
187
187
|
if (previousAnalysis?.analysisVersion === CONTINUATION_ANALYSIS_VERSION && previousAnalysis?.contextDigest === context.contextDigest && STATES.includes(previousAnalysis.state)) {
|
|
188
|
-
return { ...
|
|
188
|
+
return { ...previousAnalysis, ...snapshot };
|
|
189
189
|
}
|
|
190
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
191
|
let result;
|
|
@@ -21,6 +21,13 @@ export function textContent(content) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
const GOAL_STATES = new Set(['active', 'paused', 'blocked', 'usageLimited', 'budgetLimited', 'complete']);
|
|
24
|
+
// Goal timestamps can advance with provider bookkeeping without changing the
|
|
25
|
+
// evidence relevant to triage. Keep timestamps for provenance, not cache keys.
|
|
26
|
+
export function providerGoalEvidence(goal) {
|
|
27
|
+
return goal && typeof goal === 'object' ? { objective: goal.objective ?? null, status: goal.status ?? null,
|
|
28
|
+
textTruncated: goal.textTruncated === true } : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
24
31
|
// Original provider metadata is evidence about the goal, never permission for
|
|
25
32
|
// this separate read-only classifier to execute work or change provider limits.
|
|
26
33
|
export function boundProviderGoal(goal) {
|
|
@@ -114,6 +114,7 @@ export async function discoverSessions({ root, openClient = openCodexAppServer,
|
|
|
114
114
|
return { provider: item.provider, session: item.session, cwd: item.cwd,
|
|
115
115
|
title: sessionTitle(item.title) || (item.provider === 'codex' ? codexTitles.get(item.session) || '' : ''), source: item.source,
|
|
116
116
|
...(typeof isRunning === 'boolean' ? { isRunning } : {}),
|
|
117
|
+
...(item.latestTurnId ? { latestTurnId: item.latestTurnId } : {}),
|
|
117
118
|
...(item.providerGoal !== undefined ? { providerGoal: item.providerGoal } : {}),
|
|
118
119
|
...(item.goalReadState ? { goalReadState: item.goalReadState } : {}),
|
|
119
120
|
tracked: typeof record?.tracked === 'boolean' ? record.tracked : record?.monitor === true,
|
|
@@ -11,7 +11,7 @@ export function safeProviderDiagnostic(value) {
|
|
|
11
11
|
if (SIGNALS.has(value.signal)) result.signal = value.signal;
|
|
12
12
|
if (Number.isInteger(value.outputBytes) && value.outputBytes >= 0 && value.outputBytes <= 2147483647) result.outputBytes = value.outputBytes;
|
|
13
13
|
if (value.retryAttempt === 0 || value.retryAttempt === 1) result.retryAttempt = value.retryAttempt;
|
|
14
|
-
if (typeof value.model === "string" && /^(?:gpt-6-(?:astra)|gpt-6\.1-sol|claude-(?:sonnet|opus|haiku)-[0-9]+(?:-[0-9]+){0,3})$/.test(value.model) && value.model.length <= 64) result.model = value.model;
|
|
14
|
+
if (typeof value.model === "string" && /^(?:gpt-6-(?:astra|sol)|gpt-6\.1-sol|claude-(?:sonnet|opus|haiku)-[0-9]+(?:-[0-9]+){0,3})$/.test(value.model) && value.model.length <= 64) result.model = value.model;
|
|
15
15
|
return result;
|
|
16
16
|
}
|
|
17
17
|
|
package/src/sqm/review-worker.js
CHANGED
|
@@ -20,7 +20,8 @@ const DEFAULT_REQUEST_TIMEOUT_MS = 75_000;
|
|
|
20
20
|
const DEFAULT_EXECUTION_TIMEOUT_MS = 240_000;
|
|
21
21
|
// Keep analysis independent of interactive config. Availability varies by
|
|
22
22
|
// account and CLI; only an explicit model rejection may try the next model.
|
|
23
|
-
|
|
23
|
+
// Background SQM and conversation triage use Sol models to limit usage.
|
|
24
|
+
export const SQM_CODEX_MODELS = Object.freeze(["gpt-6.1-sol", "gpt-6-sol"]);
|
|
24
25
|
export const SQM_CODEX_MODEL = SQM_CODEX_MODELS[0];
|
|
25
26
|
|
|
26
27
|
export class SqmLocalReviewError extends Error {
|