@awak-app/simy-cli 0.5.39 → 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.
- package/package.json +1 -1
- package/src/cli-contract.js +1 -1
- package/src/desktop-host.js +1 -1
- package/src/session-supervision/analysis-manager.js +46 -12
- package/src/session-supervision/codex-app-server.js +27 -12
- package/src/session-supervision/continuation-analysis.js +15 -3
- package/src/session-supervision/index.js +1 -0
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.40";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function withCliContract(
|
package/src/desktop-host.js
CHANGED
|
@@ -310,7 +310,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
310
310
|
const context = await (dependencies.readContinuationContext || readContinuationContext)(candidate);
|
|
311
311
|
// Provider activity timestamps can have one-second precision. Re-read
|
|
312
312
|
// the bounded context before enrollment to reject same-second edits.
|
|
313
|
-
if (context.incomplete || !recommendation.contextDigest || context.contextDigest !== recommendation.contextDigest) {
|
|
313
|
+
if (context.awaitingTurn || context.incomplete || !recommendation.contextDigest || context.contextDigest !== recommendation.contextDigest) {
|
|
314
314
|
await manager.invalidate(candidate);
|
|
315
315
|
throw new Error('This conversation changed; wait for its updated analysis.');
|
|
316
316
|
}
|
|
@@ -9,7 +9,7 @@ const RESULT_STATES = new Set(['continue', 'no_action', 'needs_user', 'waiting',
|
|
|
9
9
|
const keyOf = candidate => `${candidate.provider}:${candidate.session}`;
|
|
10
10
|
const activityOf = candidate => Number.isFinite(candidate.lastActivityAt) ? candidate.lastActivityAt : 0;
|
|
11
11
|
export const continuationAnalysisRevision = candidate => createHash('sha256')
|
|
12
|
-
.update(JSON.stringify([candidate.provider, candidate.session, candidate.cwd, activityOf(candidate)]))
|
|
12
|
+
.update(JSON.stringify([candidate.provider, candidate.session, candidate.cwd, activityOf(candidate), ...(candidate.isRunning === true ? ['running'] : [])]))
|
|
13
13
|
.digest('hex');
|
|
14
14
|
const eligible = candidate => candidate.capabilities?.canAnalyze === true &&
|
|
15
15
|
['unmanaged', 'monitor'].includes(candidate.binding?.mode || 'unmanaged');
|
|
@@ -60,15 +60,19 @@ export async function createContinuationAnalysisManager({ root, analyze = analyz
|
|
|
60
60
|
} finally { await rm(temporary, { force: true }); }
|
|
61
61
|
};
|
|
62
62
|
const changed = () => { void Promise.resolve().then(onChange).catch(() => {}); };
|
|
63
|
-
const enqueue = (candidate, force = false) => {
|
|
63
|
+
const enqueue = (candidate, force = false, forceAnalysis = false) => {
|
|
64
64
|
const key = keyOf(candidate), revision = continuationAnalysisRevision(candidate);
|
|
65
65
|
const entry = entries.get(key);
|
|
66
66
|
if (entry?.revision === revision && (['queued', 'analyzing'].includes(entry.result?.state) ||
|
|
67
67
|
!force && !['failed', 'uncertain'].includes(entry.result?.state))) return false;
|
|
68
68
|
entries.delete(key);
|
|
69
|
-
entries.set(key, { revision,
|
|
69
|
+
entries.set(key, { revision, cwd: candidate.cwd, forceAnalysis, settledResult: entry?.cwd === candidate.cwd ? entry.settledResult : null,
|
|
70
|
+
result: { state: 'queued' } });
|
|
70
71
|
return true;
|
|
71
72
|
};
|
|
73
|
+
const finishedAnalysis = (result, previous) => result.contextDigest && result.settledTurnId
|
|
74
|
+
? { ...result, awaitingTurn: false, analyzedAt: result.contextDigest === previous?.contextDigest ? previous.analyzedAt : now() }
|
|
75
|
+
: previous;
|
|
72
76
|
const startDrain = () => {
|
|
73
77
|
if (closed || draining) return;
|
|
74
78
|
draining = (async () => {
|
|
@@ -80,7 +84,7 @@ export async function createContinuationAnalysisManager({ root, analyze = analyz
|
|
|
80
84
|
if (entry.result?.state !== 'queued' || !candidate || !eligible(candidate) || entry.revision !== continuationAnalysisRevision(candidate)) continue;
|
|
81
85
|
entry.result = { state: 'analyzing' };
|
|
82
86
|
await persist(); changed();
|
|
83
|
-
return { key, revision: entry.revision, candidate };
|
|
87
|
+
return { key, revision: entry.revision, candidate, previousAnalysis: entry.forceAnalysis ? null : entry.settledResult };
|
|
84
88
|
}
|
|
85
89
|
return null;
|
|
86
90
|
});
|
|
@@ -96,7 +100,7 @@ export async function createContinuationAnalysisManager({ root, analyze = analyz
|
|
|
96
100
|
});
|
|
97
101
|
let result, inference;
|
|
98
102
|
try {
|
|
99
|
-
inference = Promise.resolve(analyze(selected.candidate, { signal }));
|
|
103
|
+
inference = Promise.resolve(analyze(selected.candidate, { signal, previousAnalysis: selected.previousAnalysis }));
|
|
100
104
|
result = await Promise.race([inference, cancellation]);
|
|
101
105
|
if (!RESULT_STATES.has(result?.state) || typeof result.reason !== 'string' ||
|
|
102
106
|
(result.state === 'continue' && (!result.objective || !result.remaining?.length || !result.nextAction || !result.basis?.some(item => item.role === 'user')))) {
|
|
@@ -114,12 +118,22 @@ export async function createContinuationAnalysisManager({ root, analyze = analyz
|
|
|
114
118
|
const current = candidates.get(selected.key), entry = entries.get(selected.key);
|
|
115
119
|
if (closed) return;
|
|
116
120
|
if (!current || !eligible(current) || continuationAnalysisRevision(current) !== selected.revision || entry?.revision !== selected.revision) {
|
|
121
|
+
// Live timestamps may advance during inference while the completed
|
|
122
|
+
// boundary stays identical. Retain that snapshot for display/reuse;
|
|
123
|
+
// never publish it as a current actionable recommendation.
|
|
124
|
+
if (current && eligible(current) && current.cwd === selected.candidate.cwd && entry?.cwd === current.cwd &&
|
|
125
|
+
result.contextDigest && result.settledTurnId && ['queued', 'analyzing'].includes(entry.result?.state)) {
|
|
126
|
+
entry.settledResult = finishedAnalysis(result, selected.previousAnalysis);
|
|
127
|
+
await persist(); changed();
|
|
128
|
+
}
|
|
117
129
|
if (entry?.revision === selected.revision && entry.result?.state === 'analyzing') {
|
|
118
130
|
entry.result = { state: 'queued' }; await persist(); changed();
|
|
119
131
|
}
|
|
120
132
|
return;
|
|
121
133
|
}
|
|
122
|
-
|
|
134
|
+
const settledResult = finishedAnalysis(result, selected.previousAnalysis || entry.settledResult);
|
|
135
|
+
entries.set(selected.key, { revision: selected.revision, cwd: current.cwd, settledResult,
|
|
136
|
+
result: { ...result, analyzedAt: settledResult?.analyzedAt || now() } });
|
|
123
137
|
await persist(); changed();
|
|
124
138
|
});
|
|
125
139
|
}
|
|
@@ -141,10 +155,21 @@ export async function createContinuationAnalysisManager({ root, analyze = analyz
|
|
|
141
155
|
for (const candidate of sorted) {
|
|
142
156
|
const key = keyOf(candidate), revision = continuationAnalysisRevision(candidate), previous = known.get(key);
|
|
143
157
|
const active = activityOf(candidate);
|
|
144
|
-
const newActivity = document.initialized && (previous ? active > previous.activity : active > document.baselineAt);
|
|
145
|
-
|
|
158
|
+
const newActivity = document.initialized && (previous ? active > previous.activity || (candidate.isRunning === true) !== (previous.isRunning === true) : active > document.baselineAt);
|
|
159
|
+
const entry = entries.get(key);
|
|
160
|
+
// Migrate prior idle cache entries without treating historical sessions
|
|
161
|
+
// as fresh activity. Its old revision already proves this exact cwd.
|
|
162
|
+
if (entry && !entry.cwd && entry.revision === continuationAnalysisRevision({ ...candidate, isRunning: false })) {
|
|
163
|
+
entry.cwd = candidate.cwd;
|
|
164
|
+
entry.settledResult = entry.result?.contextDigest ? entry.result : null;
|
|
165
|
+
dirty = true;
|
|
166
|
+
}
|
|
167
|
+
// A reader that raced a terminal boundary (or a provider without live
|
|
168
|
+
// status) is retried at discovery cadence, never a busy inference loop.
|
|
169
|
+
const recheck = entry?.result?.reasonCode === 'turn_settling' || entry?.result?.awaitingTurn === true && candidate.isRunning !== true;
|
|
170
|
+
if (eligible(candidate) && (backfill.has(key) || newActivity || recheck)) dirty = enqueue(candidate, recheck) || dirty;
|
|
146
171
|
if (previous?.revision !== revision) {
|
|
147
|
-
known.delete(key); known.set(key, { revision, activity: active }); dirty = true;
|
|
172
|
+
known.delete(key); known.set(key, { revision, activity: active, isRunning: candidate.isRunning }); dirty = true;
|
|
148
173
|
}
|
|
149
174
|
}
|
|
150
175
|
// Empty discovery during provider startup must not consume backfill.
|
|
@@ -155,14 +180,23 @@ export async function createContinuationAnalysisManager({ root, analyze = analyz
|
|
|
155
180
|
},
|
|
156
181
|
project(candidate) {
|
|
157
182
|
const revision = continuationAnalysisRevision(candidate), entry = entries.get(keyOf(candidate));
|
|
158
|
-
|
|
183
|
+
let analysis = entry?.revision === revision ? { ...entry.result, revision } : { state: 'unanalysed', revision };
|
|
184
|
+
const previous = entry?.cwd === candidate.cwd ? entry.settledResult : null;
|
|
185
|
+
if (candidate.isRunning === true || analysis.awaitingTurn === true) {
|
|
186
|
+
analysis = { state: 'waiting', awaitingTurn: true, revision,
|
|
187
|
+
reason: 'This turn is still running. SIMY analyzes only finished turns and will update automatically when it ends.',
|
|
188
|
+
...(previous ? { previousAnalysis: previous } : {}) };
|
|
189
|
+
} else if (previous && ['queued', 'analyzing'].includes(analysis.state)) {
|
|
190
|
+
analysis.previousAnalysis = previous;
|
|
191
|
+
}
|
|
192
|
+
return { ...candidate, analysis };
|
|
159
193
|
},
|
|
160
194
|
async request(candidate) {
|
|
161
195
|
if (!candidate || !eligible(candidate)) throw new Error('This conversation cannot be analyzed.');
|
|
162
196
|
await exclusive(async () => {
|
|
163
197
|
if (closed) throw new Error('Session analysis is unavailable.');
|
|
164
198
|
candidates.set(keyOf(candidate), candidate);
|
|
165
|
-
if (enqueue(candidate, true)) { await persist(); changed(); }
|
|
199
|
+
if (enqueue(candidate, true, true)) { await persist(); changed(); }
|
|
166
200
|
});
|
|
167
201
|
startDrain();
|
|
168
202
|
},
|
|
@@ -178,7 +212,7 @@ export async function createContinuationAnalysisManager({ root, analyze = analyz
|
|
|
178
212
|
requireRecommendation(candidate) {
|
|
179
213
|
if (closed) throw new Error('Session analysis is unavailable.');
|
|
180
214
|
const analysis = this.project(candidate).analysis;
|
|
181
|
-
if (!eligible(candidate) || analysis.state !== 'continue') throw new Error('Analyze this conversation before continuing; it needs a current recommendation.');
|
|
215
|
+
if (!eligible(candidate) || candidate.isRunning === true || analysis.awaitingTurn === true || analysis.state !== 'continue') throw new Error('Analyze this conversation before continuing; it needs a current recommendation.');
|
|
182
216
|
return analysis;
|
|
183
217
|
},
|
|
184
218
|
async whenIdle() { do { await serial; await draining; } while (draining); await serial; },
|
|
@@ -152,6 +152,7 @@ export function codexThreadMetadata(thread) {
|
|
|
152
152
|
lastActivityAt: Number.isFinite(thread.updatedAt) ? thread.updatedAt * 1000 : null,
|
|
153
153
|
model: typeof thread.model === 'string' ? thread.model : null,
|
|
154
154
|
observation: { source: 'codex_app_server', observedAt: Date.now() },
|
|
155
|
+
...(thread.status?.type === 'active' || thread.status?.type === 'idle' ? { isRunning: thread.status.type === 'active' } : {}),
|
|
155
156
|
availability: 'unknown',
|
|
156
157
|
};
|
|
157
158
|
}
|
|
@@ -188,16 +189,14 @@ export async function readCodexConversation(candidate, { openClient = openCodexA
|
|
|
188
189
|
// reads only metadata and a fixed number of recent display summaries.
|
|
189
190
|
const metadata = await readCodexSession(client, candidate.session);
|
|
190
191
|
if (metadata.cwd !== candidate.cwd) throw new CodexAppServerError('identity', 'Exact conversation identity is unavailable.');
|
|
191
|
-
if (Number.isFinite(candidate.lastActivityAt) && metadata.lastActivityAt !== candidate.lastActivityAt) {
|
|
192
|
-
throw new CodexAppServerError('revision_changed', 'Conversation changed since discovery.');
|
|
193
|
-
}
|
|
194
192
|
if (signal?.aborted) throw new CodexAppServerError('cancelled', 'Conversation analysis was cancelled.');
|
|
193
|
+
const terminal = turn => ['completed', 'interrupted', 'failed'].includes(turn?.status);
|
|
195
194
|
const turns = [];
|
|
196
195
|
const turnIds = new Set(), cursors = new Set();
|
|
197
196
|
let cursor = null;
|
|
198
197
|
do {
|
|
199
198
|
if (signal?.aborted) throw new CodexAppServerError('cancelled', 'Conversation analysis was cancelled.');
|
|
200
|
-
const limit = Math.min(ANALYSIS_PAGE_SIZE, ANALYSIS_MAX_TURNS - turns.length);
|
|
199
|
+
const limit = Math.min(ANALYSIS_PAGE_SIZE, ANALYSIS_MAX_TURNS - turns.filter(terminal).length);
|
|
201
200
|
let page;
|
|
202
201
|
try {
|
|
203
202
|
page = await client.request('thread/turns/list', { threadId: candidate.session, cursor, limit, sortDirection: 'desc', itemsView: 'summary' });
|
|
@@ -217,10 +216,26 @@ export async function readCodexConversation(candidate, { openClient = openCodexA
|
|
|
217
216
|
cursor = page.nextCursor || null;
|
|
218
217
|
if (cursor && (!page.data.length || cursors.has(cursor))) throw new CodexAppServerError('context_unavailable', 'Codex returned ambiguous conversation pagination.');
|
|
219
218
|
if (cursor) cursors.add(cursor);
|
|
220
|
-
} while (cursor && turns.length < ANALYSIS_MAX_TURNS);
|
|
219
|
+
} while (cursor && turns.filter(terminal).length < ANALYSIS_MAX_TURNS && turns.length < ANALYSIS_MAX_TURNS + ANALYSIS_PAGE_SIZE);
|
|
220
|
+
// A timestamp covers the whole live thread. Instead pin the last terminal
|
|
221
|
+
// turn and check it again: streaming output in the newer turn is irrelevant
|
|
222
|
+
// to this read-only snapshot, but a newly settled turn must invalidate it.
|
|
223
|
+
const settled = turns.filter(terminal).slice(0, ANALYSIS_MAX_TURNS);
|
|
224
|
+
const boundary = settled[0];
|
|
225
|
+
const verification = await client.request('thread/turns/list', {
|
|
226
|
+
threadId: candidate.session, cursor: null, limit: ANALYSIS_PAGE_SIZE, sortDirection: 'desc', itemsView: 'summary',
|
|
227
|
+
});
|
|
228
|
+
if (!Array.isArray(verification?.data) || verification.data.length > ANALYSIS_PAGE_SIZE) {
|
|
229
|
+
throw new CodexAppServerError('context_unavailable', 'Codex returned an invalid snapshot check.');
|
|
230
|
+
}
|
|
231
|
+
const latestBoundary = verification.data.find(terminal);
|
|
232
|
+
// Compare the immutable settled summary, never the running turn's prose.
|
|
233
|
+
if (JSON.stringify(boundary) !== JSON.stringify(latestBoundary)) {
|
|
234
|
+
throw new CodexAppServerError('turn_settling', 'The latest turn settled during the snapshot read.');
|
|
235
|
+
}
|
|
221
236
|
const latestMetadata = await readCodexSession(client, candidate.session);
|
|
222
237
|
if (latestMetadata.cwd !== candidate.cwd) throw new CodexAppServerError('identity', 'Exact conversation identity is unavailable.');
|
|
223
|
-
if (latestMetadata.
|
|
238
|
+
if (latestMetadata.model !== metadata.model) {
|
|
224
239
|
throw new CodexAppServerError('revision_changed', 'Conversation changed while reading recent history.');
|
|
225
240
|
}
|
|
226
241
|
if (signal?.aborted) throw new CodexAppServerError('cancelled', 'Conversation analysis was cancelled.');
|
|
@@ -228,13 +243,13 @@ export async function readCodexConversation(candidate, { openClient = openCodexA
|
|
|
228
243
|
// A display summary omits intermediate reasoning/tool context even when
|
|
229
244
|
// every turn fits. Never represent it as the complete conversation; the
|
|
230
245
|
// classifier must establish a self-contained objective from retained text.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (turn.itemsView === 'notLoaded') { truncated = true; if (turn ===
|
|
246
|
+
const awaitingTurn = latestMetadata.isRunning === true || [...turns, ...verification.data].some(turn => ['inProgress', 'in_progress'].includes(turn?.status));
|
|
247
|
+
let incomplete = !settled.length || turns.some(turn => !terminal(turn) && !['inProgress', 'in_progress'].includes(turn?.status)), truncated = true;
|
|
248
|
+
for (const [index, turn] of settled.toReversed().entries()) {
|
|
249
|
+
if (turn.itemsView === 'notLoaded') { truncated = true; if (turn === boundary) incomplete = true; continue; }
|
|
235
250
|
if (turn.itemsView !== 'summary') throw new CodexAppServerError('context_unavailable', 'Codex did not return the requested conversation summaries.');
|
|
236
251
|
if (!Array.isArray(turn?.items)) { incomplete = true; continue; }
|
|
237
|
-
if (turn ===
|
|
252
|
+
if (turn === boundary && !turn.items.length) incomplete = true;
|
|
238
253
|
for (const [itemIndex, item] of turn.items.entries()) {
|
|
239
254
|
const role = item?.type === 'userMessage' ? 'user' : item?.type === 'agentMessage' ? 'assistant' : null;
|
|
240
255
|
if (!role) continue;
|
|
@@ -243,7 +258,7 @@ export async function readCodexConversation(candidate, { openClient = openCodexA
|
|
|
243
258
|
messages.push({ role, turnRef: `${turn.id || index}:${item.id || itemIndex}`, text });
|
|
244
259
|
}
|
|
245
260
|
}
|
|
246
|
-
return boundConversation(messages, { incomplete, truncated });
|
|
261
|
+
return { ...boundConversation(messages, { incomplete, truncated }), awaitingTurn, settledTurnId: boundary?.id || null };
|
|
247
262
|
} finally {
|
|
248
263
|
signal?.removeEventListener('abort', cancelled);
|
|
249
264
|
await client.close();
|
|
@@ -45,6 +45,10 @@ function uncertain(reason) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
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
|
+
};
|
|
48
52
|
// Provider error messages can include private paths or transcript text. Only
|
|
49
53
|
// these known codes select product copy; never forward the original message.
|
|
50
54
|
const reasons = {
|
|
@@ -105,11 +109,11 @@ export async function readContinuationContext(candidate, { signal, readContext,
|
|
|
105
109
|
if (signal?.aborted) throw new Error('Conversation analysis was cancelled.');
|
|
106
110
|
const context = boundConversation(raw?.messages, { truncated: raw?.truncated === true, incomplete: raw?.incomplete === true });
|
|
107
111
|
const contextDigest = createHash('sha256').update(JSON.stringify({ provider: candidate.provider, session: candidate.session, cwd: candidate.cwd, ...context })).digest('hex');
|
|
108
|
-
return { ...context, contextDigest };
|
|
112
|
+
return { ...context, contextDigest, awaitingTurn: raw?.awaitingTurn === true, settledTurnId: typeof raw?.settledTurnId === 'string' ? raw.settledTurnId.slice(0, 160) : null };
|
|
109
113
|
}
|
|
110
114
|
|
|
111
115
|
export async function analyzeContinuation(candidate, {
|
|
112
|
-
signal, readContext, openCodexClient, claudeHome,
|
|
116
|
+
signal, readContext, openCodexClient, claudeHome, previousAnalysis,
|
|
113
117
|
runProvider = runStructuredProviderReview, providerOptions = {},
|
|
114
118
|
} = {}) {
|
|
115
119
|
if (signal?.aborted) return uncertain('Conversation analysis was cancelled.');
|
|
@@ -121,6 +125,14 @@ export async function analyzeContinuation(candidate, {
|
|
|
121
125
|
context = await readContinuationContext(candidate, { signal, readContext, openCodexClient, claudeHome });
|
|
122
126
|
} catch (error) { return unreadableContext(error); }
|
|
123
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
|
+
}
|
|
124
136
|
if (context.incomplete || !context.messages.length) return uncertain('The latest conversation request or response is missing or incomplete.');
|
|
125
137
|
try {
|
|
126
138
|
const result = await runProvider(candidate.provider, {
|
|
@@ -129,7 +141,7 @@ export async function analyzeContinuation(candidate, {
|
|
|
129
141
|
if (signal?.aborted) return uncertain('Conversation analysis was cancelled.');
|
|
130
142
|
const analysis = validateContinuationAnalysis(result?.output, context);
|
|
131
143
|
const model = typeof result?.model === 'string' && /^[a-zA-Z0-9._/-]{1,120}$/.test(result.model) ? result.model : null;
|
|
132
|
-
return { ...analysis,
|
|
144
|
+
return { ...analysis, ...snapshot, ...(model ? { model } : {}) };
|
|
133
145
|
} catch {
|
|
134
146
|
// Provider stderr/errors may contain transcript text or credentials. Never
|
|
135
147
|
// expose them to the desktop, cache, diagnostics or control plane.
|
|
@@ -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),
|