@awak-app/simy-cli 0.5.38 → 0.5.40

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.
@@ -44,6 +44,27 @@ function uncertain(reason) {
44
44
  return { state: 'uncertain', reason, objective: '', remaining: [], nextAction: null, basis: [] };
45
45
  }
46
46
 
47
+ function unreadableContext(error) {
48
+ if (error?.code === 'turn_settling') return {
49
+ state: 'waiting', reason: 'The current turn is finishing. SIMY will check its completed result automatically.',
50
+ reasonCode: 'turn_settling', awaitingTurn: true, objective: '', remaining: [], nextAction: null, basis: [],
51
+ };
52
+ // Provider error messages can include private paths or transcript text. Only
53
+ // these known codes select product copy; never forward the original message.
54
+ const reasons = {
55
+ response_too_large: 'A conversation response exceeded the supported size. Open the original conversation or retry after updating SIMY.',
56
+ revision_changed: 'This conversation changed while it was being read. Wait until it finishes, then retry analysis.',
57
+ timeout: 'The provider did not return the conversation in time. Retry analysis when it is available.',
58
+ identity: 'The original conversation identity or workspace no longer matches. Refresh sessions and retry analysis.',
59
+ unsupported: 'This Codex version does not support the required conversation reader. Update Codex and retry analysis.',
60
+ unavailable: 'The local provider is unavailable. Open its app, then retry analysis.',
61
+ cancelled: 'Conversation analysis was cancelled.',
62
+ };
63
+ const code = error?.code === -32601 ? 'unsupported' : error?.code;
64
+ const reasonCode = Object.hasOwn(reasons, code) ? code : 'context_unavailable';
65
+ return { ...uncertain(reasons[reasonCode] || 'The conversation could not be read. Refresh sessions and retry analysis.'), reasonCode };
66
+ }
67
+
47
68
  function boundedString(value, maximum, required = false) {
48
69
  if (typeof value !== 'string' || value.length > maximum || sanitizedText(value) !== value || (required && !value)) throw new Error('Invalid string');
49
70
  return value;
@@ -88,11 +109,11 @@ export async function readContinuationContext(candidate, { signal, readContext,
88
109
  if (signal?.aborted) throw new Error('Conversation analysis was cancelled.');
89
110
  const context = boundConversation(raw?.messages, { truncated: raw?.truncated === true, incomplete: raw?.incomplete === true });
90
111
  const contextDigest = createHash('sha256').update(JSON.stringify({ provider: candidate.provider, session: candidate.session, cwd: candidate.cwd, ...context })).digest('hex');
91
- return { ...context, contextDigest };
112
+ return { ...context, contextDigest, awaitingTurn: raw?.awaitingTurn === true, settledTurnId: typeof raw?.settledTurnId === 'string' ? raw.settledTurnId.slice(0, 160) : null };
92
113
  }
93
114
 
94
115
  export async function analyzeContinuation(candidate, {
95
- signal, readContext, openCodexClient, claudeHome,
116
+ signal, readContext, openCodexClient, claudeHome, previousAnalysis,
96
117
  runProvider = runStructuredProviderReview, providerOptions = {},
97
118
  } = {}) {
98
119
  if (signal?.aborted) return uncertain('Conversation analysis was cancelled.');
@@ -102,8 +123,16 @@ export async function analyzeContinuation(candidate, {
102
123
  let context;
103
124
  try {
104
125
  context = await readContinuationContext(candidate, { signal, readContext, openCodexClient, claudeHome });
105
- } catch { return uncertain('Conversation context could not be read safely.'); }
126
+ } catch (error) { return unreadableContext(error); }
106
127
  if (signal?.aborted) return uncertain('Conversation analysis was cancelled.');
128
+ const snapshot = { contextDigest: context.contextDigest, awaitingTurn: context.awaitingTurn, settledTurnId: context.settledTurnId };
129
+ if (context.awaitingTurn && !context.messages.length) return {
130
+ state: 'waiting', reason: 'The first turn is still running. SIMY will analyze it after it finishes.', ...snapshot,
131
+ objective: '', remaining: [], nextAction: null, basis: [],
132
+ };
133
+ if (previousAnalysis?.contextDigest === context.contextDigest && STATES.includes(previousAnalysis.state)) {
134
+ return { ...previousAnalysis, ...snapshot };
135
+ }
107
136
  if (context.incomplete || !context.messages.length) return uncertain('The latest conversation request or response is missing or incomplete.');
108
137
  try {
109
138
  const result = await runProvider(candidate.provider, {
@@ -112,7 +141,7 @@ export async function analyzeContinuation(candidate, {
112
141
  if (signal?.aborted) return uncertain('Conversation analysis was cancelled.');
113
142
  const analysis = validateContinuationAnalysis(result?.output, context);
114
143
  const model = typeof result?.model === 'string' && /^[a-zA-Z0-9._/-]{1,120}$/.test(result.model) ? result.model : null;
115
- return { ...analysis, contextDigest: context.contextDigest, ...(model ? { model } : {}) };
144
+ return { ...analysis, ...snapshot, ...(model ? { model } : {}) };
116
145
  } catch {
117
146
  // Provider stderr/errors may contain transcript text or credentials. Never
118
147
  // expose them to the desktop, cache, diagnostics or control plane.
@@ -6,7 +6,7 @@ import { openCodexAppServer, readCodexSession } from './codex-app-server.js';
6
6
 
7
7
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
8
8
  function unsettled(managed) {
9
- return managed.phase === 'understanding' || managed.executionUncertain || managed.executionAttempt && managed.executionAttempt.phase !== 'checkpointed' || managed.providerWarning || managed.resumeLease || managed.pendingApproval || managed.approvedAction && !managed.approvedAction.dispatchedAt ||
9
+ return ['understanding', 'action_association'].includes(managed.phase) || managed.executionUncertain || managed.executionAttempt && managed.executionAttempt.phase !== 'checkpointed' || managed.providerWarning || managed.resumeLease || managed.pendingApproval || managed.approvedAction && !managed.approvedAction.dispatchedAt ||
10
10
  managed.dispatchLedger?.some(entry => !['settled', 'cancelled'].includes(entry.status)) ||
11
11
  managed.dispatches?.some(entry => entry.status && !['settled', 'cancelled', 'completed'].includes(entry.status));
12
12
  }
@@ -56,6 +56,7 @@ export async function discoverSessions({ root, openClient = openCodexAppServer,
56
56
  const supported = item.provider === 'codex' && item.observation.source === 'codex_app_server' && Boolean(item.model) && !codexError;
57
57
  const liveHook = item.provider === 'claude' && !claudeError && await liveClaudeHook(record, item, inspectProcess);
58
58
  return { provider: item.provider, session: item.session, cwd: item.cwd, title: item.title, source: item.source,
59
+ ...(typeof item.isRunning === 'boolean' ? { isRunning: item.isRunning } : {}),
59
60
  lastActivityAt: item.lastActivityAt, observation: item.observation, availability: item.availability,
60
61
  binding: { mode, taskId: run?.managed?.taskId || null },
61
62
  originalSessionTarget: originalSessionTarget(item.provider, item.session),
@@ -21,6 +21,7 @@ export async function claudePluginFiles() {
21
21
  files.set(relative, content.replaceAll("current Codex thread", "current Claude Code conversation"));
22
22
  }
23
23
  files.set("hooks/delivery-loop.mjs", await readFile(path.join(SOURCE, "hooks/delivery-loop.mjs"), "utf8"));
24
+ files.set("hooks/action-association.mjs", await readFile(path.join(SOURCE, "hooks/action-association.mjs"), "utf8"));
24
25
  files.set("hooks/workspace-lease.mjs", await readFile(path.join(SOURCE, "hooks/workspace-lease.mjs"), "utf8"));
25
26
  files.set("hooks/connection-reminder.mjs", await readFile(path.join(SOURCE, "hooks/connection-reminder.mjs"), "utf8"));
26
27
  const hooks = JSON.parse(await readFile(path.join(SOURCE, "hooks/hooks.json"), "utf8"));