@awak-app/simy-cli 0.6.4 → 0.6.6
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 +6 -1
- package/package.json +2 -2
- package/src/cli-contract.js +1 -1
- package/src/desktop-host.js +2 -2
- package/src/session-supervision/action-association-host.js +6 -2
- package/src/session-supervision/association-store.js +12 -3
- package/src/session-supervision/claude-sessions.js +36 -20
- package/src/session-supervision/codex-app-server.js +4 -2
- package/src/session-supervision/discovery-window.js +15 -0
- package/src/session-supervision/index.js +14 -11
package/README.md
CHANGED
|
@@ -178,7 +178,12 @@ marketplace still points to SIMY’s generated local source. Plugin versions inc
|
|
|
178
178
|
receive a fresh cache. Use a new Codex task to pick up updated skills; running
|
|
179
179
|
tasks are not restarted. Missing or disabled plugins are not enabled automatically.
|
|
180
180
|
Failed refreshes retry on the next check without blocking daemon startup.
|
|
181
|
-
The desktop Sessions page
|
|
181
|
+
The desktop Sessions page loads a recent window of up to 200 conversations,
|
|
182
|
+
ranked by last activity. Explicitly tracked, monitored, auto-continue and unsettled
|
|
183
|
+
supervision records remain visible even outside that window. Claude discovery
|
|
184
|
+
ranks file metadata before opening selected transcripts. A conflicting saved
|
|
185
|
+
workspace leaves only that conversation's Action association unavailable; it
|
|
186
|
+
does not stop discovery or change the saved identity. The page lets users
|
|
182
187
|
choose monitoring or supported session supervision with a goal, acceptance
|
|
183
188
|
criteria and an autonomy policy. Local time, token and continuation counters do not stop work. Codex enrollment uses an exact
|
|
184
189
|
app-server session and checks writer ownership before each continuation. Claude
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awak-app/simy-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.6",
|
|
4
4
|
"description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"test:e2e:console:real-provider": "node ./scripts/real-provider-console-e2e.js",
|
|
39
39
|
"test:e2e:console:fullstack": "node ./scripts/fullstack-cli-smoke.js",
|
|
40
40
|
"check": "npm run check:auto-update && node --check ./src/desktop-executor.js && node --check ./src/local-task.js && node --check ./scripts/desktop-executor-e2e.js && node --check ./scripts/pre-guardrail-e2e.js && node --check ./scripts/fixtures/fake-desktop-provider.js && npm run check:syntax && npm run check:session-supervision-syntax && npm run check:real-provider-syntax && npm test && npm run check:package",
|
|
41
|
-
"check:session-supervision-syntax": "node --check ./src/session-supervision/index.js && node --check ./src/session-supervision/codex-app-server.js && node --check ./src/session-supervision/claude-sessions.js && node --check ./src/session-supervision/original-session-target.js && node --check ./src/session-supervision/action-association.js && node --check ./src/session-supervision/action-association-host.js && node --check ./src/session-supervision/action-association-api.js && node --check ./src/session-supervision/association-store.js && node --check ./src/session-supervision/action-deadline.js && node --check ./src/session-supervision/action-deadline-store.js && node --check ./src/codex-plugins/simy/hooks/action-association.mjs",
|
|
41
|
+
"check:session-supervision-syntax": "node --check ./src/session-supervision/index.js && node --check ./src/session-supervision/discovery-window.js && node --check ./src/session-supervision/codex-app-server.js && node --check ./src/session-supervision/claude-sessions.js && node --check ./src/session-supervision/original-session-target.js && node --check ./src/session-supervision/action-association.js && node --check ./src/session-supervision/action-association-host.js && node --check ./src/session-supervision/action-association-api.js && node --check ./src/session-supervision/association-store.js && node --check ./src/session-supervision/action-deadline.js && node --check ./src/session-supervision/action-deadline-store.js && node --check ./src/codex-plugins/simy/hooks/action-association.mjs",
|
|
42
42
|
"check:auto-update": "node --check ./src/auto-update.js && node --check ./scripts/auto-update-e2e.js && npm run check:sqm",
|
|
43
43
|
"check:workflow-observation-sender": "node --check ./src/workflow-observation-sender.js",
|
|
44
44
|
"check:sqm": "node --check ./src/sqm/canonical.js && node --check ./src/sqm/validation.js && node --check ./src/sqm/reliability.js && node --check ./src/sqm/repository.js && node --check ./src/sqm/checkers.js && node --check ./src/sqm/bundle-store.js && node --check ./src/sqm/proof.js && node --check ./src/sqm/evidence-client.js && node --check ./src/sqm/session.js && node --check ./src/sqm/index.js && node --check ./src/sqm/command.js && node --check ./src/sqm/agent-skill.js && node --check ./src/sqm/workflow-onboarding.js && node --check ./src/sqm/workflow-update.js && node --check ./src/sqm/history-store.js && node --check ./src/sqm/review-worker.js && node --check ./scripts/sqm-complete-loop-e2e.js && node --check ./scripts/sqm-local-review-e2e.js",
|
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.6";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function withCliContract(
|
package/src/desktop-host.js
CHANGED
|
@@ -462,7 +462,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
462
462
|
if (command.force !== true) throw new Error("Confirm before stopping this task.");
|
|
463
463
|
await agent.stopDesktopTask(command.taskKey);
|
|
464
464
|
emit({ type: "result", id: command.id, ok: true });
|
|
465
|
-
void publish(); return;
|
|
465
|
+
void publish().catch(() => emit({ type: "error", message: "SIMY status could not be refreshed." })); return;
|
|
466
466
|
}
|
|
467
467
|
const access = await accessSnapshot();
|
|
468
468
|
const commandScope = accessScope(access);
|
|
@@ -482,7 +482,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
482
482
|
statisticsCache.clear(); // Only the selected conversation needs chart data.
|
|
483
483
|
statisticsCache.set(`${candidate.provider}:${candidate.session}`, statistics);
|
|
484
484
|
emit({ type: "result", id: command.id, ok: true });
|
|
485
|
-
void publish(); return;
|
|
485
|
+
void publish().catch(() => emit({ type: "error", message: "SIMY status could not be refreshed." })); return;
|
|
486
486
|
}
|
|
487
487
|
if (action === "my_ai_session") {
|
|
488
488
|
const ticket = await agent.createDesktopWebSession();
|
|
@@ -200,7 +200,10 @@ export function createActionAssociationHost({ root, sessionRoot, webOrigin, api
|
|
|
200
200
|
async decorateSessions(sessions, projection) {
|
|
201
201
|
await ordinary.decorate(sessions, projection);
|
|
202
202
|
const records = projection?.byKey || new Map((await list(root)).map(record => [`${record.provider}:${record.session}`, record]));
|
|
203
|
-
const status = { runs: sessions.filter(item =>
|
|
203
|
+
const status = { runs: sessions.filter(item => {
|
|
204
|
+
const record = records.get(`${item.provider}:${item.session}`);
|
|
205
|
+
return record?.run?.managed && record.cwd === item.cwd;
|
|
206
|
+
}).map(item => {
|
|
204
207
|
const record = records.get(`${item.provider}:${item.session}`);
|
|
205
208
|
return { managed: true, taskId: record.run.managed.taskId, actionAssociation: record.run.managed.actionAssociation, candidate: item };
|
|
206
209
|
}) };
|
|
@@ -212,6 +215,7 @@ export function createActionAssociationHost({ root, sessionRoot, webOrigin, api
|
|
|
212
215
|
const expected = projection ? projection.authRevision : await current(), record = projection
|
|
213
216
|
? projection.byKey.get(`${candidate.provider}:${candidate.session}`)
|
|
214
217
|
: await read(root, candidate.provider, candidate.session);
|
|
218
|
+
if (record?.cwd !== candidate.cwd) return null;
|
|
215
219
|
const local = record?.run?.managed?.actionAssociation || record?.actionAssociation;
|
|
216
220
|
if (!expected || projection && auth !== expected || local?.authRevision !== expected || !(candidate.actionAssociation?.canCorrect || candidate.actionAssociation?.canReadBinding)) return null;
|
|
217
221
|
const binding = candidate.actionAssociation.binding;
|
|
@@ -224,7 +228,7 @@ export function createActionAssociationHost({ root, sessionRoot, webOrigin, api
|
|
|
224
228
|
const latest = await read(root, candidate.provider, candidate.session);
|
|
225
229
|
const scoped = latest?.run?.managed?.actionAssociation || latest?.actionAssociation;
|
|
226
230
|
if (!await valid(expected)) return false;
|
|
227
|
-
return (latest?.run?.managed?.taskId || latest?.associationTaskId) === candidate.taskId && scoped?.namespace === local.namespace && scoped?.authRevision === expected && fingerprint(scoped.binding) === fingerprint(binding);
|
|
231
|
+
return latest?.cwd === candidate.cwd && (latest?.run?.managed?.taskId || latest?.associationTaskId) === candidate.taskId && scoped?.namespace === local.namespace && scoped?.authRevision === expected && fingerprint(scoped.binding) === fingerprint(binding);
|
|
228
232
|
} };
|
|
229
233
|
|
|
230
234
|
},
|
|
@@ -212,11 +212,20 @@ export function createOrdinaryAssociationStore({
|
|
|
212
212
|
for (const taskId of views.keys()) if (!taskIds.has(taskId) && !inFlight.has(taskId)) views.delete(taskId);
|
|
213
213
|
const pending = [];
|
|
214
214
|
for (const candidate of sessions) {
|
|
215
|
-
|
|
215
|
+
let record;
|
|
216
|
+
try {
|
|
217
|
+
record = await ensure(
|
|
216
218
|
candidate,
|
|
217
219
|
records.get(`${candidate.provider}:${candidate.session}`) || null,
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
+
);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
// A stale workspace or unreadable local record affects only this
|
|
223
|
+
// optional association. Never rewrite its identity or stop the host.
|
|
224
|
+
candidate.actionAssociation = { status: error?.code === 'conflict' ? 'conflict' : 'unavailable',
|
|
225
|
+
binding: null, candidates: [], canCorrect: false, canReadBinding: false };
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const taskId = record.run?.managed?.taskId || record.associationTaskId;
|
|
220
229
|
candidate.taskId = taskId;
|
|
221
230
|
candidate.binding = { ...candidate.binding, taskId };
|
|
222
231
|
if (record.run?.managed) continue;
|
|
@@ -5,6 +5,7 @@ import { homedir } from 'node:os';
|
|
|
5
5
|
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
6
6
|
import { boundConversation, CONVERSATION_LIMITS, textContent } from './conversation-context.js';
|
|
7
7
|
import { sessionTitle } from './session-titles.js';
|
|
8
|
+
import { RECENT_SESSION_LIMIT, recentSessionWindow } from './discovery-window.js';
|
|
8
9
|
|
|
9
10
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
10
11
|
const MAX_PROJECTS = 256;
|
|
@@ -147,14 +148,14 @@ async function inspectTranscript(root, projectName, fileName, observedAt, includ
|
|
|
147
148
|
finally { await handle?.close(); }
|
|
148
149
|
}
|
|
149
150
|
|
|
150
|
-
async function collect({ home = homedir(), limit = 100, session = null, includeContext = false, cacheMetadata = false, includeStatistics = false, signal } = {}) {
|
|
151
|
+
async function collect({ home = homedir(), limit = 100, session = null, prioritySessions = [], includeContext = false, cacheMetadata = false, includeStatistics = false, signal } = {}) {
|
|
151
152
|
if (signal?.aborted) return [];
|
|
152
153
|
const root = await projectsRoot(home);
|
|
153
154
|
if (!root) return [];
|
|
154
|
-
const maximum = Number.isInteger(limit) ? Math.max(1, Math.min(limit,
|
|
155
|
+
const maximum = Number.isInteger(limit) ? Math.max(1, Math.min(limit, RECENT_SESSION_LIMIT)) : 100;
|
|
155
156
|
const observedAt = Date.now();
|
|
156
157
|
const result = new Map();
|
|
157
|
-
|
|
158
|
+
const descriptors = [], priorities = new Set(prioritySessions);
|
|
158
159
|
let projects;
|
|
159
160
|
try { projects = await readdir(root, { withFileTypes: true }); } catch { return []; }
|
|
160
161
|
for (const entry of projects.filter(item => item.isDirectory() && !item.isSymbolicLink()).slice(0, MAX_PROJECTS)) {
|
|
@@ -162,32 +163,47 @@ async function collect({ home = homedir(), limit = 100, session = null, includeC
|
|
|
162
163
|
const project = join(root, entry.name);
|
|
163
164
|
let files;
|
|
164
165
|
try { files = await readdir(project, { withFileTypes: true }); } catch { continue; }
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
166
|
+
const candidates = files.filter(file => file.isFile() && !file.isSymbolicLink() &&
|
|
167
|
+
file.name.endsWith('.jsonl') && UUID.test(file.name.slice(0, -'.jsonl'.length)) &&
|
|
168
|
+
(!session || file.name === `${session}.jsonl`));
|
|
169
|
+
let next = 0;
|
|
170
|
+
// Ranking uses file metadata only. Opening every transcript before slicing
|
|
171
|
+
// made a small visible catalog scan thousands of message prefixes at startup.
|
|
172
|
+
await Promise.all(Array.from({ length: Math.min(8, candidates.length) }, async () => {
|
|
173
|
+
while (next < candidates.length && !signal?.aborted) {
|
|
174
|
+
const file = candidates[next++];
|
|
175
|
+
try {
|
|
176
|
+
const info = await lstat(join(project, file.name));
|
|
177
|
+
if (info.isFile() && !info.isSymbolicLink() && info.size <= MAX_FILE_BYTES)
|
|
178
|
+
descriptors.push({ projectName: entry.name, fileName: file.name,
|
|
179
|
+
session: file.name.slice(0, -'.jsonl'.length), lastActivityAt: info.mtimeMs });
|
|
180
|
+
} catch { /* A disappearing transcript does not stop discovery. */ }
|
|
181
|
+
}
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
if (signal?.aborted) return [];
|
|
185
|
+
const selected = session ? descriptors : recentSessionWindow(descriptors, {
|
|
186
|
+
limit: maximum, priority: item => priorities.has(item.session),
|
|
187
|
+
});
|
|
188
|
+
for (const file of selected.slice(0, MAX_TRANSCRIPTS)) {
|
|
189
|
+
if (signal?.aborted) return [];
|
|
190
|
+
const candidate = await inspectTranscript(root, file.projectName, file.fileName, observedAt, includeContext, cacheMetadata, includeStatistics);
|
|
191
|
+
if (!candidate) continue;
|
|
192
|
+
const key = `${candidate.session}\0${candidate.cwd}${includeContext || includeStatistics ? `\0${file.projectName}\0${file.fileName}` : ''}`;
|
|
193
|
+
if (!result.has(key) || candidate.lastActivityAt > result.get(key).lastActivityAt) result.set(key, candidate);
|
|
178
194
|
}
|
|
179
195
|
const sorted = [...result.values()].sort((a, b) => b.lastActivityAt - a.lastActivityAt || a.session.localeCompare(b.session));
|
|
180
|
-
return session ? sorted : sorted
|
|
196
|
+
return session ? sorted : recentSessionWindow(sorted, { limit: maximum, priority: item => priorities.has(item.session) });
|
|
181
197
|
}
|
|
182
198
|
|
|
183
|
-
export async function listClaudeSessions({ home, limit, signal } = {}) {
|
|
184
|
-
return collect({ home, limit, signal, cacheMetadata: true });
|
|
199
|
+
export async function listClaudeSessions({ home, limit, prioritySessions, signal } = {}) {
|
|
200
|
+
return collect({ home, limit, prioritySessions, signal, cacheMetadata: true });
|
|
185
201
|
}
|
|
186
202
|
|
|
187
203
|
export async function readClaudeSession(session, { home } = {}) {
|
|
188
204
|
const id = typeof session === 'string' ? session : session?.session;
|
|
189
205
|
if (typeof id !== 'string' || !UUID.test(id)) return null;
|
|
190
|
-
const matches = await collect({ home, limit:
|
|
206
|
+
const matches = await collect({ home, limit: RECENT_SESSION_LIMIT, session: id });
|
|
191
207
|
const selected = typeof session === 'object' && typeof session.cwd === 'string'
|
|
192
208
|
? matches.filter(item => item.cwd === session.cwd) : matches;
|
|
193
209
|
return selected.length === 1 ? selected[0] : null;
|
|
@@ -5,6 +5,7 @@ import { CODEX_WRITER_LOCK_MINIMUM_VERSION, compareVersions, inspectBackendExecu
|
|
|
5
5
|
import { assessedCheckpointContract, deliveryProgressContext, understandingCheckpointContract } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
6
6
|
import { boundConversation, boundProviderGoal, textContent } from './conversation-context.js';
|
|
7
7
|
import { JsonDataUrlFilter } from './json-data-url-filter.js';
|
|
8
|
+
import { RECENT_SESSION_LIMIT } from './discovery-window.js';
|
|
8
9
|
|
|
9
10
|
const REQUEST_TIMEOUT = 15_000;
|
|
10
11
|
const MAX_LINE = 256 * 1024;
|
|
@@ -187,14 +188,15 @@ export function codexThreadMetadata(thread) {
|
|
|
187
188
|
};
|
|
188
189
|
}
|
|
189
190
|
|
|
190
|
-
export async function listCodexSessions({ client, maxPages = 40, pageSize = 25, maxSessions =
|
|
191
|
+
export async function listCodexSessions({ client, maxPages = 40, pageSize = 25, maxSessions = RECENT_SESSION_LIMIT, inspectState = false, prioritySessions = [], observationBudgetMs = 3000 } = {}) {
|
|
191
192
|
const sessions = [];
|
|
192
193
|
const windowSize = Math.min(maxSessions, maxPages * pageSize);
|
|
193
194
|
let cursor = null, listed = 0, pages = 0, batchSize = pageSize;
|
|
194
195
|
while (pages < Math.max(maxPages, Math.ceil(windowSize / batchSize)) && listed < windowSize) {
|
|
195
196
|
const limit = Math.min(batchSize, windowSize - listed);
|
|
196
197
|
let response;
|
|
197
|
-
try { response = await client.request('thread/list', { cursor, limit, sourceKinds: SOURCE_KINDS,
|
|
198
|
+
try { response = await client.request('thread/list', { cursor, limit, sourceKinds: SOURCE_KINDS,
|
|
199
|
+
archived: false, sortKey: 'updated_at', sortDirection: 'desc' }); }
|
|
198
200
|
catch (error) {
|
|
199
201
|
// Keep the transport's byte limit. Retry the same cursor in smaller
|
|
200
202
|
// batches rather than dropping discovery for large metadata envelopes.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { TERMINAL } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
2
|
+
|
|
3
|
+
export const RECENT_SESSION_LIMIT = 200;
|
|
4
|
+
|
|
5
|
+
// Explicit tracking and unsettled work remain visible outside recent history.
|
|
6
|
+
// Completed, untracked records are history, not an unlimited extra catalog.
|
|
7
|
+
export const retainedSession = record => Boolean(record?.autoContinue || record?.monitor || record?.tracked ||
|
|
8
|
+
record?.run && !TERMINAL.has(record.run.state));
|
|
9
|
+
|
|
10
|
+
export function recentSessionWindow(items, { limit = RECENT_SESSION_LIMIT, priority = () => false } = {}) {
|
|
11
|
+
const recent = [...items].sort((a, b) => (b.lastActivityAt || 0) - (a.lastActivityAt || 0) || a.session.localeCompare(b.session));
|
|
12
|
+
const retained = [], history = [];
|
|
13
|
+
for (const item of recent) (priority(item) ? retained : history).push(item);
|
|
14
|
+
return [...retained, ...history.slice(0, Math.max(0, limit - retained.length))];
|
|
15
|
+
}
|
|
@@ -5,6 +5,7 @@ import { listCodexSessions, openCodexAppServer, readCodexSession, CodexAppServer
|
|
|
5
5
|
import { listClaudeSessions, readClaudeSession } from './claude-sessions.js';
|
|
6
6
|
import { originalSessionTarget } from './original-session-target.js';
|
|
7
7
|
import { readCodexTitles, sessionTitle } from './session-titles.js';
|
|
8
|
+
import { RECENT_SESSION_LIMIT, recentSessionWindow, retainedSession } from './discovery-window.js';
|
|
8
9
|
|
|
9
10
|
function autoContinueState(record, isRunning, supported) {
|
|
10
11
|
if (record?.autoContinue !== true) return { autoContinueStatus: 'off', autoContinueReason: null };
|
|
@@ -58,28 +59,34 @@ export async function discoverSessions({ root, openClient = openCodexAppServer,
|
|
|
58
59
|
const assertActive = () => { if (signal?.aborted) throw new CodexAppServerError('cancelled', 'Conversation discovery was cancelled.'); };
|
|
59
60
|
assertActive();
|
|
60
61
|
const records = await listRecords(root);
|
|
62
|
+
const priorities = records.filter(retainedSession);
|
|
61
63
|
let discovered = [], codexError = null, claude = [], claudeError = null;
|
|
62
64
|
try { discovered = await withClient(openClient, client => listCodexSessions({ client, inspectState: true,
|
|
63
|
-
|
|
65
|
+
maxSessions: RECENT_SESSION_LIMIT,
|
|
66
|
+
prioritySessions: priorities.filter(record => record.provider === 'codex').map(record => record.session) }), signal); }
|
|
64
67
|
catch (error) { codexError = error?.code === 'unavailable' ? 'Codex executable unavailable.' : error?.code === 'unsupported' ? 'This Codex version lacks a verified app-server writer lock; update before enrollment.' : 'Codex conversation discovery unavailable.'; }
|
|
65
68
|
assertActive();
|
|
66
|
-
try { claude = await listClaude({ signal
|
|
69
|
+
try { claude = await listClaude({ signal, limit: RECENT_SESSION_LIMIT,
|
|
70
|
+
prioritySessions: priorities.filter(record => record.provider === 'claude').map(record => record.session) }); }
|
|
71
|
+
catch { claudeError = 'Claude conversation discovery unavailable.'; }
|
|
67
72
|
assertActive();
|
|
68
73
|
const byKey = new Map();
|
|
69
74
|
const recordsByKey = new Map(records.map(record => [`${record.provider}:${record.session}`, record]));
|
|
70
75
|
for (const item of [...discovered, ...claude]) byKey.set(`${item.provider}:${item.session}`, item);
|
|
71
76
|
for (const record of records) {
|
|
72
77
|
const key = `${record.provider}:${record.session}`;
|
|
73
|
-
if (!byKey.has(key) && (record.observedAt || record.admission || record
|
|
78
|
+
if (!byKey.has(key) && (record.observedAt || record.admission || retainedSession(record))) byKey.set(key, {
|
|
74
79
|
provider: record.provider, session: record.session, cwd: record.cwd, title: '', source: record.admission?.source || 'hook',
|
|
75
80
|
lastActivityAt: record.observedAt || record.admission?.validatedAt || record.run?.updatedAt || null,
|
|
76
81
|
observation: { source: record.admission?.source || `${record.provider}_hook`, observedAt: record.observedAt || record.admission?.validatedAt || null },
|
|
77
82
|
availability: 'unknown', model: record.admission?.model || null,
|
|
78
83
|
});
|
|
79
84
|
}
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
const items = [...byKey.values()],
|
|
85
|
+
// Select recent history before live probes, keeping explicit supervision
|
|
86
|
+
// visible even during a truncated discovery or provider outage.
|
|
87
|
+
const items = recentSessionWindow([...byKey.values()], {
|
|
88
|
+
priority: item => retainedSession(recordsByKey.get(`${item.provider}:${item.session}`)),
|
|
89
|
+
}), projected = new Array(items.length);
|
|
83
90
|
const codexTitles = items.some(item => item.provider === 'codex' && !sessionTitle(item.title)) ? await listCodexTitles() : new Map();
|
|
84
91
|
assertActive();
|
|
85
92
|
let nextItem = 0;
|
|
@@ -136,11 +143,7 @@ export async function discoverSessions({ root, openClient = openCodexAppServer,
|
|
|
136
143
|
}));
|
|
137
144
|
assertActive();
|
|
138
145
|
projected.sort((a, b) => (b.lastActivityAt || 0) - (a.lastActivityAt || 0));
|
|
139
|
-
|
|
140
|
-
// outage, even when newer unbound conversations fill the candidate window.
|
|
141
|
-
const bound = projected.filter(item => item.autoContinue || item.binding.mode !== 'unmanaged');
|
|
142
|
-
const sessions = [...bound, ...projected.filter(item => !item.autoContinue && item.binding.mode === 'unmanaged').slice(0, Math.max(0, 200 - bound.length))];
|
|
143
|
-
return { sessions, providers: { codex: { discovery: codexError ? 'unavailable' : 'ready', reason: codexError }, claude: { discovery: claudeError ? 'unavailable' : 'local_index', reason: claudeError } }, refreshedAt: now };
|
|
146
|
+
return { sessions: projected, providers: { codex: { discovery: codexError ? 'unavailable' : 'ready', reason: codexError }, claude: { discovery: claudeError ? 'unavailable' : 'local_index', reason: claudeError } }, refreshedAt: now };
|
|
144
147
|
}
|
|
145
148
|
|
|
146
149
|
// Explicit execution authority: legacy monitor/tracked records stay read-only.
|