throughline 0.4.10 → 0.4.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -3
- package/README.ja.md +27 -0
- package/README.md +66 -35
- package/bin/throughline.mjs +4 -2
- package/codex/skills/throughline/SKILL.md +30 -29
- package/codex/skills/throughline/agents/openai.yaml +2 -2
- package/docs/THROUGHLINE_CLEAR_AUTO_HANDOFF_PLAN.md +7 -4
- package/docs/THROUGHLINE_CODEX_FIRST_ROADMAP.md +10 -10
- package/docs/THROUGHLINE_CODEX_TRIM_ROLLBACK_FIX_PLAN.md +19 -16
- package/docs/throughline-rollback-context-trim-insight.md +11 -7
- package/package.json +2 -2
- package/src/cli/codex-handoff-model-smoke.mjs +7 -7
- package/src/cli/codex-handoff-smoke.mjs +7 -7
- package/src/cli/codex-handoff-start.mjs +184 -10
- package/src/cli/codex-handoff-start.test.mjs +80 -1
- package/src/cli/codex-hook.mjs +37 -110
- package/src/cli/codex-hook.test.mjs +147 -32
- package/src/cli/codex-resume.mjs +7 -7
- package/src/cli/codex-summarize.mjs +7 -7
- package/src/cli/handoff-preview.mjs +6 -6
- package/src/cli/help.test.mjs +3 -1
- package/src/cli/install.mjs +42 -6
- package/src/cli/install.test.mjs +39 -5
- package/src/codex-app-server.mjs +156 -0
- package/src/codex-app-server.test.mjs +8 -0
- package/src/codex-auto-refresh.mjs +361 -29
- package/src/codex-auto-refresh.test.mjs +357 -0
- package/src/codex-thread-index.mjs +8 -2
- package/src/project-path.mjs +20 -0
- package/src/resume-context.mjs +58 -2
- package/src/resume-context.test.mjs +142 -6
- package/src/test-env.mjs +4 -0
- package/src/trim-model.mjs +6 -6
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir, platform } from 'node:os';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
|
|
1
5
|
import { runCodexTrimExecution } from './codex-app-server.mjs';
|
|
2
6
|
import { buildCodexRolloutTrimSource } from './codex-rollout-memory.mjs';
|
|
3
7
|
import { buildTrimPlan } from './trim-model.mjs';
|
|
4
8
|
|
|
5
9
|
export const CODEX_AUTO_REFRESH_THRESHOLD = 0.75;
|
|
10
|
+
export const CODEX_AUTO_REFRESH_USAGE_EPOCH_PERCENT = 5;
|
|
11
|
+
export const CODEX_AUTO_REFRESH_STATE_VERSION = 1;
|
|
12
|
+
|
|
13
|
+
const MAX_AUTO_REFRESH_STATE_ENTRIES = 50;
|
|
6
14
|
|
|
7
15
|
export function evaluateCodexAutoRefreshUsage(usage, { threshold = CODEX_AUTO_REFRESH_THRESHOLD } = {}) {
|
|
8
16
|
if (!usage) {
|
|
@@ -72,6 +80,8 @@ export async function runCodexAutoRefresh({
|
|
|
72
80
|
usage = null,
|
|
73
81
|
threshold = CODEX_AUTO_REFRESH_THRESHOLD,
|
|
74
82
|
command = process.env.THROUGHLINE_CODEX_APP_SERVER_BIN ?? 'codex',
|
|
83
|
+
autoRefreshStateStore = null,
|
|
84
|
+
enabled = false,
|
|
75
85
|
deps = {},
|
|
76
86
|
} = {}) {
|
|
77
87
|
if (!db) throw new Error('runCodexAutoRefresh: db is required');
|
|
@@ -82,6 +92,13 @@ export async function runCodexAutoRefresh({
|
|
|
82
92
|
throw new Error('runCodexAutoRefresh: projectPath is required');
|
|
83
93
|
}
|
|
84
94
|
|
|
95
|
+
if (!enabled) {
|
|
96
|
+
return {
|
|
97
|
+
status: 'skipped',
|
|
98
|
+
reason: 'codex_auto_refresh_disabled',
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
85
102
|
const decision = evaluateCodexAutoRefreshUsage(usage, { threshold });
|
|
86
103
|
if (!decision.shouldRefresh) {
|
|
87
104
|
return {
|
|
@@ -120,31 +137,90 @@ export async function runCodexAutoRefresh({
|
|
|
120
137
|
codexThreadIdSource,
|
|
121
138
|
trimSource,
|
|
122
139
|
});
|
|
123
|
-
|
|
140
|
+
|
|
141
|
+
const rolloutState = buildCodexAutoRefreshRolloutState({ trimSource, plan });
|
|
142
|
+
const fingerprint = buildCodexAutoRefreshFingerprint({
|
|
143
|
+
threadId,
|
|
144
|
+
projectPath,
|
|
145
|
+
usage,
|
|
146
|
+
rolloutState,
|
|
147
|
+
});
|
|
148
|
+
const backoff = checkCodexAutoRefreshBackoff({
|
|
149
|
+
autoRefreshStateStore,
|
|
150
|
+
threadId,
|
|
151
|
+
fingerprint,
|
|
152
|
+
family: 'execute',
|
|
153
|
+
suppressFamilies: ['execute'],
|
|
154
|
+
rolloutState,
|
|
155
|
+
});
|
|
156
|
+
if (backoff.shouldBackoff) {
|
|
124
157
|
return {
|
|
125
158
|
status: 'skipped',
|
|
126
|
-
reason:
|
|
159
|
+
reason: 'auto_refresh_backoff',
|
|
127
160
|
decision,
|
|
128
161
|
plan,
|
|
162
|
+
backoff,
|
|
129
163
|
};
|
|
130
164
|
}
|
|
165
|
+
|
|
166
|
+
if (plan.status === 'unavailable') {
|
|
167
|
+
return recordCodexAutoRefreshResult({
|
|
168
|
+
autoRefreshStateStore,
|
|
169
|
+
threadId,
|
|
170
|
+
fingerprint,
|
|
171
|
+
family: 'execute',
|
|
172
|
+
rolloutState,
|
|
173
|
+
result: {
|
|
174
|
+
status: 'skipped',
|
|
175
|
+
reason: plan.reason,
|
|
176
|
+
decision,
|
|
177
|
+
plan,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
}
|
|
131
181
|
if (plan.trim.rollbackTurns < 1) {
|
|
132
|
-
return {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
182
|
+
return recordCodexAutoRefreshResult({
|
|
183
|
+
autoRefreshStateStore,
|
|
184
|
+
threadId,
|
|
185
|
+
fingerprint,
|
|
186
|
+
family: 'execute',
|
|
187
|
+
rolloutState,
|
|
188
|
+
result: {
|
|
189
|
+
status: 'skipped',
|
|
190
|
+
reason: 'nothing_to_trim',
|
|
191
|
+
decision,
|
|
192
|
+
plan,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
138
195
|
}
|
|
139
196
|
if (!hasInjectableMemory(plan.memoryPreview)) {
|
|
140
|
-
return {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
197
|
+
return recordCodexAutoRefreshResult({
|
|
198
|
+
autoRefreshStateStore,
|
|
199
|
+
threadId,
|
|
200
|
+
fingerprint,
|
|
201
|
+
family: 'execute',
|
|
202
|
+
rolloutState,
|
|
203
|
+
result: {
|
|
204
|
+
status: 'skipped',
|
|
205
|
+
reason: 'injectable_memory_required',
|
|
206
|
+
decision,
|
|
207
|
+
plan,
|
|
208
|
+
},
|
|
209
|
+
});
|
|
146
210
|
}
|
|
147
211
|
|
|
212
|
+
recordCodexAutoRefreshOutcome({
|
|
213
|
+
autoRefreshStateStore,
|
|
214
|
+
threadId,
|
|
215
|
+
fingerprint,
|
|
216
|
+
family: 'execute',
|
|
217
|
+
outcome: {
|
|
218
|
+
status: 'started',
|
|
219
|
+
reason: 'threshold_reached',
|
|
220
|
+
},
|
|
221
|
+
rolloutState,
|
|
222
|
+
});
|
|
223
|
+
|
|
148
224
|
const execution = await executeTrim({
|
|
149
225
|
threadId,
|
|
150
226
|
cwd: projectPath,
|
|
@@ -154,35 +230,228 @@ export async function runCodexAutoRefresh({
|
|
|
154
230
|
command,
|
|
155
231
|
});
|
|
156
232
|
if (execution.status === 'refused') {
|
|
157
|
-
return {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
233
|
+
return recordCodexAutoRefreshResult({
|
|
234
|
+
autoRefreshStateStore,
|
|
235
|
+
threadId,
|
|
236
|
+
fingerprint,
|
|
237
|
+
family: 'execute',
|
|
238
|
+
rolloutState,
|
|
239
|
+
result: {
|
|
240
|
+
status: 'refused',
|
|
241
|
+
reason: execution.reason,
|
|
242
|
+
decision,
|
|
243
|
+
plan,
|
|
244
|
+
execution,
|
|
245
|
+
},
|
|
246
|
+
});
|
|
164
247
|
}
|
|
165
248
|
|
|
166
249
|
const postInjectVisibilityStatus = execution.postInjectVisibilityCheck?.status ?? 'unchecked';
|
|
167
250
|
if (postInjectVisibilityStatus !== 'match') {
|
|
168
|
-
return {
|
|
169
|
-
|
|
170
|
-
|
|
251
|
+
return recordCodexAutoRefreshResult({
|
|
252
|
+
autoRefreshStateStore,
|
|
253
|
+
threadId,
|
|
254
|
+
fingerprint,
|
|
255
|
+
family: 'execute',
|
|
256
|
+
rolloutState,
|
|
257
|
+
quietThreadUntilNewUserTurn: Boolean(execution.rollbackSent || execution.injectSent),
|
|
258
|
+
result: {
|
|
259
|
+
status: 'unverified',
|
|
260
|
+
reason: execution.postInjectVisibilityCheck?.reason ?? 'post_inject_visibility_unverified',
|
|
261
|
+
decision,
|
|
262
|
+
plan,
|
|
263
|
+
execution,
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return recordCodexAutoRefreshResult({
|
|
269
|
+
autoRefreshStateStore,
|
|
270
|
+
threadId,
|
|
271
|
+
fingerprint,
|
|
272
|
+
family: 'execute',
|
|
273
|
+
rolloutState,
|
|
274
|
+
quietThreadUntilNewUserTurn: Boolean(execution.rollbackSent || execution.injectSent),
|
|
275
|
+
result: {
|
|
276
|
+
status: 'refreshed-live',
|
|
277
|
+
reason: 'rollback_and_inject_sent_live',
|
|
171
278
|
decision,
|
|
172
279
|
plan,
|
|
173
280
|
execution,
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function buildCodexAutoRefreshRolloutState({ captured = null, trimSource = null, plan = null } = {}) {
|
|
286
|
+
const stats = trimSource?.stats ?? plan?.trim?.rolloutStats ?? captured?.stats ?? {};
|
|
287
|
+
return {
|
|
288
|
+
rolloutPath: plan?.trim?.rolloutPath ?? trimSource?.rolloutPath ?? captured?.rolloutPath ?? null,
|
|
289
|
+
capturedTurns: numberOrNull(plan?.trim?.capturedTurns ?? trimSource?.capturedTurns ?? captured?.capturedTurns),
|
|
290
|
+
rollbackTurns: numberOrNull(plan?.trim?.rollbackTurns),
|
|
291
|
+
capturedRows: numberOrNull(captured?.capturedRows),
|
|
292
|
+
capturedDetails: numberOrNull(captured?.capturedDetails),
|
|
293
|
+
rollbackEvents: numberOrNull(stats?.rollbackEvents),
|
|
294
|
+
rolledBackTurns: numberOrNull(stats?.rolledBackTurns),
|
|
295
|
+
injectedDeveloperMessages: numberOrNull(stats?.injectedDeveloperMessages),
|
|
296
|
+
userMessagesAfterRollback: numberOrNull(stats?.userMessagesAfterRollback),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function buildCodexAutoRefreshFingerprint({
|
|
301
|
+
threadId,
|
|
302
|
+
projectPath,
|
|
303
|
+
usage,
|
|
304
|
+
rolloutState,
|
|
305
|
+
} = {}) {
|
|
306
|
+
return JSON.stringify({
|
|
307
|
+
version: CODEX_AUTO_REFRESH_STATE_VERSION,
|
|
308
|
+
threadId: typeof threadId === 'string' ? threadId : null,
|
|
309
|
+
projectPath: normalizeProjectPath(projectPath),
|
|
310
|
+
usageEpochPercent: usageEpochPercent(usage),
|
|
311
|
+
contextWindowSize: numberOrNull(usage?.contextWindowSize),
|
|
312
|
+
usageEstimated: Boolean(usage?.estimated),
|
|
313
|
+
contextWindowEstimated: Boolean(usage?.contextWindowEstimated),
|
|
314
|
+
usageSource: typeof usage?.source === 'string' ? usage.source : null,
|
|
315
|
+
rolloutPath: rolloutState?.rolloutPath ?? null,
|
|
316
|
+
capturedTurns: numberOrNull(rolloutState?.capturedTurns),
|
|
317
|
+
rollbackTurns: numberOrNull(rolloutState?.rollbackTurns),
|
|
318
|
+
rollbackEvents: numberOrNull(rolloutState?.rollbackEvents),
|
|
319
|
+
rolledBackTurns: numberOrNull(rolloutState?.rolledBackTurns),
|
|
320
|
+
injectedDeveloperMessages: numberOrNull(rolloutState?.injectedDeveloperMessages),
|
|
321
|
+
userMessagesAfterRollback: numberOrNull(rolloutState?.userMessagesAfterRollback),
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function checkCodexAutoRefreshBackoff({
|
|
326
|
+
autoRefreshStateStore,
|
|
327
|
+
threadId,
|
|
328
|
+
fingerprint,
|
|
329
|
+
family,
|
|
330
|
+
suppressFamilies = [family],
|
|
331
|
+
rolloutState = null,
|
|
332
|
+
} = {}) {
|
|
333
|
+
if (!autoRefreshStateStore) {
|
|
334
|
+
return { shouldBackoff: false, reason: 'state_store_unavailable' };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const state = normalizeAutoRefreshState(autoRefreshStateStore.read(threadId), threadId);
|
|
338
|
+
const quiet = state.threadQuiet;
|
|
339
|
+
if (
|
|
340
|
+
quiet?.family === 'execute' &&
|
|
341
|
+
suppressFamilies.includes('execute') &&
|
|
342
|
+
shouldSuppressForThreadQuiet(quiet, rolloutState)
|
|
343
|
+
) {
|
|
344
|
+
return {
|
|
345
|
+
shouldBackoff: true,
|
|
346
|
+
reason: 'thread_quiet_until_new_user_turn',
|
|
347
|
+
family,
|
|
348
|
+
suppressFamilies,
|
|
349
|
+
entry: quiet,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const entry = state.entries.find(
|
|
354
|
+
(candidate) => suppressFamilies.includes(candidate.family) && candidate.fingerprint === fingerprint,
|
|
355
|
+
);
|
|
356
|
+
if (entry) {
|
|
357
|
+
return {
|
|
358
|
+
shouldBackoff: true,
|
|
359
|
+
reason: 'matching_refresh_state',
|
|
360
|
+
family,
|
|
361
|
+
suppressFamilies,
|
|
362
|
+
entry,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
shouldBackoff: false,
|
|
368
|
+
reason: 'no_matching_refresh_state',
|
|
369
|
+
family,
|
|
370
|
+
suppressFamilies,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function recordCodexAutoRefreshOutcome({
|
|
375
|
+
autoRefreshStateStore,
|
|
376
|
+
threadId,
|
|
377
|
+
fingerprint,
|
|
378
|
+
family,
|
|
379
|
+
outcome,
|
|
380
|
+
rolloutState = null,
|
|
381
|
+
quietThreadUntilNewUserTurn = false,
|
|
382
|
+
} = {}) {
|
|
383
|
+
if (!autoRefreshStateStore) return null;
|
|
384
|
+
|
|
385
|
+
const state = normalizeAutoRefreshState(autoRefreshStateStore.read(threadId), threadId);
|
|
386
|
+
const entry = {
|
|
387
|
+
family,
|
|
388
|
+
fingerprint,
|
|
389
|
+
outcomeStatus: outcome?.status ?? null,
|
|
390
|
+
outcomeReason: outcome?.reason ?? null,
|
|
391
|
+
recordedAt: Date.now(),
|
|
392
|
+
rolloutState,
|
|
393
|
+
};
|
|
394
|
+
state.entries = [
|
|
395
|
+
entry,
|
|
396
|
+
...state.entries.filter(
|
|
397
|
+
(candidate) => candidate.family !== family || candidate.fingerprint !== fingerprint,
|
|
398
|
+
),
|
|
399
|
+
].slice(0, MAX_AUTO_REFRESH_STATE_ENTRIES);
|
|
400
|
+
|
|
401
|
+
if (quietThreadUntilNewUserTurn) {
|
|
402
|
+
state.threadQuiet = {
|
|
403
|
+
family,
|
|
404
|
+
reason: 'refresh_sent_quiet_until_new_user_turn',
|
|
405
|
+
outcomeStatus: outcome?.status ?? null,
|
|
406
|
+
outcomeReason: outcome?.reason ?? null,
|
|
407
|
+
recordedAt: entry.recordedAt,
|
|
408
|
+
userMessagesAfterRollback: numberOrNull(rolloutState?.userMessagesAfterRollback),
|
|
409
|
+
rollbackEvents: numberOrNull(rolloutState?.rollbackEvents),
|
|
410
|
+
injectedDeveloperMessages: numberOrNull(rolloutState?.injectedDeveloperMessages),
|
|
174
411
|
};
|
|
175
412
|
}
|
|
176
413
|
|
|
414
|
+
autoRefreshStateStore.write(threadId, state);
|
|
415
|
+
return entry;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function createFileCodexAutoRefreshStateStore({
|
|
419
|
+
dir = join(homedir(), '.throughline', 'codex-auto-refresh'),
|
|
420
|
+
} = {}) {
|
|
177
421
|
return {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
422
|
+
read(threadId) {
|
|
423
|
+
const file = codexAutoRefreshStatePath(dir, threadId);
|
|
424
|
+
if (!existsSync(file)) return null;
|
|
425
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
426
|
+
},
|
|
427
|
+
write(threadId, state) {
|
|
428
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
429
|
+
writeFileSync(codexAutoRefreshStatePath(dir, threadId), JSON.stringify(state));
|
|
430
|
+
},
|
|
183
431
|
};
|
|
184
432
|
}
|
|
185
433
|
|
|
434
|
+
function recordCodexAutoRefreshResult({
|
|
435
|
+
autoRefreshStateStore,
|
|
436
|
+
threadId,
|
|
437
|
+
fingerprint,
|
|
438
|
+
family,
|
|
439
|
+
rolloutState,
|
|
440
|
+
quietThreadUntilNewUserTurn = false,
|
|
441
|
+
result,
|
|
442
|
+
}) {
|
|
443
|
+
recordCodexAutoRefreshOutcome({
|
|
444
|
+
autoRefreshStateStore,
|
|
445
|
+
threadId,
|
|
446
|
+
fingerprint,
|
|
447
|
+
family,
|
|
448
|
+
outcome: result,
|
|
449
|
+
rolloutState,
|
|
450
|
+
quietThreadUntilNewUserTurn,
|
|
451
|
+
});
|
|
452
|
+
return result;
|
|
453
|
+
}
|
|
454
|
+
|
|
186
455
|
function hasInjectableMemory(memoryPreview) {
|
|
187
456
|
const text = memoryPreview?.text;
|
|
188
457
|
return (
|
|
@@ -192,3 +461,66 @@ function hasInjectableMemory(memoryPreview) {
|
|
|
192
461
|
text !== '(no captured memory available)'
|
|
193
462
|
);
|
|
194
463
|
}
|
|
464
|
+
|
|
465
|
+
function normalizeAutoRefreshState(value, threadId) {
|
|
466
|
+
if (!value || typeof value !== 'object') {
|
|
467
|
+
return {
|
|
468
|
+
version: CODEX_AUTO_REFRESH_STATE_VERSION,
|
|
469
|
+
threadId,
|
|
470
|
+
entries: [],
|
|
471
|
+
threadQuiet: null,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
version: CODEX_AUTO_REFRESH_STATE_VERSION,
|
|
476
|
+
threadId: typeof value.threadId === 'string' ? value.threadId : threadId,
|
|
477
|
+
entries: Array.isArray(value.entries) ? value.entries.filter(isRefreshStateEntry) : [],
|
|
478
|
+
threadQuiet: value.threadQuiet && typeof value.threadQuiet === 'object' ? value.threadQuiet : null,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function isRefreshStateEntry(entry) {
|
|
483
|
+
return (
|
|
484
|
+
entry &&
|
|
485
|
+
typeof entry === 'object' &&
|
|
486
|
+
typeof entry.family === 'string' &&
|
|
487
|
+
typeof entry.fingerprint === 'string'
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function shouldSuppressForThreadQuiet(quiet, rolloutState) {
|
|
492
|
+
const quietUserMessages = numberOrNull(quiet?.userMessagesAfterRollback);
|
|
493
|
+
const currentUserMessages = numberOrNull(rolloutState?.userMessagesAfterRollback);
|
|
494
|
+
if (quietUserMessages === null || currentUserMessages === null) return false;
|
|
495
|
+
return currentUserMessages <= quietUserMessages;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function usageEpochPercent(usage) {
|
|
499
|
+
const tokens = Number(usage?.tokens);
|
|
500
|
+
const contextWindowSize = Number(usage?.contextWindowSize);
|
|
501
|
+
if (!Number.isFinite(tokens) || !Number.isFinite(contextWindowSize) || contextWindowSize <= 0) {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
const percent = (tokens / contextWindowSize) * 100;
|
|
505
|
+
return Math.floor(percent / CODEX_AUTO_REFRESH_USAGE_EPOCH_PERCENT) * CODEX_AUTO_REFRESH_USAGE_EPOCH_PERCENT;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function normalizeProjectPath(value) {
|
|
509
|
+
if (typeof value !== 'string' || value.length === 0) return '';
|
|
510
|
+
let result = resolve(value).replace(/\\/g, '/');
|
|
511
|
+
if (result.length > 1 && result.endsWith('/')) result = result.slice(0, -1);
|
|
512
|
+
if (platform() === 'win32') result = result.toLowerCase();
|
|
513
|
+
return result;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function numberOrNull(value) {
|
|
517
|
+
const number = Number(value);
|
|
518
|
+
return Number.isFinite(number) ? number : null;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function codexAutoRefreshStatePath(dir, threadId) {
|
|
522
|
+
if (typeof threadId !== 'string' || threadId.length === 0) {
|
|
523
|
+
throw new Error('codex auto-refresh state requires threadId');
|
|
524
|
+
}
|
|
525
|
+
return join(dir, `${encodeURIComponent(threadId)}.json`);
|
|
526
|
+
}
|