tide-commander 1.8.2 → 1.8.3

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 +1 @@
1
- import{W as a}from"./main-DscwhTSu.js";import{ImpactStyle as i,NotificationType as r}from"./index-rAvAlVdj.js";import"./modulepreload-polyfill-B5Qt9EMX.js";import"./vendor-react-uS-d4TUT.js";import"./vendor-three-DJ4p3FLF.js";class u extends a{constructor(){super(...arguments),this.selectionStarted=!1}async impact(t){const e=this.patternForImpact(t==null?void 0:t.style);this.vibrateWithPattern(e)}async notification(t){const e=this.patternForNotification(t==null?void 0:t.type);this.vibrateWithPattern(e)}async vibrate(t){const e=(t==null?void 0:t.duration)||300;this.vibrateWithPattern([e])}async selectionStart(){this.selectionStarted=!0}async selectionChanged(){this.selectionStarted&&this.vibrateWithPattern([70])}async selectionEnd(){this.selectionStarted=!1}patternForImpact(t=i.Heavy){return t===i.Medium?[43]:t===i.Light?[20]:[61]}patternForNotification(t=r.Success){return t===r.Warning?[30,40,30,50,60]:t===r.Error?[27,45,50]:[35,65,21]}vibrateWithPattern(t){if(navigator.vibrate)navigator.vibrate(t);else throw this.unavailable("Browser does not support the vibrate API")}}export{u as HapticsWeb};
1
+ import{W as a}from"./main-BkgbNnt9.js";import{ImpactStyle as i,NotificationType as r}from"./index-B6Hy-8p3.js";import"./modulepreload-polyfill-B5Qt9EMX.js";import"./vendor-react-uS-d4TUT.js";import"./vendor-three-DJ4p3FLF.js";class u extends a{constructor(){super(...arguments),this.selectionStarted=!1}async impact(t){const e=this.patternForImpact(t==null?void 0:t.style);this.vibrateWithPattern(e)}async notification(t){const e=this.patternForNotification(t==null?void 0:t.type);this.vibrateWithPattern(e)}async vibrate(t){const e=(t==null?void 0:t.duration)||300;this.vibrateWithPattern([e])}async selectionStart(){this.selectionStarted=!0}async selectionChanged(){this.selectionStarted&&this.vibrateWithPattern([70])}async selectionEnd(){this.selectionStarted=!1}patternForImpact(t=i.Heavy){return t===i.Medium?[43]:t===i.Light?[20]:[61]}patternForNotification(t=r.Success){return t===r.Warning?[30,40,30,50,60]:t===r.Error?[27,45,50]:[35,65,21]}vibrateWithPattern(t){if(navigator.vibrate)navigator.vibrate(t);else throw this.unavailable("Browser does not support the vibrate API")}}export{u as HapticsWeb};
@@ -1 +1 @@
1
- import{W as s}from"./main-DscwhTSu.js";import"./modulepreload-polyfill-B5Qt9EMX.js";import"./vendor-react-uS-d4TUT.js";import"./vendor-three-DJ4p3FLF.js";class f extends s{constructor(){super(...arguments),this.pending=[],this.deliveredNotifications=[],this.hasNotificationSupport=()=>{if(!("Notification"in window)||!Notification.requestPermission)return!1;if(Notification.permission!=="granted")try{new Notification("")}catch(i){if(i instanceof Error&&i.name==="TypeError")return!1}return!0}}async getDeliveredNotifications(){const i=[];for(const t of this.deliveredNotifications){const e={title:t.title,id:parseInt(t.tag),body:t.body};i.push(e)}return{notifications:i}}async removeDeliveredNotifications(i){for(const t of i.notifications){const e=this.deliveredNotifications.find(n=>n.tag===String(t.id));e==null||e.close(),this.deliveredNotifications=this.deliveredNotifications.filter(()=>!e)}}async removeAllDeliveredNotifications(){for(const i of this.deliveredNotifications)i.close();this.deliveredNotifications=[]}async createChannel(){throw this.unimplemented("Not implemented on web.")}async deleteChannel(){throw this.unimplemented("Not implemented on web.")}async listChannels(){throw this.unimplemented("Not implemented on web.")}async schedule(i){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");for(const t of i.notifications)this.sendNotification(t);return{notifications:i.notifications.map(t=>({id:t.id}))}}async getPending(){return{notifications:this.pending}}async registerActionTypes(){throw this.unimplemented("Not implemented on web.")}async cancel(i){this.pending=this.pending.filter(t=>!i.notifications.find(e=>e.id===t.id))}async areEnabled(){const{display:i}=await this.checkPermissions();return{value:i==="granted"}}async changeExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async checkExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async requestPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(await Notification.requestPermission())}}async checkPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(Notification.permission)}}transformNotificationPermission(i){switch(i){case"granted":return"granted";case"denied":return"denied";default:return"prompt"}}sendPending(){var i;const t=[],e=new Date().getTime();for(const n of this.pending)!((i=n.schedule)===null||i===void 0)&&i.at&&n.schedule.at.getTime()<=e&&(this.buildNotification(n),t.push(n));this.pending=this.pending.filter(n=>!t.find(o=>o===n))}sendNotification(i){var t;if(!((t=i.schedule)===null||t===void 0)&&t.at){const e=i.schedule.at.getTime()-new Date().getTime();this.pending.push(i),setTimeout(()=>{this.sendPending()},e);return}this.buildNotification(i)}buildNotification(i){const t=new Notification(i.title,{body:i.body,tag:String(i.id)});return t.addEventListener("click",this.onClick.bind(this,i),!1),t.addEventListener("show",this.onShow.bind(this,i),!1),t.addEventListener("close",()=>{this.deliveredNotifications=this.deliveredNotifications.filter(()=>!this)},!1),this.deliveredNotifications.push(t),t}onClick(i){const t={actionId:"tap",notification:i};this.notifyListeners("localNotificationActionPerformed",t)}onShow(i){this.notifyListeners("localNotificationReceived",i)}}export{f as LocalNotificationsWeb};
1
+ import{W as s}from"./main-BkgbNnt9.js";import"./modulepreload-polyfill-B5Qt9EMX.js";import"./vendor-react-uS-d4TUT.js";import"./vendor-three-DJ4p3FLF.js";class f extends s{constructor(){super(...arguments),this.pending=[],this.deliveredNotifications=[],this.hasNotificationSupport=()=>{if(!("Notification"in window)||!Notification.requestPermission)return!1;if(Notification.permission!=="granted")try{new Notification("")}catch(i){if(i instanceof Error&&i.name==="TypeError")return!1}return!0}}async getDeliveredNotifications(){const i=[];for(const t of this.deliveredNotifications){const e={title:t.title,id:parseInt(t.tag),body:t.body};i.push(e)}return{notifications:i}}async removeDeliveredNotifications(i){for(const t of i.notifications){const e=this.deliveredNotifications.find(n=>n.tag===String(t.id));e==null||e.close(),this.deliveredNotifications=this.deliveredNotifications.filter(()=>!e)}}async removeAllDeliveredNotifications(){for(const i of this.deliveredNotifications)i.close();this.deliveredNotifications=[]}async createChannel(){throw this.unimplemented("Not implemented on web.")}async deleteChannel(){throw this.unimplemented("Not implemented on web.")}async listChannels(){throw this.unimplemented("Not implemented on web.")}async schedule(i){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");for(const t of i.notifications)this.sendNotification(t);return{notifications:i.notifications.map(t=>({id:t.id}))}}async getPending(){return{notifications:this.pending}}async registerActionTypes(){throw this.unimplemented("Not implemented on web.")}async cancel(i){this.pending=this.pending.filter(t=>!i.notifications.find(e=>e.id===t.id))}async areEnabled(){const{display:i}=await this.checkPermissions();return{value:i==="granted"}}async changeExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async checkExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async requestPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(await Notification.requestPermission())}}async checkPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(Notification.permission)}}transformNotificationPermission(i){switch(i){case"granted":return"granted";case"denied":return"denied";default:return"prompt"}}sendPending(){var i;const t=[],e=new Date().getTime();for(const n of this.pending)!((i=n.schedule)===null||i===void 0)&&i.at&&n.schedule.at.getTime()<=e&&(this.buildNotification(n),t.push(n));this.pending=this.pending.filter(n=>!t.find(o=>o===n))}sendNotification(i){var t;if(!((t=i.schedule)===null||t===void 0)&&t.at){const e=i.schedule.at.getTime()-new Date().getTime();this.pending.push(i),setTimeout(()=>{this.sendPending()},e);return}this.buildNotification(i)}buildNotification(i){const t=new Notification(i.title,{body:i.body,tag:String(i.id)});return t.addEventListener("click",this.onClick.bind(this,i),!1),t.addEventListener("show",this.onShow.bind(this,i),!1),t.addEventListener("close",()=>{this.deliveredNotifications=this.deliveredNotifications.filter(()=>!this)},!1),this.deliveredNotifications.push(t),t}onClick(i){const t={actionId:"tap",notification:i};this.notifyListeners("localNotificationActionPerformed",t)}onShow(i){this.notifyListeners("localNotificationReceived",i)}}export{f as LocalNotificationsWeb};
package/dist/index.html CHANGED
@@ -22,11 +22,11 @@
22
22
  <link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
23
23
  <link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png" />
24
24
  <title>Tide Commander</title>
25
- <script type="module" crossorigin src="/assets/main-DscwhTSu.js"></script>
25
+ <script type="module" crossorigin src="/assets/main-BkgbNnt9.js"></script>
26
26
  <link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-B5Qt9EMX.js">
27
27
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-uS-d4TUT.js">
28
28
  <link rel="modulepreload" crossorigin href="/assets/vendor-three-DJ4p3FLF.js">
29
- <link rel="stylesheet" crossorigin href="/assets/main-B8W4PG4C.css">
29
+ <link rel="stylesheet" crossorigin href="/assets/main-CfLrhuQx.css">
30
30
  </head>
31
31
  <body>
32
32
  <div id="app"></div>
@@ -1,4 +1,5 @@
1
1
  import { saveRunningProcesses, loadRunningProcesses, isProcessRunning, clearRunningProcesses, } from '../../data/index.js';
2
+ import * as agentService from '../../services/agent-service.js';
2
3
  import { createLogger } from '../../utils/logger.js';
3
4
  const log = createLogger('Runner');
4
5
  export class RunnerRecoveryStore {
@@ -14,6 +15,7 @@ export class RunnerRecoveryStore {
14
15
  const processes = [];
15
16
  for (const [agentId, activeProcess] of this.activeProcesses) {
16
17
  if (activeProcess.process.pid) {
18
+ const agent = agentService.getAgent(agentId);
17
19
  processes.push({
18
20
  agentId,
19
21
  pid: activeProcess.process.pid,
@@ -22,6 +24,7 @@ export class RunnerRecoveryStore {
22
24
  outputFile: activeProcess.outputFile,
23
25
  stderrFile: activeProcess.stderrFile,
24
26
  lastRequest: activeProcess.lastRequest,
27
+ agentStatus: agent?.status,
25
28
  });
26
29
  }
27
30
  }
@@ -48,6 +51,12 @@ export class RunnerRecoveryStore {
48
51
  }
49
52
  log.log(`❌ Process for agent ${savedProcess.agentId} (PID ${savedProcess.pid}) is no longer running`);
50
53
  if (!this.backend.requiresStdinInput() && savedProcess.sessionId && savedProcess.lastRequest) {
54
+ // Only resume agents that were actively working when persisted.
55
+ // Idle agents with live processes (e.g. Codex waiting for stdin) should not be resumed.
56
+ if (savedProcess.agentStatus && savedProcess.agentStatus !== 'working') {
57
+ log.log(`🔄 [RESUME] Skipping resume for agent ${savedProcess.agentId} - was ${savedProcess.agentStatus} (not working)`);
58
+ continue;
59
+ }
51
60
  toResume.push(savedProcess);
52
61
  }
53
62
  }
@@ -57,6 +66,12 @@ export class RunnerRecoveryStore {
57
66
  }
58
67
  setTimeout(() => {
59
68
  for (const saved of toResume) {
69
+ // Verify the agent still exists (it may have been deleted)
70
+ const agent = agentService.getAgent(saved.agentId);
71
+ if (!agent) {
72
+ log.log(`🔄 [RESUME] Skipping resume for agent ${saved.agentId} - agent no longer exists`);
73
+ continue;
74
+ }
60
75
  const lastRequest = saved.lastRequest;
61
76
  log.log(`🔄 [RESUME] Resuming codex session for agent ${saved.agentId} (session ${saved.sessionId})`);
62
77
  this.run({
@@ -102,9 +102,17 @@ function parseResponsePayload(payload) {
102
102
  return {
103
103
  type: asString(payload.type),
104
104
  status: asString(payload.status),
105
+ text: asString(payload.text),
105
106
  action: parseAction(payload.action),
106
107
  item: parseItem(payload.item),
107
108
  usage: parseUsage(payload.usage),
109
+ info: isObject(payload.info)
110
+ ? {
111
+ model_context_window: asNumber(payload.info.model_context_window),
112
+ last_token_usage: parseUsage(payload.info.last_token_usage),
113
+ }
114
+ : undefined,
115
+ model_context_window: asNumber(payload.model_context_window),
108
116
  call_id: asString(payload.call_id),
109
117
  name: asString(payload.name),
110
118
  input: asString(payload.input),
@@ -119,6 +127,7 @@ function parseResponsePayload(payload) {
119
127
  export class CodexJsonEventParser {
120
128
  activeToolByItemId = new Map();
121
129
  lastAgentMessageText;
130
+ lastModelUsageSnapshot;
122
131
  enableFileDiffEnrichment;
123
132
  workingDirectory;
124
133
  gitRootCache = new Map();
@@ -148,7 +157,7 @@ export class CodexJsonEventParser {
148
157
  if (!event?.type)
149
158
  return [];
150
159
  if (event.type === 'event_msg') {
151
- return this.parseEventMsg(rawEvent);
160
+ return this.parseEventMsg(event.payload);
152
161
  }
153
162
  if (event.type === 'response_item') {
154
163
  return this.parseResponseItem(event.payload);
@@ -172,15 +181,19 @@ export class CodexJsonEventParser {
172
181
  }
173
182
  return [this.buildUnknownEventFallback(`Unhandled Codex event type: ${event.type}`, rawEvent)];
174
183
  }
175
- parseEventMsg(rawEvent) {
176
- if (!isObject(rawEvent))
177
- return [];
178
- const payload = rawEvent.payload;
179
- if (!isObject(payload))
184
+ parseEventMsg(payload) {
185
+ if (!payload)
180
186
  return [];
181
187
  const payloadType = asString(payload.type);
182
188
  // token_count: Silent. Token accounting is handled by turn.completed.
183
189
  if (payloadType === 'token_count') {
190
+ const usage = payload.info?.last_token_usage;
191
+ this.lastModelUsageSnapshot = {
192
+ contextWindow: payload.info?.model_context_window ?? this.lastModelUsageSnapshot?.contextWindow,
193
+ inputTokens: usage?.input_tokens,
194
+ outputTokens: usage?.output_tokens,
195
+ cacheReadInputTokens: usage?.cached_input_tokens,
196
+ };
184
197
  return [];
185
198
  }
186
199
  // agent_reasoning: Map to thinking event
@@ -196,6 +209,12 @@ export class CodexJsonEventParser {
196
209
  }
197
210
  // task_started: Silent. Envelope event with no user-facing content.
198
211
  if (payloadType === 'task_started') {
212
+ if (payload.model_context_window) {
213
+ this.lastModelUsageSnapshot = {
214
+ ...this.lastModelUsageSnapshot,
215
+ contextWindow: payload.model_context_window,
216
+ };
217
+ }
199
218
  return [];
200
219
  }
201
220
  // user_message / agent_message are handled via item.completed, skip if seen here
@@ -434,6 +453,18 @@ export class CodexJsonEventParser {
434
453
  event.resultText = this.lastAgentMessageText;
435
454
  this.lastAgentMessageText = undefined; // Reset for next turn
436
455
  }
456
+ if (this.lastModelUsageSnapshot && (this.lastModelUsageSnapshot.contextWindow
457
+ || this.lastModelUsageSnapshot.inputTokens !== undefined
458
+ || this.lastModelUsageSnapshot.outputTokens !== undefined
459
+ || this.lastModelUsageSnapshot.cacheReadInputTokens !== undefined)) {
460
+ event.modelUsage = {
461
+ contextWindow: this.lastModelUsageSnapshot.contextWindow,
462
+ inputTokens: this.lastModelUsageSnapshot.inputTokens ?? 0,
463
+ outputTokens: this.lastModelUsageSnapshot.outputTokens ?? 0,
464
+ cacheReadInputTokens: this.lastModelUsageSnapshot.cacheReadInputTokens ?? 0,
465
+ };
466
+ this.lastModelUsageSnapshot = undefined;
467
+ }
437
468
  return [event];
438
469
  }
439
470
  parseFileChange(item) {
@@ -3,12 +3,19 @@
3
3
  * Business logic for managing agents
4
4
  */
5
5
  import * as fs from 'fs';
6
+ import * as os from 'os';
7
+ import * as path from 'path';
6
8
  import { loadAgents, saveAgents, saveAgentsAsync, getDataDir } from '../data/index.js';
7
9
  import { listSessions, getSessionSummary, loadSession, loadToolHistory, searchSession, } from '../claude/session-loader.js';
8
10
  import { loadSubagentHistory } from '../claude/subagent-history-loader.js';
9
11
  import { logger, generateId } from '../utils/index.js';
10
12
  const log = logger.agent;
11
13
  const CLAUDE_MODELS = new Set(['sonnet', 'opus', 'haiku']);
14
+ const DEFAULT_CLAUDE_CONTEXT_LIMIT = 200000;
15
+ const DEFAULT_CODEX_CONTEXT_LIMIT = 258400;
16
+ function getDefaultContextLimit(provider) {
17
+ return provider === 'codex' ? DEFAULT_CODEX_CONTEXT_LIMIT : DEFAULT_CLAUDE_CONTEXT_LIMIT;
18
+ }
12
19
  // In-memory agent storage
13
20
  const agents = new Map();
14
21
  const listeners = new Set();
@@ -29,6 +36,101 @@ export function sanitizeCodexModel(model) {
29
36
  const trimmed = model.trim();
30
37
  return trimmed.length > 0 ? trimmed : undefined;
31
38
  }
39
+ function findFileRecursively(rootDir, pattern) {
40
+ if (!fs.existsSync(rootDir))
41
+ return null;
42
+ const entries = fs.readdirSync(rootDir, { withFileTypes: true });
43
+ for (const entry of entries) {
44
+ const fullPath = path.join(rootDir, entry.name);
45
+ if (entry.isDirectory()) {
46
+ const nested = findFileRecursively(fullPath, pattern);
47
+ if (nested)
48
+ return nested;
49
+ continue;
50
+ }
51
+ if (entry.isFile() && entry.name.includes(pattern) && entry.name.endsWith('.jsonl')) {
52
+ return fullPath;
53
+ }
54
+ }
55
+ return null;
56
+ }
57
+ function findCodexRolloutPath(sessionId) {
58
+ const codexHome = path.join(os.homedir(), '.codex');
59
+ return findFileRecursively(path.join(codexHome, 'sessions'), sessionId)
60
+ || findFileRecursively(path.join(codexHome, 'archived_sessions'), sessionId);
61
+ }
62
+ function getCodexLogPath() {
63
+ return path.join(os.homedir(), '.codex', 'log', 'codex-tui.log');
64
+ }
65
+ function parseCodexEstimatedContextSnapshot(sessionId, contextLimit) {
66
+ const logPath = getCodexLogPath();
67
+ if (!fs.existsSync(logPath))
68
+ return null;
69
+ const lines = fs.readFileSync(logPath, 'utf8').split('\n');
70
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
71
+ const line = lines[index];
72
+ if (!line.includes(sessionId) || !line.includes('estimated_token_count=Some('))
73
+ continue;
74
+ const estimatedMatch = line.match(/estimated_token_count=Some\((\d+)\)/);
75
+ if (!estimatedMatch)
76
+ continue;
77
+ const estimatedTokens = Number(estimatedMatch[1]);
78
+ if (!Number.isFinite(estimatedTokens) || estimatedTokens < 0)
79
+ continue;
80
+ return {
81
+ contextUsed: Math.max(0, Math.round(estimatedTokens)),
82
+ contextLimit,
83
+ };
84
+ }
85
+ return null;
86
+ }
87
+ function parseCodexContextSnapshot(rolloutPath) {
88
+ if (!fs.existsSync(rolloutPath))
89
+ return null;
90
+ const lines = fs.readFileSync(rolloutPath, 'utf8').split('\n');
91
+ let contextLimit = DEFAULT_CODEX_CONTEXT_LIMIT;
92
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
93
+ const line = lines[index].trim();
94
+ if (!line)
95
+ continue;
96
+ try {
97
+ const parsed = JSON.parse(line);
98
+ const payload = parsed?.payload;
99
+ if (payload?.type === 'token_count' && payload.info) {
100
+ const modelContextWindow = Number(payload.info.model_context_window);
101
+ if (Number.isFinite(modelContextWindow) && modelContextWindow > 0) {
102
+ contextLimit = modelContextWindow;
103
+ }
104
+ const inputTokens = Number(payload.info?.last_token_usage?.input_tokens);
105
+ if (Number.isFinite(inputTokens) && inputTokens >= 0) {
106
+ return {
107
+ contextUsed: Math.min(Math.round(inputTokens), contextLimit),
108
+ contextLimit,
109
+ };
110
+ }
111
+ }
112
+ if (payload?.type === 'task_started') {
113
+ const modelContextWindow = Number(payload.model_context_window);
114
+ if (Number.isFinite(modelContextWindow) && modelContextWindow > 0) {
115
+ contextLimit = modelContextWindow;
116
+ }
117
+ }
118
+ }
119
+ catch {
120
+ // Ignore malformed lines and continue scanning older entries.
121
+ }
122
+ }
123
+ return null;
124
+ }
125
+ export function getCodexContextSnapshotFromSession(sessionId) {
126
+ if (!sessionId)
127
+ return null;
128
+ const rolloutPath = findCodexRolloutPath(sessionId);
129
+ const rolloutSnapshot = rolloutPath ? parseCodexContextSnapshot(rolloutPath) : null;
130
+ const contextLimit = rolloutSnapshot?.contextLimit ?? DEFAULT_CODEX_CONTEXT_LIMIT;
131
+ const estimatedSnapshot = parseCodexEstimatedContextSnapshot(sessionId, contextLimit);
132
+ return estimatedSnapshot ?? rolloutSnapshot;
133
+ }
32
134
  // ============================================================================
33
135
  // Initialization
34
136
  // ============================================================================
@@ -36,16 +138,29 @@ export function initAgents() {
36
138
  try {
37
139
  const storedAgents = loadAgents();
38
140
  for (const stored of storedAgents) {
39
- const contextLimit = stored.contextLimit ?? 200000;
141
+ const isCodexProvider = (stored.provider ?? 'claude') === 'codex';
142
+ const repairedCodexContext = isCodexProvider
143
+ ? getCodexContextSnapshotFromSession(stored.sessionId)
144
+ : null;
145
+ const defaultContextLimit = getDefaultContextLimit(stored.provider ?? 'claude');
146
+ const migratedPersistedContextLimit = isCodexProvider
147
+ && (stored.contextLimit === undefined || stored.contextLimit === DEFAULT_CLAUDE_CONTEXT_LIMIT)
148
+ ? defaultContextLimit
149
+ : stored.contextLimit;
150
+ const contextLimit = repairedCodexContext?.contextLimit ?? migratedPersistedContextLimit ?? defaultContextLimit;
40
151
  const tokensUsed = stored.tokensUsed ?? 0;
41
152
  // Preserve persisted context usage. Falling back to lifetime tokens can
42
153
  // inflate context on restart because tokensUsed is cumulative over time.
43
154
  const persistedContextUsed = typeof stored.contextUsed === 'number'
44
155
  ? stored.contextUsed
45
156
  : tokensUsed;
157
+ const baseContextUsed = repairedCodexContext?.contextUsed ?? persistedContextUsed;
46
158
  // Don't clamp to contextLimit - contextUsed can legitimately exceed the default
47
159
  // 200k limit for models with larger context windows (up to 1M).
48
- const contextUsed = Math.max(0, persistedContextUsed);
160
+ const contextUsed = Math.max(0, baseContextUsed);
161
+ const clearStaleContextStats = isCodexProvider
162
+ && stored.contextStats
163
+ && (stored.contextStats.contextWindow !== contextLimit || stored.contextStats.totalTokens !== contextUsed);
49
164
  const agent = {
50
165
  ...stored,
51
166
  status: 'idle', // Ready to receive commands
@@ -56,6 +171,7 @@ export function initAgents() {
56
171
  // Ensure context fields have defaults (migration for existing agents)
57
172
  contextUsed,
58
173
  contextLimit,
174
+ contextStats: clearStaleContextStats ? undefined : stored.contextStats,
59
175
  taskCount: stored.taskCount ?? 0, // Migration for existing agents
60
176
  permissionMode: stored.permissionMode ?? 'bypass', // Migration for existing agents
61
177
  useChrome: stored.useChrome, // Restore Chrome flag
@@ -169,7 +285,7 @@ export async function createAgent(name, agentClass, cwd, position, sessionId, us
169
285
  codexConfig,
170
286
  tokensUsed: 0,
171
287
  contextUsed: 0,
172
- contextLimit: 200000, // Claude's default context limit
288
+ contextLimit: getDefaultContextLimit(provider),
173
289
  taskCount: 0, // Initialize task counter
174
290
  createdAt: Date.now(),
175
291
  lastActivity: Date.now(),
@@ -1,5 +1,5 @@
1
1
  import * as agentService from './agent-service.js';
2
- import { clearPendingSilentContextRefresh, hasPendingSilentContextRefresh, markPendingSilentContextRefresh, startStdinWatchdog, } from './runtime-watchdog.js';
2
+ import { clearPendingSilentContextRefresh, clearStdinWatchdog, hasPendingSilentContextRefresh, markPendingSilentContextRefresh, startStdinWatchdog, } from './runtime-watchdog.js';
3
3
  export function createRuntimeCommandExecution(deps) {
4
4
  const { log, getRunner, getRunnerForAgent, notifyCommandStarted, emitOutput, killDetachedProviderProcessInCwd, } = deps;
5
5
  async function executeCommand(agentId, command, systemPrompt, forceNewSession, customAgent, silent) {
@@ -139,6 +139,9 @@ export function createRuntimeCommandExecution(deps) {
139
139
  await executeCommand(agentId, command, undefined, undefined, undefined, true);
140
140
  }
141
141
  async function stopAgent(agentId) {
142
+ // Cancel any pending stdin watchdog timer to prevent it from respawning
143
+ // the process after we've stopped it
144
+ clearStdinWatchdog(agentId);
142
145
  const runner = getRunnerForAgent(agentId);
143
146
  if (runner) {
144
147
  await runner.stop(agentId);
@@ -3,7 +3,8 @@ import * as agentService from './agent-service.js';
3
3
  import * as supervisorService from './supervisor-service.js';
4
4
  import { clearPendingSilentContextRefresh, consumeStepCompleteReceived, markStepCompleteReceived, } from './runtime-watchdog.js';
5
5
  import { handleTaskToolResult, handleTaskToolStart } from './runtime-subagents.js';
6
- const DEFAULT_CODEX_CONTEXT_WINDOW = 200000;
6
+ const DEFAULT_CLAUDE_CONTEXT_WINDOW = 200000;
7
+ const DEFAULT_CODEX_CONTEXT_WINDOW = 258400;
7
8
  const CODEX_ROLLING_CONTEXT_TURNS = 40;
8
9
  const CODEX_PLAUSIBLE_USAGE_MULTIPLIER = 1.2;
9
10
  const CODEX_RECOVERABLE_RESUME_ERRORS = [
@@ -36,6 +37,9 @@ function estimateTokensFromText(text) {
36
37
  return 0;
37
38
  return Math.max(1, Math.ceil(normalized.length / 4));
38
39
  }
40
+ function getDefaultContextWindow(provider) {
41
+ return provider === 'codex' ? DEFAULT_CODEX_CONTEXT_WINDOW : DEFAULT_CLAUDE_CONTEXT_WINDOW;
42
+ }
39
43
  function updateCodexRollingContextEstimate(agentId, turnGrowth) {
40
44
  const history = codexContextGrowthHistory.get(agentId) || [];
41
45
  history.push(Math.max(0, Math.round(turnGrowth)));
@@ -65,7 +69,7 @@ function buildCodexRecoverySystemPrompt(sessionId, messages) {
65
69
  ].join('\n\n');
66
70
  }
67
71
  function buildEstimatedContextStats(totalTokens, contextWindow, model) {
68
- const safeWindow = contextWindow > 0 ? contextWindow : DEFAULT_CODEX_CONTEXT_WINDOW;
72
+ const safeWindow = contextWindow > 0 ? contextWindow : DEFAULT_CLAUDE_CONTEXT_WINDOW;
69
73
  const usedPercent = Math.min(100, Math.max(0, Math.round((totalTokens / safeWindow) * 100)));
70
74
  const freeTokens = Math.max(0, safeWindow - totalTokens);
71
75
  const messagesPercent = Number(((totalTokens / safeWindow) * 100).toFixed(1));
@@ -179,7 +183,7 @@ export function createRuntimeEventHandlers(deps) {
179
183
  // Output tokens are the model's response and don't count toward the limit.
180
184
  const snapshotContextUsed = cacheRead + cacheCreation + inputTokens;
181
185
  if (snapshotContextUsed > 0) {
182
- const effectiveLimit = agent.contextLimit || 200000;
186
+ const effectiveLimit = agent.contextLimit || getDefaultContextWindow(agent.provider);
183
187
  // Guard against cumulative session totals: if the sum exceeds the
184
188
  // context window, it can't represent per-request context fill.
185
189
  if (snapshotContextUsed > effectiveLimit) {
@@ -233,7 +237,7 @@ export function createRuntimeEventHandlers(deps) {
233
237
  const lastTask = agent.lastAssignedTask?.trim() || '';
234
238
  const isContextCommand = lastTask === '/context' || lastTask === '/cost' || lastTask === '/compact';
235
239
  let contextUsed = agent.contextUsed || 0;
236
- let contextLimit = agent.contextLimit || 200000;
240
+ let contextLimit = agent.contextLimit || getDefaultContextWindow(agent.provider);
237
241
  // IMPORTANT: event.modelUsage is often {} (empty object) which is truthy.
238
242
  // We must check that it has actual data before using it, otherwise we'd
239
243
  // zero out contextUsed (since all fields would be undefined → 0).
@@ -262,12 +266,16 @@ export function createRuntimeEventHandlers(deps) {
262
266
  contextLimit = event.modelUsage.contextWindow;
263
267
  }
264
268
  const turnGrowthEstimate = estimateTokensFromText(agent.lastAssignedTask) + outputTokens;
265
- const rollingEstimate = updateCodexRollingContextEstimate(agentId, turnGrowthEstimate);
269
+ updateCodexRollingContextEstimate(agentId, turnGrowthEstimate);
266
270
  const plausibleSnapshotLimit = contextLimit * CODEX_PLAUSIBLE_USAGE_MULTIPLIER;
267
- const hasPlausibleSnapshot = inputTokens > 0 && inputTokens <= plausibleSnapshotLimit;
268
- contextUsed = hasPlausibleSnapshot
269
- ? Math.max(rollingEstimate, inputTokens + outputTokens)
270
- : rollingEstimate;
271
+ const hasAuthoritativeSnapshot = inputTokens > 0 && inputTokens <= plausibleSnapshotLimit;
272
+ if (hasAuthoritativeSnapshot) {
273
+ contextUsed = inputTokens;
274
+ }
275
+ else {
276
+ const rollingEstimate = updateCodexRollingContextEstimate(agentId, 0);
277
+ contextUsed = rollingEstimate;
278
+ }
271
279
  log.log(`[step_complete] Codex modelUsage for ${agentId}: input=${inputTokens}, contextWindow=${event.modelUsage.contextWindow || 'none'}`);
272
280
  }
273
281
  else if (event.tokens) {
@@ -292,12 +300,12 @@ export function createRuntimeEventHandlers(deps) {
292
300
  const stats = agent.contextStats;
293
301
  if (stats && stats.contextWindow > 0) {
294
302
  contextUsed = Math.max(0, stats.totalTokens || 0);
295
- contextLimit = Math.max(1, stats.contextWindow || 200000);
303
+ contextLimit = Math.max(1, stats.contextWindow || getDefaultContextWindow(agent.provider));
296
304
  log.log(`[step_complete] Context command for ${agentId}; preserving context from context_stats ${contextUsed}/${contextLimit}`);
297
305
  }
298
306
  else {
299
307
  contextUsed = agent.contextUsed || 0;
300
- contextLimit = agent.contextLimit || 200000;
308
+ contextLimit = agent.contextLimit || getDefaultContextWindow(agent.provider);
301
309
  log.log(`[step_complete] Context command for ${agentId}; preserving context values from tracked fields`);
302
310
  }
303
311
  }
@@ -380,12 +388,34 @@ export function createRuntimeEventHandlers(deps) {
380
388
  }
381
389
  function handleComplete(agentId, success) {
382
390
  const receivedStepComplete = consumeStepCompleteReceived(agentId);
383
- agentService.updateAgent(agentId, {
391
+ const agent = agentService.getAgent(agentId);
392
+ const isCodexProvider = (agent?.provider ?? 'claude') === 'codex';
393
+ const finalCodexSnapshot = isCodexProvider
394
+ ? agentService.getCodexContextSnapshotFromSession(agent?.sessionId)
395
+ : null;
396
+ const completionUpdates = {
384
397
  status: 'idle',
385
398
  currentTask: undefined,
386
399
  currentTool: undefined,
387
400
  isDetached: false,
388
- });
401
+ };
402
+ if (finalCodexSnapshot) {
403
+ completionUpdates.contextUsed = finalCodexSnapshot.contextUsed;
404
+ completionUpdates.contextLimit = finalCodexSnapshot.contextLimit;
405
+ // Build proper contextStats from the snapshot. Don't blindly copy
406
+ // finalCodexSnapshot.contextStats — CodexContextSnapshot doesn't have
407
+ // that field, so it would be `undefined` and wipe the valid stats
408
+ // set by step_complete.
409
+ const existingStats = agent?.contextStats;
410
+ if (existingStats && existingStats.lastUpdated) {
411
+ completionUpdates.contextStats = updateContextStatsTokens(existingStats, finalCodexSnapshot.contextUsed, finalCodexSnapshot.contextLimit);
412
+ }
413
+ else {
414
+ completionUpdates.contextStats = buildEstimatedContextStats(finalCodexSnapshot.contextUsed, finalCodexSnapshot.contextLimit, agent?.codexModel || agent?.model);
415
+ }
416
+ log.log(`[complete] Refreshed Codex context for ${agentId}: ${finalCodexSnapshot.contextUsed}/${finalCodexSnapshot.contextLimit}`);
417
+ }
418
+ agentService.updateAgent(agentId, completionUpdates);
389
419
  emitComplete(agentId, success);
390
420
  // Real-time context tracking via usage_snapshot events replaces automatic /context refresh.
391
421
  // The /context command is now only triggered manually via the UI refresh button.
@@ -238,9 +238,9 @@ function _checkForOrphanedProcess(agentId) {
238
238
  return false;
239
239
  }
240
240
  }
241
- export async function syncAgentStatus(agentId, isStartupSync = false) {
242
- await statusSync.syncAgentStatus(agentId, isStartupSync);
241
+ export async function syncAgentStatus(agentId) {
242
+ await statusSync.syncAgentStatus(agentId);
243
243
  }
244
- export async function syncAllAgentStatus(isStartupSync = false) {
245
- await statusSync.syncAllAgentStatus(isStartupSync);
244
+ export async function syncAllAgentStatus() {
245
+ await statusSync.syncAllAgentStatus();
246
246
  }
@@ -36,15 +36,20 @@ export function createRuntimeStatusSync(deps) {
36
36
  }
37
37
  }
38
38
  }
39
- async function syncAgentStatus(agentId, isStartupSync = false) {
39
+ async function syncAgentStatus(agentId) {
40
40
  const agent = agentService.getAgent(agentId);
41
41
  if (!agent)
42
42
  return;
43
+ // Only working agents need sync — idle agents stay idle.
44
+ // We intentionally do NOT promote idle agents to working based on
45
+ // orphaned processes or session activity. That caused agents to
46
+ // incorrectly resume after backend restarts/reconnects.
47
+ if (agent.status !== 'working')
48
+ return;
43
49
  const isTrackedProcess = getRunnerForAgent(agentId)?.isRunning(agentId) ?? false;
44
50
  if (isTrackedProcess)
45
51
  return;
46
52
  let isRecentlyActive = false;
47
- let hasOrphanedProcess = false;
48
53
  if (agent.sessionId && agent.cwd) {
49
54
  try {
50
55
  const activity = await getSessionActivityStatus(agent.cwd, agent.sessionId, 60);
@@ -55,20 +60,8 @@ export function createRuntimeStatusSync(deps) {
55
60
  catch {
56
61
  // Session activity check failed, assume not active.
57
62
  }
58
- if (agent.status === 'idle') {
59
- try {
60
- const provider = agent.provider ?? 'claude';
61
- hasOrphanedProcess = await isProviderProcessRunningInCwd(provider, agent.cwd);
62
- if (hasOrphanedProcess) {
63
- log.log(`[syncAgentStatus] Agent ${agentId}: Found orphaned ${provider} process, isRecentlyActive=${isRecentlyActive}`);
64
- }
65
- }
66
- catch (err) {
67
- log.error(`[syncAgentStatus] Agent ${agentId}: Failed to check for orphaned process:`, err);
68
- }
69
- }
70
63
  }
71
- if (agent.status === 'working' && !isRecentlyActive && !hasOrphanedProcess) {
64
+ if (!isRecentlyActive) {
72
65
  agentService.updateAgent(agentId, {
73
66
  status: 'idle',
74
67
  currentTask: undefined,
@@ -76,25 +69,10 @@ export function createRuntimeStatusSync(deps) {
76
69
  isDetached: false,
77
70
  });
78
71
  }
79
- else if (agent.status === 'idle' && hasOrphanedProcess && isRecentlyActive) {
80
- const provider = agent.provider ?? 'claude';
81
- log.log(`Agent ${agentId} has orphaned ${provider} process with recent activity - marking as working (detached)`);
82
- agentService.updateAgent(agentId, {
83
- status: 'working',
84
- currentTask: 'Processing (detached)...',
85
- isDetached: true,
86
- });
87
- }
88
- else if (isStartupSync && agent.status === 'idle' && isRecentlyActive) {
89
- agentService.updateAgent(agentId, {
90
- status: 'working',
91
- currentTask: 'Processing...',
92
- });
93
- }
94
72
  }
95
- async function syncAllAgentStatus(isStartupSync = false) {
73
+ async function syncAllAgentStatus() {
96
74
  const agents = agentService.getAllAgents();
97
- await Promise.all(agents.map((agent) => syncAgentStatus(agent.id, isStartupSync)));
75
+ await Promise.all(agents.map((agent) => syncAgentStatus(agent.id)));
98
76
  }
99
77
  return {
100
78
  pollOrphanedAgents,
@@ -1,3 +1,4 @@
1
+ import * as agentService from './agent-service.js';
1
2
  import { logger } from '../utils/logger.js';
2
3
  const log = logger.claude;
3
4
  const pendingSilentContextRefresh = new Set();
@@ -32,6 +33,12 @@ export function startStdinWatchdog(options) {
32
33
  log.log(`[STDIN-WATCHDOG] Starting watchdog for ${agentId}, timeout=${STDIN_ACTIVITY_TIMEOUT_MS}ms`);
33
34
  const watchdogTimer = setTimeout(async () => {
34
35
  stdinWatchdogTimers.delete(agentId);
36
+ // Don't respawn if the agent was explicitly stopped (status is idle)
37
+ const agent = agentService.getAgent(agentId);
38
+ if (!agent || agent.status !== 'working') {
39
+ log.log(`[STDIN-WATCHDOG] Agent ${agentId}: Skipping respawn - agent is ${agent?.status ?? 'not found'}`);
40
+ return;
41
+ }
35
42
  if (runner && !runner.hasRecentActivity(agentId, STDIN_ACTIVITY_TIMEOUT_MS)) {
36
43
  log.warn(`[STDIN-WATCHDOG] Agent ${agentId}: No activity after stdin message, respawning process...`);
37
44
  await runner.stop(agentId);
@@ -440,7 +440,8 @@ function buildStatsFromTrackedData(agent) {
440
440
  // Do NOT prefer agent.contextStats here — those can become stale when previous
441
441
  // fallback calls save their (potentially wrong) output back to agent.contextStats
442
442
  // via broadcastContextStats, creating a self-reinforcing feedback loop.
443
- const contextLimit = Math.max(1, Math.round(agent.contextLimit || 200000));
443
+ const defaultContextLimit = (agent.provider ?? 'claude') === 'codex' ? 258400 : 200000;
444
+ const contextLimit = Math.max(1, Math.round(agent.contextLimit || defaultContextLimit));
444
445
  const rawContextUsed = Math.max(0, Math.round(agent.contextUsed || 0));
445
446
  // Guard: contextUsed can never exceed contextLimit. Values above the limit are
446
447
  // cumulative session totals from result events, not per-request context fill.
@@ -511,6 +512,7 @@ export async function handleRequestContextStats(ctx, payload) {
511
512
  return;
512
513
  }
513
514
  const isClaudeProvider = (agent.provider ?? 'claude') === 'claude';
515
+ const isCodexProvider = (agent.provider ?? 'claude') === 'codex';
514
516
  // For Claude agents with an active session, fetch real context stats from the CLI
515
517
  if (isClaudeProvider && agent.sessionId) {
516
518
  log.log(`[contextStats] Fetching real context from CLI for ${agent.name} (session=${agent.sessionId})`);
@@ -543,6 +545,32 @@ export async function handleRequestContextStats(ctx, payload) {
543
545
  log.error(`[contextStats] In-session /context failed: ${err}`);
544
546
  }
545
547
  }
548
+ if (isCodexProvider && agent.sessionId) {
549
+ const codexSnapshot = agentService.getCodexContextSnapshotFromSession(agent.sessionId);
550
+ if (codexSnapshot) {
551
+ const contextLimit = Math.max(1, codexSnapshot.contextLimit);
552
+ const contextUsed = Math.max(0, Math.min(codexSnapshot.contextUsed, contextLimit));
553
+ const usedPercent = Math.min(100, Math.round((contextUsed / contextLimit) * 100));
554
+ const freeTokens = Math.max(0, contextLimit - contextUsed);
555
+ const stats = {
556
+ model: agent.codexModel || agent.model || 'codex',
557
+ contextWindow: contextLimit,
558
+ totalTokens: contextUsed,
559
+ usedPercent,
560
+ categories: {
561
+ systemPrompt: { tokens: 0, percent: 0 },
562
+ systemTools: { tokens: 0, percent: 0 },
563
+ messages: { tokens: contextUsed, percent: Number(((contextUsed / contextLimit) * 100).toFixed(1)) },
564
+ freeSpace: { tokens: freeTokens, percent: Number(((freeTokens / contextLimit) * 100).toFixed(1)) },
565
+ autocompactBuffer: { tokens: 0, percent: 0 },
566
+ },
567
+ lastUpdated: Date.now(),
568
+ };
569
+ log.log(`[contextStats] Codex session snapshot for ${agent.name}: ${contextUsed}/${contextLimit}`);
570
+ broadcastContextStats(ctx, payload.agentId, stats, 'from Codex session');
571
+ return;
572
+ }
573
+ }
546
574
  // Fallback: generate from tracked data
547
575
  const latestAgent = agentService.getAgent(payload.agentId) || agent;
548
576
  const stats = buildStatsFromTrackedData(latestAgent);