@awak-app/simy-cli 0.6.1 → 0.6.4

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";
@@ -23,7 +24,8 @@ import { dirname, join } from "node:path";
23
24
  import { homedir } from "node:os";
24
25
  import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
25
26
  import { startAgent } from "./agent.js";
26
- import { createRuntimeDiagnostics } from "./runtime-diagnostics.js";
27
+ import { createRuntimeDiagnostics, captureRuntimeOutput } from "./runtime-diagnostics.js";
28
+ import { applyLoginShellPath } from "./login-shell-path.js";
27
29
  import { createCliAutoUpdater, readCliPackageInfo } from "./auto-update.js";
28
30
  import { createAgentShutdown } from "./shutdown.js";
29
31
  import { resolveWebOrigin } from "./web-origin.js";
@@ -94,6 +96,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
94
96
  const makeActionAssociations = () => (dependencies.createActionAssociationHost || createActionAssociationHost)({ root: join(root, "delivery-hooks"), sessionRoot: root, webOrigin, list: () => recordReader.list(), onChange: () => { if (!closed) setImmediate(() => { void publish().catch(() => {}); }); } });
95
97
  let actionAssociations = makeActionAssociations();
96
98
  const statisticsCache = new Map();
99
+ const stopNotifier = createStopNotifier({ emit: event => { if (!closed && !relay) emit(event); } });
97
100
  let currentAccess = { state: 'checking', organizationId: null, organizationName: null };
98
101
  const accessScope = access => JSON.stringify([access?.revision ?? null, access?.organizationId ?? null]);
99
102
  const setAccessSnapshot = async access => {
@@ -104,7 +107,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
104
107
  discoveryController?.abort();
105
108
  sessionRefreshAgain = Boolean(sessionRefresh) && desktopAccessReady(currentAccess);
106
109
  for (const item of sessionStatus.sessions) automaticContinuation?.revoke(item.provider, item.session);
107
- sessionStatus = { sessions: [], providers: {}, refreshedAt: null }; sessionRefreshedAt = 0;
110
+ sessionStatus = { sessions: [], providers: {}, refreshedAt: null, initialLoading: true }; sessionRefreshedAt = 0;
108
111
  await analysisManager?.update([]);
109
112
  }
110
113
  return currentAccess;
@@ -133,7 +136,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
133
136
  beforeClaim: record => actionAssociations.prepareCached?.(record) || record });
134
137
  let shuttingDown = null;
135
138
  let commands;
136
- 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 };
137
140
  let sessionRefresh = null, sessionRefreshedAt = 0, sessionRefreshAgain = false, discoveryController = null, discoveryStopped = false;
138
141
  const stopDiscovery = async ({ drain = false } = {}) => {
139
142
  discoveryController?.abort();
@@ -157,8 +160,8 @@ export async function startDesktopHost({ input = process.stdin, output = process
157
160
  sessionRefreshAgain = desktopAccessReady(latestAccess);
158
161
  return;
159
162
  }
160
- await analysisManager.update(value.sessions); sessionStatus = value; sessionRefreshedAt = Date.now(); })
161
- .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(); })
162
165
  .finally(() => { if (discoveryController === controller) discoveryController = null; sessionRefresh = null; if (sessionRefreshAgain && !closed && !relay) { sessionRefreshAgain = false; refreshSessions(true); }
163
166
  if (!closed) setImmediate(() => { void publish().catch(() => {}); }); });
164
167
  }
@@ -243,6 +246,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
243
246
  }
244
247
  await analysisManager.updateProviderStatuses?.(status.providers);
245
248
  const records = await recordReader.list();
249
+ stopNotifier.observe(records);
246
250
  const projection = await actionAssociations.beginProjection?.({ records });
247
251
  status.deliveryHooks = await (dependencies.deliveryHookStatus || deliveryHookStatus)({ root: deliveryRoot, records: projection?.records || records }).catch(() => ({ error: "Delivery hook status unavailable", providers: {}, runs: [] }));
248
252
  await actionAssociations.decorate(status.deliveryHooks, projection);
@@ -572,9 +576,9 @@ export async function startDesktopHost({ input = process.stdin, output = process
572
576
  else if (action === "codex_hooks" || action === "claude_hooks") {
573
577
  const provider = action === "codex_hooks" ? "codex" : "claude";
574
578
  await (dependencies.openHookSettings || openHookSetup)(provider);
575
- } 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)) {
576
580
  await controlDelivery({ root: join(root, "delivery-hooks"), provider: command.provider, session: command.session,
577
- 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) });
578
582
  } else if (action === "delivery_verify") {
579
583
  await verifyDelivery({ root: join(root, "delivery-hooks"), provider: command.provider, session: command.session,
580
584
  taskId: command.taskId, criterionId: command.criterionId });
@@ -590,7 +594,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
590
594
  }
591
595
  if (command.enabled) await authorizeWork(commandScope);
592
596
  await (dependencies.setSessionAutoContinue || setSessionAutoContinue)({ root: join(root, 'delivery-hooks'),
593
- 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 });
594
598
  if (!command.enabled) automaticContinuation.revoke(command.provider, command.session);
595
599
  // Acknowledge the durable preference before discovery or analysis.
596
600
  emit({ type: "result", id: command.id, ok: true });
@@ -625,8 +629,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
625
629
  } else if (action === "session_enroll") {
626
630
  await (dependencies.enrollSession || enrollSession)({ root: join(root, "delivery-hooks"), provider: command.provider,
627
631
  session: command.session, objective: command.objective, acceptanceCriteria: command.acceptanceCriteria,
628
- autonomyPolicy: command.autonomyPolicy, maxMinutes: command.maxMinutes,
629
- 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 });
630
633
  await refreshSessions(true);
631
634
  } else if (action === "session_analyze") {
632
635
  const candidate = await selectedSession(command, { reuseDiscovery: true });
@@ -638,9 +641,8 @@ export async function startDesktopHost({ input = process.stdin, output = process
638
641
  await continueAnalyzedSession({ root: join(root, 'delivery-hooks'), candidate, manager,
639
642
  readContext: dependencies.readContinuationContext || readContinuationContext,
640
643
  enroll: async options => { await authorizeWork(commandScope); return guardedEnroll(options); }, beforeEnroll: () => authorizeWork(commandScope),
641
- settings: { autonomyPolicy: command.autonomyPolicy, maxMinutes: command.maxMinutes,
642
- maxContinuations: command.maxContinuations, maxFailures: command.maxFailures,
643
- 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 } });
644
646
  await refreshSessions(true);
645
647
  } else if (action === "session_answer") {
646
648
  await (dependencies.answerSession || answerSession)({ root: join(root, "delivery-hooks"), provider: command.provider,
@@ -735,7 +737,7 @@ export async function openProviderLogin(provider, root, {
735
737
  const directory = await mkdtemp(join(root, `${provider}-login-`));
736
738
  const script = join(directory, `Sign in to ${label}.command`);
737
739
  const quote = (s) => `'${s.replaceAll("'", "'\\''")}'`;
738
- await writeFile(script, `#!/bin/bash\nexport PATH="/opt/homebrew/bin:/usr/local/bin:$HOME/.local/bin:$PATH"\n${quote(executable.executable)} ${args.join(" ")}\nresult=$?\nrm -f -- ${quote(script)}\nrmdir -- ${quote(directory)}\nexit "$result"\n`, { mode: 0o700 });
740
+ await writeFile(script, `#!/bin/bash\nexport PATH=${quote(env.PATH || "/usr/bin:/bin")}\n${quote(executable.executable)} ${args.join(" ")}\nresult=$?\nrm -f -- ${quote(script)}\nrmdir -- ${quote(directory)}\nexit "$result"\n`, { mode: 0o700 });
739
741
  await chmod(script, 0o700);
740
742
  try { await execFileImpl("/usr/bin/open", ["-a", "Terminal", script], { timeout: 10_000 }); }
741
743
  catch (error) { await rm(directory, { recursive: true, force: true }); throw error; }
@@ -750,8 +752,12 @@ if (isDesktopHostEntry()) {
750
752
  let host;
751
753
  const stop = () => { void host?.close().finally(() => process.exit(0)); };
752
754
  process.once("SIGTERM", stop); process.once("SIGINT", stop);
753
- startDesktopHost().then((value) => { host = value; }).catch((error) => {
754
- createRuntimeDiagnostics(process.env.SIMY_HOME?.trim() || join(homedir(), ".simy"), { name: "desktop-host" }).write(`startup failed: ${error?.stack || error}`);
755
+ const diagnostics = createRuntimeDiagnostics(process.env.SIMY_HOME?.trim() || join(homedir(), ".simy"), { name: "desktop-host" });
756
+ // stdout is the native JSONL channel; stderr is the only unstructured output
757
+ // and the native window does not display it, so keep it for support.
758
+ captureRuntimeOutput(diagnostics, { stderr: process.stderr });
759
+ applyLoginShellPath({ diagnostics }).catch(() => {}).then(() => startDesktopHost()).then((value) => { host = value; }).catch((error) => {
760
+ diagnostics.write(`startup failed: ${error?.stack || error}`);
755
761
  const conflict = error?.code === PROCESS_CONFLICT_CODE;
756
762
  process.stdout.write(`${JSON.stringify({
757
763
  type: "error",
@@ -0,0 +1,29 @@
1
+ import { existsSync } from "node:fs";
2
+
3
+ // Version-specific runtimes are replaced by the stable alias that follows
4
+ // upgrades: the installer's `current` release link and Homebrew's `opt` link.
5
+ const STABLE_RUNTIME_ALIASES = [
6
+ [/^(.*)\/installer\/releases\/[^/]+\/[^/]+\.app\/(Contents\/Resources\/runtime\/bin\/node)$/, "$1/installer/current/$2"],
7
+ [/^(.*)\/Cellar\/([^/]+)\/[^/]+\/(bin\/node)$/, "$1/opt/$2/$3"],
8
+ ];
9
+
10
+ // Provider hooks are persisted into Codex/Claude configuration and run long
11
+ // after this process exits, often from GUI apps without the user's shell PATH.
12
+ // They must therefore name an absolute runtime that survives SIMY and Node updates.
13
+ export function stableHookRuntime({ execPath = process.execPath, fileExists = existsSync } = {}) {
14
+ for (const [pattern, replacement] of STABLE_RUNTIME_ALIASES) {
15
+ if (!pattern.test(execPath)) continue;
16
+ const alias = execPath.replace(pattern, replacement);
17
+ if (fileExists(alias)) return alias;
18
+ }
19
+ return execPath;
20
+ }
21
+
22
+ export function hookRuntimeCommand(command, { platform = process.platform, ...options } = {}) {
23
+ if (typeof command !== "string" || !command.startsWith("node ")) return command;
24
+ return `${quoteRuntime(stableHookRuntime(options), platform)} ${command.slice("node ".length)}`;
25
+ }
26
+
27
+ function quoteRuntime(value, platform) {
28
+ return platform === "win32" ? `"${value.replaceAll('"', '""')}"` : `'${value.replaceAll("'", "'\\''")}'`;
29
+ }
@@ -0,0 +1,67 @@
1
+ import { execFile } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import path from "node:path";
4
+
5
+ const START = "__SIMY_LOGIN_PATH_START__";
6
+ const END = "__SIMY_LOGIN_PATH_END__";
7
+
8
+ // Apps opened from Finder or the Dock inherit launchd's minimal PATH, not the
9
+ // user's shell profile. Provider CLIs installed through nvm, volta, fnm, asdf,
10
+ // pnpm or custom prefixes are therefore invisible unless we ask the user's own
11
+ // login shell once. A slow or broken profile must never prevent startup.
12
+ export async function resolveLoginShellPath({
13
+ platform = process.platform,
14
+ env = process.env,
15
+ execFileImpl = execFile,
16
+ fileExists = existsSync,
17
+ timeoutMs = 5_000,
18
+ } = {}) {
19
+ if (platform === "win32") return null;
20
+ const shell = loginShell({ platform, env, fileExists });
21
+ if (!shell) return null;
22
+ const output = await new Promise((resolve) => {
23
+ // Read the exported environment rather than expanding $PATH in the shell:
24
+ // fish expands a quoted $PATH with spaces, but exports it colon-separated.
25
+ const child = execFileImpl(shell, ["-l", "-i", "-c", `echo ${START}; /usr/bin/env; echo ${END}`], {
26
+ env: { ...env, TERM: "dumb", DISABLE_AUTO_UPDATE: "true" },
27
+ timeout: timeoutMs,
28
+ killSignal: "SIGKILL",
29
+ maxBuffer: 1024 * 1024,
30
+ }, (_error, stdout) => resolve(String(stdout || "")));
31
+ child?.stdin?.end();
32
+ });
33
+ const start = output.lastIndexOf(START);
34
+ const end = output.indexOf(END, start);
35
+ if (start < 0 || end < 0) return null;
36
+ const line = output.slice(start + START.length, end).split(/\r?\n/).find((item) => item.startsWith("PATH="));
37
+ return line?.slice("PATH=".length) || null;
38
+ }
39
+
40
+ // Login-shell entries take priority, matching what the user sees in Terminal.
41
+ // Existing entries (including the launcher's fallback directories) are kept.
42
+ export function mergePathEntries(...values) {
43
+ const entries = [];
44
+ for (const value of values) {
45
+ for (const entry of String(value || "").split(path.delimiter)) {
46
+ if (path.isAbsolute(entry) && !entries.includes(entry)) entries.push(entry);
47
+ }
48
+ }
49
+ return entries.join(path.delimiter);
50
+ }
51
+
52
+ export async function applyLoginShellPath({ env = process.env, platform = process.platform, diagnostics, ...options } = {}) {
53
+ if (platform === "win32") return false;
54
+ const loginPath = await resolveLoginShellPath({ env, platform, ...options });
55
+ if (!loginPath) {
56
+ diagnostics?.write("login shell PATH unavailable; using launcher PATH");
57
+ return false;
58
+ }
59
+ env.PATH = mergePathEntries(loginPath, env.PATH);
60
+ diagnostics?.write(`login shell PATH applied (${env.PATH.split(path.delimiter).length} entries)`);
61
+ return true;
62
+ }
63
+
64
+ function loginShell({ platform, env, fileExists }) {
65
+ const candidates = [env.SHELL, platform === "darwin" ? "/bin/zsh" : null, "/bin/sh"];
66
+ return candidates.find((candidate) => candidate && path.isAbsolute(candidate) && fileExists(candidate)) || null;
67
+ }
@@ -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
  }
@@ -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
  }
@@ -2,7 +2,7 @@ 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
8
 
@@ -394,7 +394,7 @@ export function isCodexWriterBusy(error) {
394
394
  return error instanceof CodexAppServerError && error.code === -32600 && /already has an active writer/i.test(error.message);
395
395
  }
396
396
 
397
- export async function executeCodexAppServer(record, { openClient = openCodexAppServer, signal, beforeDispatch = async () => {}, onActivity = () => {}, onUsage = () => {} } = {}) {
397
+ export async function executeCodexAppServer(record, { openClient = openCodexAppServer, signal, beforeDispatch = async () => {}, onActivity = () => {} } = {}) {
398
398
  const run = record.run, decision = run.managed?.decision;
399
399
  if (!decision || !run.managed.autonomyPolicy) throw new CodexAppServerError('decision', 'An assessed action is required before Codex dispatch.');
400
400
  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 +405,6 @@ export async function executeCodexAppServer(record, { openClient = openCodexAppS
405
405
  const client = await openClient({ signal });
406
406
  let originalSandbox = null, overrideStarted = false, activeTurnId = null, turnCompleted = false, turnStart = null;
407
407
  let primaryError = null;
408
- let usage = null, lastTotal = null;
409
408
  try {
410
409
  const metadata = await readCodexSession(client, record.session);
411
410
  if (await realpath(metadata.cwd) !== record.cwd || metadata.model !== run.managed.resumeModel) throw new CodexAppServerError('identity', 'Codex session workspace or model changed.');
@@ -422,8 +421,7 @@ export async function executeCodexAppServer(record, { openClient = openCodexAppS
422
421
  enrolledScopes: run.managed.approvalScopes,
423
422
  lastDependencyResolution: run.managed.lastDependencyResolution,
424
423
  ...(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 } };
424
+ progress: deliveryProgressContext(run) };
427
425
  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
426
  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
427
  const initialSandbox = run.managed.providerSandbox || resumed.sandbox;
@@ -439,21 +437,12 @@ export async function executeCodexAppServer(record, { openClient = openCodexAppS
439
437
  let turnId = null, completion = null, settled = false;
440
438
  const messages = new Map();
441
439
  const activeTools = new Set();
442
- const earlyUsage = [];
443
440
  const earlyActivity = [];
444
441
  const observeActivity = (method, params) => {
445
442
  if (method === 'item/started' && ['commandExecution', 'mcpToolCall', 'webSearch', 'fileChange'].includes(params.item?.type)) activeTools.add(params.item.id);
446
443
  if (method === 'item/completed') activeTools.delete(params.item?.id);
447
444
  onActivity({ waitingForTool: activeTools.size > 0 });
448
445
  };
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
446
  const cleanup = () => { unsubscribe(); signal?.removeEventListener('abort', cancelled); client.child.removeListener('close', closed); };
458
447
  const finish = (error, value) => { if (settled) return; settled = true; cleanup(); if (error) reject(error); else resolve(value); };
459
448
  const cancelled = () => finish(new CodexAppServerError('cancelled', 'Codex turn was cancelled.'));
@@ -474,10 +463,6 @@ export async function executeCodexAppServer(record, { openClient = openCodexAppS
474
463
  if (turnId) observeActivity(method, params);
475
464
  } else if (!turnId && typeof params.turnId === 'string' && earlyActivity.length < 100) earlyActivity.push({ method, turnId: params.turnId,
476
465
  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
466
  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
467
  if (method === 'turn/completed') { completion = params; if (params.turn?.id === turnId) turnCompleted = true; complete(); }
483
468
  });
@@ -487,14 +472,13 @@ export async function executeCodexAppServer(record, { openClient = openCodexAppS
487
472
  turnStart = client.request('turn/start', { threadId: record.session, input: [{ type: 'text', text: prompt }], cwd: record.cwd,
488
473
  model: run.managed.resumeModel, sandboxPolicy })
489
474
  .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
475
  for (const params of earlyActivity) if (params.turnId === turnId) observeActivity(params.method, params);
492
476
  complete();
493
477
  } })
494
478
  .catch(error => finish(error));
495
479
  });
496
480
  const checkpoint = await done;
497
- return { code: 0, checkpoint, usage };
481
+ return { code: 0, checkpoint };
498
482
  } catch (error) {
499
483
  primaryError = error;
500
484
  throw error;
@@ -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.');
@@ -144,7 +144,7 @@ export async function discoverSessions({ root, openClient = openCodexAppServer,
144
144
  }
145
145
 
146
146
  // Explicit execution authority: legacy monitor/tracked records stay read-only.
147
- export async function setSessionAutoContinue({ root, provider, session, enabled, continuationPolicy, hardMaxMinutes, hardMaxContinuations, providerTokenCeiling, ...options }) {
147
+ export async function setSessionAutoContinue({ root, provider, session, enabled, continuationPolicy, ...options }) {
148
148
  if (!['codex', 'claude'].includes(provider) || typeof enabled !== 'boolean') throw new Error('Unsupported automatic continuation request.');
149
149
  if (enabled) await trackSession({ root, provider, session, tracked: true, ...options });
150
150
  return updateRecord(root, provider, session, async previous => {
@@ -155,18 +155,13 @@ export async function setSessionAutoContinue({ root, provider, session, enabled,
155
155
  if (enabled && provider === 'claude') await requireClaudeChannel(previous,
156
156
  { session, cwd: previous.cwd, observation: { source: previous.admission?.source } }, options.inspectProcess || processIdentity);
157
157
  const epoch = previous.autoContinue === enabled && previous.autoContinueEpoch ? previous.autoContinueEpoch : randomUUID();
158
- const settings = { autonomyPolicy: 'balanced', maxMinutes: 30, maxContinuations: 6, maxFailures: 3,
159
- ...previous.autoContinueSettings,
160
- ...(continuationPolicy === undefined ? {} : { continuationPolicy }),
161
- ...(hardMaxMinutes === undefined ? {} : { hardMaxMinutes }),
162
- ...(hardMaxContinuations === undefined ? {} : { hardMaxContinuations }),
163
- ...(providerTokenCeiling === undefined ? {} : { providerTokenCeiling }) };
158
+ // Settings persisted by older versions may carry removed time, turn and
159
+ // token caps; only the fields that still govern a task are forwarded.
160
+ const { autonomyPolicy = 'balanced', maxFailures = 3, continuationPolicy: savedPolicy } = previous.autoContinueSettings || {};
161
+ const settings = { autonomyPolicy, maxFailures,
162
+ ...(continuationPolicy ?? savedPolicy) === undefined ? {} : { continuationPolicy: continuationPolicy ?? savedPolicy } };
164
163
  if (settings.continuationPolicy !== undefined && !['bounded', 'progress'].includes(settings.continuationPolicy)) throw new Error('Choose a continuation policy.');
165
- for (const [key, maximum] of [['hardMaxMinutes', 1440], ['hardMaxContinuations', 50], ['providerTokenCeiling', 15_000_000]]) {
166
- if (settings[key] !== undefined && (!Number.isSafeInteger(settings[key]) || settings[key] < 1 || settings[key] > maximum)) throw new Error('Invalid continuation ceiling.');
167
- }
168
- if (providerTokenCeiling !== undefined && provider !== 'codex') throw new Error('Token ceilings require host-observed Codex usage.');
169
- if (previous.run && [continuationPolicy, hardMaxMinutes, hardMaxContinuations, providerTokenCeiling].some(value => value !== undefined)) throw new Error('Change an existing task through its explicit continuation controls.');
164
+ if (previous.run && continuationPolicy !== undefined) throw new Error('Change an existing task through its explicit continuation controls.');
170
165
  const next = { ...previous, autoContinue: enabled, autoContinueEpoch: epoch,
171
166
  ...(enabled ? { tracked: true, monitor: true, autoContinueSettings: settings } : { tracked: false, monitor: false }) };
172
167
  // Adopt existing supervision without restarting it or renewing its budgets.
@@ -224,7 +219,7 @@ export async function monitorSession({ root, provider, session, monitor = true,
224
219
  }
225
220
 
226
221
  export async function enrollSession({ root, provider, session, objective, acceptanceCriteria, contextBootstrap = false, autonomyPolicy = 'balanced',
227
- maxMinutes = 30, maxContinuations = 6, maxFailures = 3, maxNoProgressMinutes, maxTokens, continuationPolicy, hardMaxMinutes, hardMaxContinuations, providerTokenCeiling, autoContinueEpoch, openClient = openCodexAppServer,
222
+ maxFailures = 3, maxNoProgressMinutes, continuationPolicy, autoContinueEpoch, openClient = openCodexAppServer,
228
223
  readClaude = readClaudeSession, inspectProcess = processIdentity, now = Date.now() }) {
229
224
  if (!['codex', 'claude'].includes(provider)) throw new Error('This provider does not have a verified exact-session enrollment channel.');
230
225
  if (typeof objective !== 'string' || !objective.trim() || objective.length > 4000) throw new Error('A concrete objective is required.');
@@ -249,13 +244,13 @@ export async function enrollSession({ root, provider, session, objective, accept
249
244
  const admission = provider === 'codex' ? { source: 'codex_app_server', provider, session, cwd, model: metadata.model } : undefined;
250
245
  const liveRecord = provider === 'claude' ? await readRecord(root, provider, session) : null;
251
246
  const run = await startDelivery({ root, provider, session, cwd, objective: objective.trim(), nextStep: 'Inspect current evidence and propose the next assessed action.',
252
- maxMinutes, maxContinuations, maxFailures, continuationPolicy, hardMaxMinutes, hardMaxContinuations, providerTokenCeiling, now, admission, inspectProcess, autoContinueEpoch,
247
+ maxFailures, continuationPolicy, now, admission, inspectProcess, autoContinueEpoch,
253
248
  liveHookAdmission: liveRecord ? { hookStartedAt: liveRecord.hookStartedAt, pid: liveRecord.process.pid, identity: liveRecord.process.identity } : undefined,
254
- managed: { acceptanceCriteria, contextBootstrap, autonomyPolicy, maxNoProgressMinutes, maxTokens, approvalScopes: ['task'], approvalPolicy: 'wait',
249
+ managed: { acceptanceCriteria, contextBootstrap, autonomyPolicy, maxNoProgressMinutes, approvalScopes: ['task'], approvalPolicy: 'wait',
255
250
  initialAction: initialPlan.alternatives[0].action, initialPlan, remainingWork: objective.trim(),
256
251
  autoRecovery: provider === 'codex', resumeChannel: provider === 'codex' ? 'codex_app_server' : null,
257
252
  resumeModel: metadata.model } });
258
- return { provider, session, taskId: run.run.managed.taskId, state: run.run.state, deadline: run.run.deadline };
253
+ return { provider, session, taskId: run.run.managed.taskId, state: run.run.state };
259
254
  }
260
255
 
261
256
  export async function continueSession(options) {