@awak-app/simy-cli 0.6.2 → 0.6.5

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.
@@ -1,6 +1,6 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
- import { canReplanDelivery, deliveryBudgetState, managedBudgetReason, reconcileRecords, rootDirectory } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
3
+ import { canReplanDelivery, deliveryStop, deliveryStopBlocksResume, reconcileRecords, rootDirectory } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
4
4
  import { originalSessionTarget } from '../session-supervision/original-session-target.js';
5
5
 
6
6
  export const HOOK_SETUP = Object.freeze({
@@ -31,17 +31,11 @@ export async function deliveryHookStatus({ root = rootDirectory(), now = Date.no
31
31
  runs: records.filter(r => r.run).sort((a, b) => b.run.updatedAt - a.run.updatedAt).slice(0, 30).map(r => ({
32
32
  provider: r.provider, session: r.session, originalSessionTarget: originalSessionTarget(r.provider, r.session),
33
33
  objective: r.run.objective, state: r.run.state, reason: r.run.reason,
34
- continuations: r.run.continuations, maxContinuations: null, deadline: null,
35
- localExecutionLimits: false,
34
+ continuations: r.run.continuations,
36
35
  continuationPolicy: r.run.managed?.continuationPolicy || 'bounded',
37
36
  startedAt: r.run.startedAt,
38
- continuationLimits: deliveryBudgetState(r.run, now),
39
- canChangeContinuation: false,
40
- hardDeadline: null, hardMaxContinuations: null,
41
- budgetWarnings: deliveryBudgetState(r.run, now).warnings,
42
- budgetCapability: r.run.managed?.budgetCapability || null,
37
+ stopCode: deliveryStop(r.run, now)?.code ?? null,
43
38
  outcomeProgress: r.run.managed?.outcomeProgress || null,
44
- budgetExtensionHistory: r.run.budgetExtensionHistory || [],
45
39
  needsAttention: ['blocked', 'failed', 'recovery_required', 'budget_exhausted', 'verification_pending'].includes(r.run.state), updatedAt: r.run.updatedAt,
46
40
  managed: Boolean(r.run.managed), taskId: r.run.managed?.taskId || null, actionId: r.run.managed?.actionId || null,
47
41
  actionDueAt: r.run.managed?.actionDueAt ?? null,
@@ -56,8 +50,6 @@ export async function deliveryHookStatus({ root = rootDirectory(), now = Date.no
56
50
  handoff: r.run.managed?.handoff || null, threadHistory: r.run.managed?.threadHistory || [],
57
51
  progressBudget: r.run.managed ? { minutes: r.run.managed.maxNoProgressMinutes, lastVerifiedProgressAt: r.run.managed.lastVerifiedProgressAt,
58
52
  attempts: r.run.managed.noProgress, maxAttempts: r.run.managed.maxNoProgress } : null,
59
- tokenBudget: r.run.managed?.maxTokens ? { limit: r.run.managed.maxTokens, used: r.run.managed.tokensUsed,
60
- usageAvailable: r.run.managed.usageAvailable, source: 'provider_observed', dollarCost: null } : null,
61
53
  pendingApproval: r.run.managed?.pendingApproval || null,
62
54
  autonomyPolicy: r.run.managed?.autonomyPolicy || null, plan: r.run.managed?.plan || null,
63
55
  decision: r.run.managed?.decision || null, decisionHistory: r.run.managed?.decisionHistory || [],
@@ -77,14 +69,14 @@ export async function deliveryHookStatus({ root = rootDirectory(), now = Date.no
77
69
  r.run.managed.resumeChannel === 'codex_app_server' && r.run.managed.autoRecovery),
78
70
  canResume: ['blocked', 'recovery_required', 'paused'].includes(r.run.state) && r.run.managed?.phase !== 'needs_clarification' &&
79
71
  !r.run.managed?.pendingApproval && !r.run.managed?.dependencyWait && !['pending', 'pending_received', 'transferred'].includes(r.run.managed?.handoff?.status) &&
80
- !managedBudgetReason(r.run, now),
72
+ !deliveryStopBlocksResume(r.run, now),
81
73
  canApprove: r.run.state === 'blocked' && Boolean(r.run.managed?.pendingApproval) && !r.run.managed.pendingApproval.denied,
82
74
  canReject: r.run.state === 'blocked' && Boolean(r.run.managed?.pendingApproval) && !r.run.managed.pendingApproval.denied,
83
75
  canReplan: canReplanDelivery(r.run, r.provider, now),
84
76
  canAnswer: r.run.managed?.phase === 'needs_clarification' && r.run.state === 'blocked' && Boolean(r.run.managed.understandingQuestion?.id) &&
85
- !managedBudgetReason(r.run, now),
77
+ !deliveryStopBlocksResume(r.run, now),
86
78
  canResolveDependency: r.run.state === 'blocked' && r.run.blocker === 'external_dependency' && Boolean(r.run.managed?.dependencyWait?.id) &&
87
- !managedBudgetReason(r.run, now) && !['pending', 'pending_received', 'transferred'].includes(r.run.managed?.handoff?.status),
79
+ !deliveryStopBlocksResume(r.run, now) && !['pending', 'pending_received', 'transferred'].includes(r.run.managed?.handoff?.status),
88
80
  canSetPolicy: Boolean(r.run.managed?.autonomyPolicy) && !['completed', 'reported_done', 'stopped', 'failed', 'budget_exhausted'].includes(r.run.state),
89
81
  })),
90
82
  };
@@ -1,14 +1,14 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { dirname } from 'node:path';
4
- import { automaticContinuationAllowed, assessedCheckpointContract, checkpointDelivery, decideStop, deliveryBudgetState, listRecords, readRecord, managedBudgetReason, managedDispatchDeadline, notifyState, processIdentity, reconcileRecords, updateRecord, verifyAutomaticCriteria } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
4
+ import { automaticContinuationAllowed, assessedCheckpointContract, checkpointDelivery, decideStop, deliveryProgressContext, deliveryStop, listRecords, readRecord, managedBudgetReason, notifyState, processIdentity, reconcileRecords, updateRecord, verifyAutomaticCriteria } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
5
5
  import { resolveBackendExecutable } from '../backend-executable.js';
6
6
  import { executeCodexAppServer } from '../session-supervision/codex-app-server.js';
7
7
  import { acquireWorkspaceLease, leaseOwnerDead } from '../session-supervision/workspace-lease.js';
8
8
 
9
9
  const MAX_OUTPUT = 64 * 1024;
10
10
  const SAFE_PROVIDER_CODES = new Set(['identity', 'permission_narrowed', 'permission_restore', 'turn_failed', 'checkpoint',
11
- 'closed', 'timeout', 'unsupported', 'unavailable', 'manual_provider_action', 'checkpoint_rejected', 'provider_approval', 'usage_unavailable']);
11
+ 'closed', 'timeout', 'unsupported', 'unavailable', 'manual_provider_action', 'checkpoint_rejected', 'provider_approval']);
12
12
  function safeProviderCode(code) { return SAFE_PROVIDER_CODES.has(code) ? code : 'provider_error'; }
13
13
  function providerFailureReason(code) {
14
14
  if (code === 'permission_restore') return 'Codex sandbox restoration could not be confirmed; inspect the original conversation before resuming.';
@@ -40,9 +40,8 @@ async function assertActiveLease(root, provider, session, leaseId, signal, curre
40
40
  if (signal?.aborted || current?.run?.state !== 'auto_resuming' || current.run.managed?.resumeLease?.id !== leaseId) {
41
41
  throw new Error('Managed recovery lease changed before provider dispatch.');
42
42
  }
43
- if (currentTime() >= managedDispatchDeadline(current.run)) throw new Error('Managed recovery deadline expired before provider dispatch.');
44
43
  if (!automaticContinuationAllowed(current)) throw new Error('Automatic continuation was disabled before provider dispatch.');
45
- if (managedBudgetReason(current.run, currentTime())) throw new Error('Managed progress budget expired before provider dispatch.');
44
+ if (managedBudgetReason(current.run, currentTime())) throw new Error('Managed task stopped before provider dispatch.');
46
45
  const managed = current.run.managed;
47
46
  const approvedWait = managed.decision?.outcome === 'wait' && managed.approvedAction?.id === managed.decision.id && !managed.approvedAction.dispatchedAt;
48
47
  if (managed.autonomyPolicy && ((managed.decision?.outcome === 'wait' && !approvedWait) || managed.resumeLease.decisionId !== managed.decision?.id)) throw new Error('Assessed action changed before provider dispatch.');
@@ -80,13 +79,11 @@ export async function executeCodexResume(record, { signal, root, leaseId, execut
80
79
  lastDependencyResolution: run.managed.lastDependencyResolution,
81
80
  remainingWork: run.managed.remainingWork, acceptanceCriteria: run.managed.acceptanceCriteria, proposedAction: run.managed.proposedAction,
82
81
  currentDecision: run.managed.autonomyPolicy ? run.managed.decision : undefined, diagnosis: run.managed.plan?.diagnosis,
83
- budgets: { ...deliveryBudgetState(run), continuations: run.continuations, failures: run.failures, maxFailures: run.maxFailures,
84
- localExecutionLimits: false } };
82
+ progress: deliveryProgressContext(run) };
85
83
  const prompt = run.managed.autonomyPolicy ? `Continue the existing SIMY managed task in this exact session. Context: ${JSON.stringify(context)}\nExecute ONLY currentDecision.action. Its expectedVerification and rollback apply to this turn. Stay within provider permissions. End with ONLY one JSON checkpoint. ${assessedCheckpointContract(run.managed.decision)} Do not write SIMY state directly.` : `Continue the existing SIMY managed task in this exact session. Context: ${JSON.stringify(context)}\nStay within the original task and provider permissions. End with ONLY one JSON object: {"status":"in_progress|completed|blocked|error","summary":"...","nextStep":"...","remainingWork":"...","proposedAction":{"kind":"read|local_edit|local_test|external|destructive|scope_change|unknown","scope":"...","summary":"..."},"evidence":[],"acceptanceMet":false}. For blocked include blocker (authorization, missing_information, external_dependency), and for authorization include approval {kind,scope,summary}. Do not claim completion without checking frozen acceptance. SIMY independently verifies supported criteria. Do not write SIMY state directly. If approval or input is needed, return blocked and stop.`;
86
84
  const sandbox = run.managed.autonomyPolicy && run.managed.decision?.action.kind === 'read' ? 'read-only' : run.managed.autoRecoverySandbox;
87
85
  const args = ['exec', '--json', '--skip-git-repo-check', '-s', sandbox, '-m', run.managed.resumeModel, 'resume', record.session, prompt];
88
86
  if (signal?.aborted) throw new Error('Managed recovery cancelled before provider dispatch.');
89
- if (currentTime() >= managedDispatchDeadline(run)) throw new Error('Managed recovery deadline expired before provider dispatch.');
90
87
  await beforeDispatch();
91
88
  if (signal?.aborted) throw new Error('Managed recovery cancelled before provider dispatch.');
92
89
 
@@ -196,9 +193,9 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
196
193
  const approved = live?.managed?.decision?.outcome === 'wait' && ['read', 'local_edit', 'local_test'].includes(live.managed.decision.action?.kind) &&
197
194
  live.managed.approvedAction?.id === live.managed.decision.id && !live.managed.approvedAction.dispatchedAt;
198
195
  if (!live?.managed?.autoRecovery || live.state !== 'recovery_required' || !allowedCause || live.managed.pendingApproval || live.managed.autonomyPolicy && (!live.managed.decision || live.managed.decision.outcome === 'wait' && !approved) || appChannel && live.managed.nextAttemptAt > currentTime()) return current;
199
- const budgetReason = managedBudgetReason(live, currentTime());
200
- if (budgetReason) {
201
- live.state = budgetReason.startsWith('No verified') ? 'paused' : 'failed'; live.reason = budgetReason; live.updatedAt = currentTime(); notifyState(live, live.updatedAt); return current;
196
+ const stop = deliveryStop(live, currentTime());
197
+ if (stop) {
198
+ live.state = stop.code === 'failure_limit' ? 'failed' : 'paused'; live.reason = stop.reason; live.updatedAt = currentTime(); notifyState(live, live.updatedAt); return current;
202
199
  }
203
200
  live.state = 'auto_resuming'; live.managed.resumeLease = { id: leaseId, claimedAt: currentTime(), checkpointRevision: live.revision, decisionId: live.managed.decision?.id || null,
204
201
  owner: { pid: process.pid, identity: ownerIdentity } };
@@ -217,7 +214,6 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
217
214
  let result;
218
215
  let resource;
219
216
  let lastActivityAt = currentTime();
220
- let usage = null;
221
217
  let dispatched = false, waitingForTool = false;
222
218
  const controller = new AbortController();
223
219
  const abort = () => controller.abort();
@@ -262,10 +258,6 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
262
258
  const runner = execute || (appServer ? executeCodexAppServer : executeCodexResume);
263
259
  result = await runner(claimed, { signal: controller.signal, root, leaseId, currentTime, leaseMonitored: true,
264
260
  onActivity: observation => { lastActivityAt = currentTime(); waitingForTool = observation?.waitingForTool === true; },
265
- onUsage: observed => {
266
- usage = observed;
267
-
268
- },
269
261
  beforeDispatch: async providerSandbox => {
270
262
  await assertActiveLease(root, record.provider, record.session, leaseId, controller.signal, currentTime);
271
263
  await updateRecord(root, record.provider, record.session, current => {
@@ -298,9 +290,6 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
298
290
  errorCode: error?.code || 'provider_error', restorationUnverified: error?.restorationUnverified === true }; }
299
291
  finally { stopWatch(); signal?.removeEventListener('abort', abort); }
300
292
  if (controller.signal.aborted) result = { ...result, code: -2, cancelled: true };
301
- if (deliveryBudgetState(claimed.run).tokenCeiling && !usage && !result.busy && dispatched) {
302
- result = { ...result, usageUnavailable: true };
303
- }
304
293
  if (result.code === 0) {
305
294
  try {
306
295
  await checkpointDelivery({ root, provider: record.provider, session: record.session, cwd: record.cwd,
@@ -327,14 +316,6 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
327
316
  const live = current?.run;
328
317
  if (live?.managed?.resumeLease?.id !== leaseId) return current;
329
318
  delete live.managed.resumeLease;
330
- delete live.managed.usageBlocked;
331
- if (usage) {
332
- live.managed.tokensUsed = (live.managed.tokensUsed || 0) + usage.tokens;
333
- live.managed.usageAvailable = true;
334
- live.managed.lastUsage = { ...usage, attemptId: leaseId };
335
- } else if (!result.busy && deliveryBudgetState(live).tokenCeiling && dispatched) {
336
- live.managed.usageAvailable = false; live.managed.usageBlocked = true;
337
- }
338
319
  const attempt = live.managed.executionAttempt;
339
320
  if (!result.busy && attempt?.phase === 'dispatching' && attempt.action?.kind !== 'read') live.managed.executionUncertain = {
340
321
  attemptId: leaseId, decisionId: attempt.decisionId, action: attempt.action, at: Date.now() };
@@ -351,13 +332,8 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
351
332
  if (live.state !== 'auto_resuming') live.reason = `${String(live.reason || '').slice(0, 400)} ${live.managed.providerWarning}`.trim();
352
333
  }
353
334
  if (live.state !== 'auto_resuming') {
354
- if (live.managed.usageBlocked && !['stopped', 'paused', 'completed', 'reported_done'].includes(live.state)) {
355
- live.managed.autoRecovery = false;
356
- live.reason = `${live.reason} Provider token usage is unavailable; further budgeted turns are blocked.`;
357
- }
358
335
  live.updatedAt = Date.now(); notifyState(live, live.updatedAt); return current;
359
336
  }
360
- live.budgetWarnings = deliveryBudgetState(live).warnings;
361
337
  if (result.busy) {
362
338
  if (result.accessDenied && dispatched) {
363
339
  // The final guard rejected before the provider call. Keep its one-shot approval unused.
@@ -9,6 +9,7 @@ import { deliveryHookStatus, openHookSetup } from "./delivery-hooks/desktop.js";
9
9
  import { controlDelivery, verifyDelivery, createRecordReader } from "./codex-plugins/simy/hooks/delivery-loop.mjs";
10
10
  import { createManagedSupervisor } from "./delivery-hooks/supervisor.js";
11
11
  import { createActionAssociationHost } from "./session-supervision/action-association-host.js";
12
+ import { createStopNotifier } from "./session-supervision/stop-notifications.js";
12
13
  import { answerSession, continueSession, discoverSessions, enrollSession, monitorSession, trackSession, setSessionAutoContinue } from "./session-supervision/index.js";
13
14
  import { createContinuationAnalysisManager } from "./session-supervision/analysis-manager.js";
14
15
  import { analyzeContinuation, readContinuationContext } from "./session-supervision/continuation-analysis.js";
@@ -95,6 +96,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
95
96
  const makeActionAssociations = () => (dependencies.createActionAssociationHost || createActionAssociationHost)({ root: join(root, "delivery-hooks"), sessionRoot: root, webOrigin, list: () => recordReader.list(), onChange: () => { if (!closed) setImmediate(() => { void publish().catch(() => {}); }); } });
96
97
  let actionAssociations = makeActionAssociations();
97
98
  const statisticsCache = new Map();
99
+ const stopNotifier = createStopNotifier({ emit: event => { if (!closed && !relay) emit(event); } });
98
100
  let currentAccess = { state: 'checking', organizationId: null, organizationName: null };
99
101
  const accessScope = access => JSON.stringify([access?.revision ?? null, access?.organizationId ?? null]);
100
102
  const setAccessSnapshot = async access => {
@@ -105,7 +107,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
105
107
  discoveryController?.abort();
106
108
  sessionRefreshAgain = Boolean(sessionRefresh) && desktopAccessReady(currentAccess);
107
109
  for (const item of sessionStatus.sessions) automaticContinuation?.revoke(item.provider, item.session);
108
- sessionStatus = { sessions: [], providers: {}, refreshedAt: null }; sessionRefreshedAt = 0;
110
+ sessionStatus = { sessions: [], providers: {}, refreshedAt: null, initialLoading: true }; sessionRefreshedAt = 0;
109
111
  await analysisManager?.update([]);
110
112
  }
111
113
  return currentAccess;
@@ -134,7 +136,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
134
136
  beforeClaim: record => actionAssociations.prepareCached?.(record) || record });
135
137
  let shuttingDown = null;
136
138
  let commands;
137
- let sessionStatus = { sessions: [], providers: { codex: { discovery: "loading", reason: null }, claude: { discovery: "loading", reason: null } }, refreshedAt: null };
139
+ let sessionStatus = { sessions: [], providers: { codex: { discovery: "loading", reason: null }, claude: { discovery: "loading", reason: null } }, refreshedAt: null, initialLoading: true };
138
140
  let sessionRefresh = null, sessionRefreshedAt = 0, sessionRefreshAgain = false, discoveryController = null, discoveryStopped = false;
139
141
  const stopDiscovery = async ({ drain = false } = {}) => {
140
142
  discoveryController?.abort();
@@ -158,8 +160,8 @@ export async function startDesktopHost({ input = process.stdin, output = process
158
160
  sessionRefreshAgain = desktopAccessReady(latestAccess);
159
161
  return;
160
162
  }
161
- await analysisManager.update(value.sessions); sessionStatus = value; sessionRefreshedAt = Date.now(); })
162
- .catch(() => { if (closed || relay || controller.signal.aborted) return; sessionStatus = { ...sessionStatus, providers: { codex: { discovery: "unavailable", reason: "Conversation discovery unavailable." }, claude: { discovery: "unavailable", reason: "Conversation discovery unavailable." } } }; sessionRefreshedAt = Date.now(); })
163
+ await analysisManager.update(value.sessions); sessionStatus = { ...value, initialLoading: false }; sessionRefreshedAt = Date.now(); })
164
+ .catch(() => { if (closed || relay || controller.signal.aborted) return; sessionStatus = { ...sessionStatus, initialLoading: false, providers: { codex: { discovery: "unavailable", reason: "Conversation discovery unavailable." }, claude: { discovery: "unavailable", reason: "Conversation discovery unavailable." } } }; sessionRefreshedAt = Date.now(); })
163
165
  .finally(() => { if (discoveryController === controller) discoveryController = null; sessionRefresh = null; if (sessionRefreshAgain && !closed && !relay) { sessionRefreshAgain = false; refreshSessions(true); }
164
166
  if (!closed) setImmediate(() => { void publish().catch(() => {}); }); });
165
167
  }
@@ -244,6 +246,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
244
246
  }
245
247
  await analysisManager.updateProviderStatuses?.(status.providers);
246
248
  const records = await recordReader.list();
249
+ stopNotifier.observe(records);
247
250
  const projection = await actionAssociations.beginProjection?.({ records });
248
251
  status.deliveryHooks = await (dependencies.deliveryHookStatus || deliveryHookStatus)({ root: deliveryRoot, records: projection?.records || records }).catch(() => ({ error: "Delivery hook status unavailable", providers: {}, runs: [] }));
249
252
  await actionAssociations.decorate(status.deliveryHooks, projection);
@@ -459,7 +462,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
459
462
  if (command.force !== true) throw new Error("Confirm before stopping this task.");
460
463
  await agent.stopDesktopTask(command.taskKey);
461
464
  emit({ type: "result", id: command.id, ok: true });
462
- void publish(); return;
465
+ void publish().catch(() => emit({ type: "error", message: "SIMY status could not be refreshed." })); return;
463
466
  }
464
467
  const access = await accessSnapshot();
465
468
  const commandScope = accessScope(access);
@@ -479,7 +482,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
479
482
  statisticsCache.clear(); // Only the selected conversation needs chart data.
480
483
  statisticsCache.set(`${candidate.provider}:${candidate.session}`, statistics);
481
484
  emit({ type: "result", id: command.id, ok: true });
482
- void publish(); return;
485
+ void publish().catch(() => emit({ type: "error", message: "SIMY status could not be refreshed." })); return;
483
486
  }
484
487
  if (action === "my_ai_session") {
485
488
  const ticket = await agent.createDesktopWebSession();
@@ -573,9 +576,9 @@ export async function startDesktopHost({ input = process.stdin, output = process
573
576
  else if (action === "codex_hooks" || action === "claude_hooks") {
574
577
  const provider = action === "codex_hooks" ? "codex" : "claude";
575
578
  await (dependencies.openHookSettings || openHookSetup)(provider);
576
- } else if (["delivery_stop", "delivery_resume", "delivery_pause", "delivery_approve", "delivery_reject", "delivery_replan", "delivery_set_policy", "delivery_dependency_ready", "delivery_extend", "delivery_continuation_policy"].includes(action)) {
579
+ } else if (["delivery_stop", "delivery_resume", "delivery_pause", "delivery_approve", "delivery_reject", "delivery_replan", "delivery_set_policy", "delivery_dependency_ready", "delivery_continuation_policy"].includes(action)) {
577
580
  await controlDelivery({ root: join(root, "delivery-hooks"), provider: command.provider, session: command.session,
578
- taskId: command.taskId, approvalId: command.approvalId, autonomyPolicy: command.autonomyPolicy, limits: command.limits, continuationPolicy: command.continuationPolicy, waitId: command.waitId, answer: command.answer, action: action.slice("delivery_".length) });
581
+ taskId: command.taskId, approvalId: command.approvalId, autonomyPolicy: command.autonomyPolicy, continuationPolicy: command.continuationPolicy, waitId: command.waitId, answer: command.answer, action: action.slice("delivery_".length) });
579
582
  } else if (action === "delivery_verify") {
580
583
  await verifyDelivery({ root: join(root, "delivery-hooks"), provider: command.provider, session: command.session,
581
584
  taskId: command.taskId, criterionId: command.criterionId });
@@ -591,7 +594,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
591
594
  }
592
595
  if (command.enabled) await authorizeWork(commandScope);
593
596
  await (dependencies.setSessionAutoContinue || setSessionAutoContinue)({ root: join(root, 'delivery-hooks'),
594
- provider: command.provider, session: command.session, enabled: command.enabled, continuationPolicy: command.continuationPolicy, hardMaxMinutes: command.hardMaxMinutes, hardMaxContinuations: command.hardMaxContinuations, providerTokenCeiling: command.providerTokenCeiling });
597
+ provider: command.provider, session: command.session, enabled: command.enabled, continuationPolicy: command.continuationPolicy });
595
598
  if (!command.enabled) automaticContinuation.revoke(command.provider, command.session);
596
599
  // Acknowledge the durable preference before discovery or analysis.
597
600
  emit({ type: "result", id: command.id, ok: true });
@@ -626,8 +629,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
626
629
  } else if (action === "session_enroll") {
627
630
  await (dependencies.enrollSession || enrollSession)({ root: join(root, "delivery-hooks"), provider: command.provider,
628
631
  session: command.session, objective: command.objective, acceptanceCriteria: command.acceptanceCriteria,
629
- autonomyPolicy: command.autonomyPolicy, maxMinutes: command.maxMinutes,
630
- maxContinuations: command.maxContinuations, maxFailures: command.maxFailures, maxNoProgressMinutes: command.maxNoProgressMinutes, maxTokens: command.maxTokens, continuationPolicy: command.continuationPolicy, hardMaxMinutes: command.hardMaxMinutes, hardMaxContinuations: command.hardMaxContinuations, providerTokenCeiling: command.providerTokenCeiling });
632
+ autonomyPolicy: command.autonomyPolicy, maxFailures: command.maxFailures, maxNoProgressMinutes: command.maxNoProgressMinutes, continuationPolicy: command.continuationPolicy });
631
633
  await refreshSessions(true);
632
634
  } else if (action === "session_analyze") {
633
635
  const candidate = await selectedSession(command, { reuseDiscovery: true });
@@ -639,9 +641,8 @@ export async function startDesktopHost({ input = process.stdin, output = process
639
641
  await continueAnalyzedSession({ root: join(root, 'delivery-hooks'), candidate, manager,
640
642
  readContext: dependencies.readContinuationContext || readContinuationContext,
641
643
  enroll: async options => { await authorizeWork(commandScope); return guardedEnroll(options); }, beforeEnroll: () => authorizeWork(commandScope),
642
- settings: { autonomyPolicy: command.autonomyPolicy, maxMinutes: command.maxMinutes,
643
- maxContinuations: command.maxContinuations, maxFailures: command.maxFailures,
644
- maxNoProgressMinutes: command.maxNoProgressMinutes, maxTokens: command.maxTokens, continuationPolicy: command.continuationPolicy, hardMaxMinutes: command.hardMaxMinutes, hardMaxContinuations: command.hardMaxContinuations, providerTokenCeiling: command.providerTokenCeiling } });
644
+ settings: { autonomyPolicy: command.autonomyPolicy, maxFailures: command.maxFailures,
645
+ maxNoProgressMinutes: command.maxNoProgressMinutes, continuationPolicy: command.continuationPolicy } });
645
646
  await refreshSessions(true);
646
647
  } else if (action === "session_answer") {
647
648
  await (dependencies.answerSession || answerSession)({ root: join(root, "delivery-hooks"), provider: command.provider,
@@ -160,7 +160,7 @@ export function createActionAssociationHost({ root, sessionRoot, webOrigin, api
160
160
  if (closed) return;
161
161
  const managed = record.run.managed, job = managed.actionAssociation?.job;
162
162
  if (job && !managed.resumeLease && (!expected || TERMINAL.has(record.run.state) || ['paused', 'blocked'].includes(record.run.state) || record.run.state === 'recovery_required' && record.run.recoveryCause !== 'session_supervision' ||
163
- managedBudgetReason(record.run, now()) || now() >= record.run.deadline || record.run.continuations >= record.run.maxContinuations ||
163
+ managedBudgetReason(record.run, now()) ||
164
164
  managed.understandingRevision !== job.understandingRevision || record.run.revision !== job.runRevision || managed.actionAssociation.invalidated)) {
165
165
  await cancelActionAssociation({ root, ...identity(record), expectedJobId: job.id, now: now() });
166
166
  }
@@ -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 => records.get(`${item.provider}:${item.session}`)?.run?.managed).map(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
  },
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { listRecords, updateRecord, TERMINAL } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
2
+ import { listRecords, managedBudgetReason, updateRecord, TERMINAL } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
3
3
  import { associationSignals, associationRevision } from '../codex-plugins/simy/hooks/action-association.mjs';
4
4
  export { associationSignals, associationRevision, associationContract, validateAssociationResult } from '../codex-plugins/simy/hooks/action-association.mjs';
5
5
 
@@ -25,7 +25,7 @@ export async function queueActionAssociation({ root, provider, session, taskId,
25
25
  await assertScope(validate);
26
26
  const run = record?.run, managed = run?.managed;
27
27
  if (!managed || managed.taskId !== taskId || run.revision !== expectedRevision) return record;
28
- if (!stable(run) || now >= run.deadline || run.continuations >= run.maxContinuations) {
28
+ if (!stable(run) || managedBudgetReason(run, now)) {
29
29
  if (!managed.actionAssociation?.job && !managed.actionAssociation?.outbox && !managed.actionAssociation?.manualOverride) managed.actionAssociation = { ...managed.actionAssociation, status: 'waiting_boundary', retryOn: 'original_session_safe_boundary', updatedAt: now };
30
30
  return record;
31
31
  }
@@ -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
- const record = await ensure(
215
+ let record;
216
+ try {
217
+ record = await ensure(
216
218
  candidate,
217
219
  records.get(`${candidate.provider}:${candidate.session}`) || null,
218
- ),
219
- taskId = record.run?.managed?.taskId || record.associationTaskId;
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, 200)) : 100;
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
- let inspected = 0;
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
- for (const file of files) {
166
- if (signal?.aborted) return [];
167
- if (inspected >= MAX_TRANSCRIPTS) break;
168
- if (!file.isFile() || file.isSymbolicLink() || !UUID.test(file.name.replace(/\.jsonl$/, '')) || !file.name.endsWith('.jsonl')) continue;
169
- const id = file.name.slice(0, -'.jsonl'.length);
170
- if (session && id !== session) continue;
171
- inspected++;
172
- const candidate = await inspectTranscript(root, entry.name, file.name, observedAt, includeContext, cacheMetadata, includeStatistics);
173
- if (!candidate) continue;
174
- const key = `${candidate.session}\0${candidate.cwd}${includeContext || includeStatistics ? `\0${entry.name}\0${file.name}` : ''}`;
175
- if (!result.has(key) || candidate.lastActivityAt > result.get(key).lastActivityAt) result.set(key, candidate);
176
- }
177
- if (inspected >= MAX_TRANSCRIPTS) break;
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.slice(0, maximum);
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: 200, session: id });
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;
@@ -2,9 +2,10 @@ import { associationContract } from '../codex-plugins/simy/hooks/action-associat
2
2
  import { spawn } from 'node:child_process';
3
3
  import { realpath } from 'node:fs/promises';
4
4
  import { CODEX_WRITER_LOCK_MINIMUM_VERSION, compareVersions, inspectBackendExecutable } from '../backend-executable.js';
5
- import { assessedCheckpointContract, deliveryBudgetState, understandingCheckpointContract } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
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 = 200, inspectState = false, prioritySessions = [], observationBudgetMs = 3000 } = {}) {
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, archived: false }); }
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.
@@ -394,7 +396,7 @@ export function isCodexWriterBusy(error) {
394
396
  return error instanceof CodexAppServerError && error.code === -32600 && /already has an active writer/i.test(error.message);
395
397
  }
396
398
 
397
- export async function executeCodexAppServer(record, { openClient = openCodexAppServer, signal, beforeDispatch = async () => {}, onActivity = () => {}, onUsage = () => {} } = {}) {
399
+ export async function executeCodexAppServer(record, { openClient = openCodexAppServer, signal, beforeDispatch = async () => {}, onActivity = () => {} } = {}) {
398
400
  const run = record.run, decision = run.managed?.decision;
399
401
  if (!decision || !run.managed.autonomyPolicy) throw new CodexAppServerError('decision', 'An assessed action is required before Codex dispatch.');
400
402
  if (!['read', 'local_edit', 'local_test'].includes(decision.action?.kind)) throw new CodexAppServerError('manual_provider_action', 'This action must be completed in the original provider conversation.');
@@ -405,7 +407,6 @@ export async function executeCodexAppServer(record, { openClient = openCodexAppS
405
407
  const client = await openClient({ signal });
406
408
  let originalSandbox = null, overrideStarted = false, activeTurnId = null, turnCompleted = false, turnStart = null;
407
409
  let primaryError = null;
408
- let usage = null, lastTotal = null;
409
410
  try {
410
411
  const metadata = await readCodexSession(client, record.session);
411
412
  if (await realpath(metadata.cwd) !== record.cwd || metadata.model !== run.managed.resumeModel) throw new CodexAppServerError('identity', 'Codex session workspace or model changed.');
@@ -422,8 +423,7 @@ export async function executeCodexAppServer(record, { openClient = openCodexAppS
422
423
  enrolledScopes: run.managed.approvalScopes,
423
424
  lastDependencyResolution: run.managed.lastDependencyResolution,
424
425
  ...(authorizedWait ? { approvedAction: { approvalId: approved.id, action: approved.action, oneShot: true } } : {}),
425
- budgets: { ...deliveryBudgetState(run), continuations: run.continuations, failures: run.failures, maxFailures: run.maxFailures,
426
- localExecutionLimits: false } };
426
+ progress: deliveryProgressContext(run) };
427
427
  const approvalInstruction = authorizedWait ? `SIMY recorded a matching, one-shot scoped user approval ${approved.id} for this exact selected action. The decision outcome "wait" describes its pre-approval assessment; the approved action is authorized now. Execute it once, then report what actually happened. ` : '';
428
428
  const prompt = run.managed.phase === 'action_association' ? associationContract(run.managed.actionAssociation.job) : run.managed.phase === 'understanding' ? `Understand the ORIGINAL user request in this exact Codex conversation. This first/clarifying turn is read-only and must not perform task work. Context: ${JSON.stringify({ ...context, priorUnderstanding: run.managed.sessionUnderstanding, userAnswer: run.managed.understandingAnswer })}\n${understandingCheckpointContract(decision)} Do not write SIMY state directly.` : `Continue this existing SIMY managed task in the exact Codex conversation. Context: ${JSON.stringify(context)}\n${approvalInstruction}Execute ONLY the selected decision action. Do not perform any action from the NEXT plan in this turn. For a next local action within the enrolled task use the exact scope "task"; do not relabel external, broad, or destructive work as local. Preserve provider permissions. End with one JSON checkpoint. ${assessedCheckpointContract(decision)} Do not write SIMY state directly.`;
429
429
  const initialSandbox = run.managed.providerSandbox || resumed.sandbox;
@@ -439,21 +439,12 @@ export async function executeCodexAppServer(record, { openClient = openCodexAppS
439
439
  let turnId = null, completion = null, settled = false;
440
440
  const messages = new Map();
441
441
  const activeTools = new Set();
442
- const earlyUsage = [];
443
442
  const earlyActivity = [];
444
443
  const observeActivity = (method, params) => {
445
444
  if (method === 'item/started' && ['commandExecution', 'mcpToolCall', 'webSearch', 'fileChange'].includes(params.item?.type)) activeTools.add(params.item.id);
446
445
  if (method === 'item/completed') activeTools.delete(params.item?.id);
447
446
  onActivity({ waitingForTool: activeTools.size > 0 });
448
447
  };
449
- const observeUsage = params => {
450
- const total = params.tokenUsage?.total?.totalTokens, last = params.tokenUsage?.last?.totalTokens;
451
- if (!Number.isSafeInteger(total) || total < 0 || !Number.isSafeInteger(last) || last < 0 || last > total) return;
452
- if (lastTotal === null) usage = { source: 'codex_app_server', tokens: last, observedAt: Date.now() };
453
- else usage.tokens += Math.max(0, total - lastTotal);
454
- lastTotal = Math.max(lastTotal ?? 0, total);
455
- onUsage({ ...usage });
456
- };
457
448
  const cleanup = () => { unsubscribe(); signal?.removeEventListener('abort', cancelled); client.child.removeListener('close', closed); };
458
449
  const finish = (error, value) => { if (settled) return; settled = true; cleanup(); if (error) reject(error); else resolve(value); };
459
450
  const cancelled = () => finish(new CodexAppServerError('cancelled', 'Codex turn was cancelled.'));
@@ -474,10 +465,6 @@ export async function executeCodexAppServer(record, { openClient = openCodexAppS
474
465
  if (turnId) observeActivity(method, params);
475
466
  } else if (!turnId && typeof params.turnId === 'string' && earlyActivity.length < 100) earlyActivity.push({ method, turnId: params.turnId,
476
467
  item: { id: params.item?.id, type: params.item?.type } });
477
- if (method === 'thread/tokenUsage/updated') {
478
- if (turnId && params.turnId === turnId) observeUsage(params);
479
- else if (!turnId && earlyUsage.length < 100) earlyUsage.push({ turnId: params.turnId, tokenUsage: params.tokenUsage });
480
- }
481
468
  if (method === 'item/completed' && params.item?.type === 'agentMessage' && typeof params.item.text === 'string' && typeof params.turnId === 'string') messages.set(params.turnId, params.item.text);
482
469
  if (method === 'turn/completed') { completion = params; if (params.turn?.id === turnId) turnCompleted = true; complete(); }
483
470
  });
@@ -487,14 +474,13 @@ export async function executeCodexAppServer(record, { openClient = openCodexAppS
487
474
  turnStart = client.request('turn/start', { threadId: record.session, input: [{ type: 'text', text: prompt }], cwd: record.cwd,
488
475
  model: run.managed.resumeModel, sandboxPolicy })
489
476
  .then(response => { turnId = response?.turn?.id; activeTurnId = turnId; if (!turnId) finish(new CodexAppServerError('turn_failed', 'Codex did not start a turn.')); else {
490
- for (const params of earlyUsage) if (params.turnId === turnId) observeUsage(params);
491
477
  for (const params of earlyActivity) if (params.turnId === turnId) observeActivity(params.method, params);
492
478
  complete();
493
479
  } })
494
480
  .catch(error => finish(error));
495
481
  });
496
482
  const checkpoint = await done;
497
- return { code: 0, checkpoint, usage };
483
+ return { code: 0, checkpoint };
498
484
  } catch (error) {
499
485
  primaryError = error;
500
486
  throw error;
@@ -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
+ }
@@ -12,7 +12,8 @@ function unsettled(managed) {
12
12
  }
13
13
  function exactSource(record, taskId, targetSession, now, allowStoppedPending = false) {
14
14
  if (!record?.run?.managed || !record.admission || record.admission.session !== record.session || record.admission.cwd !== record.cwd || record.run.managed.taskId !== taskId) throw new Error('Exact managed task is unavailable.');
15
- if (now !== undefined && managedBudgetReason(record.run, now)) throw new Error('Original task budget exhausted; handoff cannot restore it.');
15
+ const stopReason = now === undefined ? null : managedBudgetReason(record.run, now);
16
+ if (stopReason) throw new Error(`${stopReason} Resume it in its original conversation before handing it off.`);
16
17
  const prior = record.run.managed.handoff;
17
18
  const transfer = prior?.status === 'received' ? null : prior;
18
19
  if (transfer && transfer.targetSession !== targetSession) throw new Error('A different handoff is already recorded.');