codex-overleaf-link 2.4.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -24
- package/README.zh-CN.md +27 -24
- package/extension/src/content/contentRuntime.js +74 -73
- package/extension/src/content/generated/content.bundle.js +657 -445
- package/extension/src/content/generated/content.bundle.meta.json +3 -3
- package/extension/src/content/panelMaintenance.js +8 -4
- package/extension/src/content/recentProjects.js +354 -355
- package/extension/src/content/runTimelineView.js +8 -10
- package/extension/src/content/scopedPersistenceCoordinator.js +2 -1
- package/extension/src/content/scopedPersistencePanelState.js +45 -2
- package/extension/src/content/sessionPersistence.js +4 -1
- package/extension/src/content/settingsPanel.js +16 -2
- package/extension/src/page/saveState.js +44 -1
- package/extension/src/page/trackedChangesLifecycle.js +4 -1
- package/extension/src/page/treeOperations.js +10 -27
- package/extension/src/page/writebackRouter.js +1 -1
- package/extension/src/pageBridge.js +3 -0
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/i18n.js +94 -40
- package/extension/src/shared/storageDb.js +1 -0
- package/extension/src/shared/storageRunActions.js +44 -0
- package/extension/styles/panel.css +199 -395
- package/native-host/src/codexSessionRunner.js +41 -38
- package/native-host/src/codexSessionTiming.js +68 -0
- package/native-host/src/readProgressGuard.js +84 -17
- package/package.json +1 -1
|
@@ -25,12 +25,14 @@ const { createSubagentBroker } = require('./subagentBroker');
|
|
|
25
25
|
const { prepareBinaryAssetChanges } = require('./nativeAssetTransfer');
|
|
26
26
|
const { resolveCodexCommand, shouldUseShellForCommand } = require('./codexCommand');
|
|
27
27
|
const { applyProviderEnvironment, buildProviderConfigArgs, prepareProviderLaunch } = require('./codexProviderLaunch');
|
|
28
|
-
const { buildReadProgressRules, createReadProgressController } = require('./readProgressGuard');
|
|
28
|
+
const { buildReadProgressRules, createReadProgressController, createRunEventScope } = require('./readProgressGuard');
|
|
29
29
|
const {
|
|
30
30
|
createCodexIdleWatchdog,
|
|
31
31
|
createOptionalTimeout,
|
|
32
32
|
getAbortReason,
|
|
33
33
|
parseOptionalPositiveInteger,
|
|
34
|
+
rejectPendingRequests,
|
|
35
|
+
stopCodexAppServer,
|
|
34
36
|
throwIfAborted
|
|
35
37
|
} = require('./codexSessionTiming');
|
|
36
38
|
|
|
@@ -806,23 +808,26 @@ function runCodexAppServerProcess(input) {
|
|
|
806
808
|
const child = spawn(codexCommand, buildCodexAppServerArgs(input), {
|
|
807
809
|
env: childEnv,
|
|
808
810
|
shell: shouldUseShellForCommand(codexCommand, childEnv),
|
|
811
|
+
detached: process.platform !== 'win32',
|
|
809
812
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
810
813
|
});
|
|
811
814
|
const pending = new Map();
|
|
812
815
|
let nextId = 1;
|
|
813
816
|
let stdoutBuffer = '';
|
|
814
817
|
let stderr = '';
|
|
815
|
-
let activeThreadId = '';
|
|
818
|
+
let activeThreadId = String(input.threadId || '');
|
|
816
819
|
let activeTurnId = '';
|
|
817
820
|
let controlPublished = false;
|
|
818
821
|
const assistantMessages = new Map();
|
|
819
822
|
const assistantMessageOrder = [];
|
|
823
|
+
const eventScope = createRunEventScope(() => ({ threadId: activeThreadId, turnId: activeTurnId }));
|
|
820
824
|
const readProgressController = createReadProgressController({
|
|
821
825
|
input, request, fail,
|
|
822
826
|
getTurn: () => ({ threadId: activeThreadId, turnId: activeTurnId }),
|
|
823
827
|
emitEvent: (...args) => emitCodexEvent(input.emit, ...args)
|
|
824
828
|
});
|
|
825
829
|
let settled = false;
|
|
830
|
+
let stopping = false;
|
|
826
831
|
// Two-layer timeout strategy:
|
|
827
832
|
// 1. Optional absolute deadline (CODEX_OVERLEAF_CODEX_TIMEOUT_MS) —
|
|
828
833
|
// legacy override; off by default. When set, the whole run must
|
|
@@ -847,23 +852,14 @@ function runCodexAppServerProcess(input) {
|
|
|
847
852
|
}
|
|
848
853
|
fail(new Error(`Codex app-server produced no events for ${ms}ms (idle watchdog); the run was aborted to release the project lock.`));
|
|
849
854
|
});
|
|
850
|
-
const onAbort = () =>
|
|
851
|
-
const reason = getAbortReason(input.signal);
|
|
852
|
-
if (!activeThreadId || !activeTurnId) {
|
|
853
|
-
fail(reason);
|
|
854
|
-
return;
|
|
855
|
-
}
|
|
856
|
-
Promise.race([
|
|
857
|
-
interruptActiveTurn().catch(() => null),
|
|
858
|
-
new Promise(resolveDelay => setTimeout(resolveDelay, 1500))
|
|
859
|
-
]).finally(() => fail(reason));
|
|
860
|
-
};
|
|
855
|
+
const onAbort = () => fail(getAbortReason(input.signal));
|
|
861
856
|
input.signal?.addEventListener('abort', onAbort, { once: true });
|
|
862
857
|
|
|
863
858
|
child.stdout.setEncoding('utf8');
|
|
864
859
|
child.stderr.setEncoding('utf8');
|
|
865
860
|
child.stdout.on('data', chunk => {
|
|
866
|
-
|
|
861
|
+
if (settled) return;
|
|
862
|
+
if (!stopping) idleWatchdog.reset();
|
|
867
863
|
stdoutBuffer += chunk;
|
|
868
864
|
const lines = stdoutBuffer.split(/\r?\n/);
|
|
869
865
|
stdoutBuffer = lines.pop() || '';
|
|
@@ -876,6 +872,7 @@ function runCodexAppServerProcess(input) {
|
|
|
876
872
|
child.stderr.on('data', chunk => {
|
|
877
873
|
stderr += chunk;
|
|
878
874
|
});
|
|
875
|
+
child.stdin.on?.('error', fail);
|
|
879
876
|
child.on('error', fail);
|
|
880
877
|
child.on('close', code => {
|
|
881
878
|
if (settled) {
|
|
@@ -951,12 +948,14 @@ function runCodexAppServerProcess(input) {
|
|
|
951
948
|
}
|
|
952
949
|
|
|
953
950
|
function request(method, params) {
|
|
954
|
-
|
|
951
|
+
if (settled || (stopping && method !== 'turn/interrupt')) return Promise.reject(new Error('Codex turn is stopping or has ended.'));
|
|
952
|
+
if (!stopping) idleWatchdog.reset();
|
|
955
953
|
const id = nextId++;
|
|
956
954
|
const message = { id, method, params };
|
|
957
955
|
child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
958
956
|
return new Promise((resolveRequest, rejectRequest) => {
|
|
959
957
|
pending.set(id, {
|
|
958
|
+
method, params,
|
|
960
959
|
resolve: resolveRequest,
|
|
961
960
|
reject: rejectRequest
|
|
962
961
|
});
|
|
@@ -972,10 +971,12 @@ function runCodexAppServerProcess(input) {
|
|
|
972
971
|
}
|
|
973
972
|
|
|
974
973
|
function handleMessage(line) {
|
|
974
|
+
if (settled) return;
|
|
975
975
|
let message;
|
|
976
976
|
try {
|
|
977
977
|
message = JSON.parse(line);
|
|
978
978
|
} catch {
|
|
979
|
+
if (stopping) return;
|
|
979
980
|
emitCodexEvent(input.emit, 'codex.session.raw', 'Codex app-server emitted non-JSON output', {
|
|
980
981
|
text: truncateText(line, 1000)
|
|
981
982
|
});
|
|
@@ -992,23 +993,29 @@ function runCodexAppServerProcess(input) {
|
|
|
992
993
|
if (message.error) {
|
|
993
994
|
pendingRequest.reject(new Error(message.error.message || JSON.stringify(message.error)));
|
|
994
995
|
} else {
|
|
996
|
+
if (pendingRequest.method === 'thread/start' || pendingRequest.method === 'thread/resume') {
|
|
997
|
+
activeThreadId = message.result?.thread?.id || message.result?.threadId || pendingRequest.params?.threadId || activeThreadId;
|
|
998
|
+
}
|
|
999
|
+
if (pendingRequest.method === 'turn/start') activeTurnId = message.result?.turn?.id || activeTurnId;
|
|
995
1000
|
pendingRequest.resolve(message.result);
|
|
996
1001
|
}
|
|
997
1002
|
return;
|
|
998
1003
|
}
|
|
999
1004
|
|
|
1000
1005
|
if (Object.prototype.hasOwnProperty.call(message, 'id') && message.method) {
|
|
1006
|
+
if (stopping) { response(message.id, { decision: 'decline' }); return; }
|
|
1001
1007
|
handleServerRequest(message);
|
|
1002
1008
|
return;
|
|
1003
1009
|
}
|
|
1004
1010
|
|
|
1005
1011
|
if (message.method) {
|
|
1012
|
+
if (stopping || !eventScope.accepts(message.params)) return;
|
|
1006
1013
|
if (message.method === 'turn/started') {
|
|
1007
|
-
|
|
1008
|
-
activeTurnId = message.params?.turn?.id || message.params?.turnId ||
|
|
1014
|
+
const threadId = message.params?.thread?.id || message.params?.threadId;
|
|
1015
|
+
if (!activeTurnId && threadId === activeThreadId) activeTurnId = message.params?.turn?.id || message.params?.turnId || '';
|
|
1009
1016
|
publishActiveTurnControl();
|
|
1010
1017
|
}
|
|
1011
|
-
recordAssistantMessage(message);
|
|
1018
|
+
if (activeTurnId) recordAssistantMessage(message);
|
|
1012
1019
|
// For `error` events surface the actual error text as the visible
|
|
1013
1020
|
// title so the run timeline reads "Reconnecting... 2/5" instead of
|
|
1014
1021
|
// a generic "error". Other methods continue to use the method name.
|
|
@@ -1019,9 +1026,9 @@ function runCodexAppServerProcess(input) {
|
|
|
1019
1026
|
method: message.method,
|
|
1020
1027
|
params: message.params || {}
|
|
1021
1028
|
}, inferNotificationStatus(message));
|
|
1022
|
-
if (message.method === 'item/completed' &&
|
|
1029
|
+
if (message.method === 'item/completed' && eventScope.owns(message.params) &&
|
|
1023
1030
|
!readProgressController.observe(message.params?.item || {})) return;
|
|
1024
|
-
if (message.method === 'turn/completed' &&
|
|
1031
|
+
if (message.method === 'turn/completed' && activeTurnId && (message.params?.turn?.id === activeTurnId || message.params?.turnId === activeTurnId)) {
|
|
1025
1032
|
succeed();
|
|
1026
1033
|
}
|
|
1027
1034
|
if (message.method === 'error' && isTransientCodexAppServerError(message.params)) {
|
|
@@ -1084,7 +1091,7 @@ function runCodexAppServerProcess(input) {
|
|
|
1084
1091
|
}
|
|
1085
1092
|
|
|
1086
1093
|
function publishActiveTurnControl() {
|
|
1087
|
-
if (controlPublished || !activeThreadId || !activeTurnId) {
|
|
1094
|
+
if (settled || stopping || controlPublished || !activeThreadId || !activeTurnId) {
|
|
1088
1095
|
return;
|
|
1089
1096
|
}
|
|
1090
1097
|
controlPublished = true;
|
|
@@ -1100,6 +1107,7 @@ function runCodexAppServerProcess(input) {
|
|
|
1100
1107
|
interrupt: () => interruptActiveTurn()
|
|
1101
1108
|
});
|
|
1102
1109
|
input.onControlReady?.(control);
|
|
1110
|
+
if (settled || stopping) return;
|
|
1103
1111
|
emitCodexEvent(input.emit, 'codex.turn.bound', 'Codex turn is ready for follow-up guidance', {
|
|
1104
1112
|
threadId: activeThreadId,
|
|
1105
1113
|
turnId: activeTurnId
|
|
@@ -1117,16 +1125,13 @@ function runCodexAppServerProcess(input) {
|
|
|
1117
1125
|
}
|
|
1118
1126
|
|
|
1119
1127
|
function succeed() {
|
|
1120
|
-
if (settled) {
|
|
1128
|
+
if (settled || stopping) {
|
|
1121
1129
|
return;
|
|
1122
1130
|
}
|
|
1123
1131
|
settled = true;
|
|
1124
1132
|
const turnEndedError = new Error('Codex turn ended before the pending app-server request completed.');
|
|
1125
1133
|
turnEndedError.code = 'codex_turn_ended';
|
|
1126
|
-
|
|
1127
|
-
pendingRequest.reject(turnEndedError);
|
|
1128
|
-
}
|
|
1129
|
-
pending.clear();
|
|
1134
|
+
rejectPendingRequests(pending, turnEndedError);
|
|
1130
1135
|
cleanup();
|
|
1131
1136
|
child.kill('SIGTERM');
|
|
1132
1137
|
resolve({
|
|
@@ -1136,19 +1141,17 @@ function runCodexAppServerProcess(input) {
|
|
|
1136
1141
|
}
|
|
1137
1142
|
|
|
1138
1143
|
function fail(error) {
|
|
1139
|
-
if (settled)
|
|
1140
|
-
|
|
1141
|
-
}
|
|
1142
|
-
settled = true;
|
|
1144
|
+
if (settled || stopping) return;
|
|
1145
|
+
stopping = true;
|
|
1143
1146
|
cleanup();
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1147
|
+
rejectPendingRequests(pending, error);
|
|
1148
|
+
return stopCodexAppServer(child, {
|
|
1149
|
+
interrupt: interruptActiveTurn, reason: error, processGroup: process.platform !== 'win32'
|
|
1150
|
+
}).then(() => error, stopError => stopError).then(finalError => {
|
|
1151
|
+
settled = true;
|
|
1152
|
+
rejectPendingRequests(pending, finalError);
|
|
1153
|
+
reject(finalError);
|
|
1154
|
+
});
|
|
1152
1155
|
}
|
|
1153
1156
|
|
|
1154
1157
|
function cleanup() {
|
|
@@ -36,10 +36,78 @@ function throwIfAborted(signal) {
|
|
|
36
36
|
if (signal?.aborted) throw getAbortReason(signal);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
function rejectPendingRequests(pending, error) {
|
|
40
|
+
for (const entry of pending.values()) entry.reject(error);
|
|
41
|
+
pending.clear();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function stopCodexAppServer(child, {
|
|
45
|
+
interrupt, reason, processGroup = false, platform = process.platform,
|
|
46
|
+
interruptTimeoutMs = 1500, terminateTimeoutMs = 500, forceTimeoutMs = 1500,
|
|
47
|
+
kill = process.kill.bind(process), taskkill = defaultTaskkill
|
|
48
|
+
} = {}) {
|
|
49
|
+
const pid = Number(child?.pid);
|
|
50
|
+
if (!Number.isInteger(pid) || pid <= 0) return;
|
|
51
|
+
const grouped = processGroup && platform !== 'win32';
|
|
52
|
+
const exited = () => child.exitCode != null || child.signalCode != null;
|
|
53
|
+
const groupGone = () => {
|
|
54
|
+
if (!grouped) return true;
|
|
55
|
+
try { kill(-pid, 0); return false; } catch (error) { return error?.code === 'ESRCH'; }
|
|
56
|
+
};
|
|
57
|
+
const stopped = () => exited() && groupGone();
|
|
58
|
+
const waitForStop = async timeoutMs => {
|
|
59
|
+
const deadline = Date.now() + timeoutMs;
|
|
60
|
+
while (!stopped() && Date.now() < deadline) {
|
|
61
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(20, Math.max(1, deadline - Date.now()))));
|
|
62
|
+
}
|
|
63
|
+
return stopped();
|
|
64
|
+
};
|
|
65
|
+
const signal = value => {
|
|
66
|
+
if (grouped) {
|
|
67
|
+
try { kill(-pid, value); return; } catch (_error) { /* fall back to the owned child */ }
|
|
68
|
+
}
|
|
69
|
+
if (!exited()) {
|
|
70
|
+
try { child.kill(value); } catch (_error) { /* the exit check remains authoritative */ }
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
if (stopped()) return;
|
|
74
|
+
if (!exited() && typeof interrupt === 'function') {
|
|
75
|
+
let timer;
|
|
76
|
+
try {
|
|
77
|
+
await Promise.race([
|
|
78
|
+
Promise.resolve().then(interrupt).catch(() => {}),
|
|
79
|
+
new Promise(resolve => { timer = setTimeout(resolve, interruptTimeoutMs); })
|
|
80
|
+
]);
|
|
81
|
+
} finally { clearTimeout(timer); }
|
|
82
|
+
}
|
|
83
|
+
if (platform === 'win32') {
|
|
84
|
+
if (!exited()) await taskkill(pid, forceTimeoutMs).catch(() => {});
|
|
85
|
+
} else {
|
|
86
|
+
signal('SIGTERM');
|
|
87
|
+
if (await waitForStop(terminateTimeoutMs)) return;
|
|
88
|
+
signal('SIGKILL');
|
|
89
|
+
}
|
|
90
|
+
if (!await waitForStop(forceTimeoutMs)) {
|
|
91
|
+
const error = new Error('Codex process exit could not be confirmed. Do not retry this project until the old process has stopped.');
|
|
92
|
+
error.code = 'codex_process_stop_unconfirmed';
|
|
93
|
+
error.cause = reason;
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function defaultTaskkill(pid, timeout) {
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
require('node:child_process').execFile('taskkill', ['/pid', String(pid), '/t', '/f'],
|
|
101
|
+
{ windowsHide: true, timeout }, error => error ? reject(error) : resolve());
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
39
105
|
module.exports = {
|
|
40
106
|
createCodexIdleWatchdog,
|
|
41
107
|
createOptionalTimeout,
|
|
42
108
|
getAbortReason,
|
|
43
109
|
parseOptionalPositiveInteger,
|
|
110
|
+
rejectPendingRequests,
|
|
111
|
+
stopCodexAppServer,
|
|
44
112
|
throwIfAborted
|
|
45
113
|
};
|
|
@@ -19,6 +19,36 @@ function buildReadProgressRules() {
|
|
|
19
19
|
].join('\n');
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function createRunEventScope(getTurn = () => ({})) {
|
|
23
|
+
const owners = new Map();
|
|
24
|
+
const identity = params => ({
|
|
25
|
+
threadId: String(params?.threadId || params?.thread?.id || ''),
|
|
26
|
+
turnId: String(params?.turnId || params?.turn?.id || '')
|
|
27
|
+
});
|
|
28
|
+
const itemId = params => String(params?.itemId || params?.item?.id || '');
|
|
29
|
+
const matches = owner => {
|
|
30
|
+
const active = getTurn() || {};
|
|
31
|
+
return (!owner.threadId || owner.threadId === active.threadId)
|
|
32
|
+
&& (!owner.turnId || !active.turnId || owner.turnId === active.turnId);
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
accepts(params = {}) {
|
|
36
|
+
const owner = identity(params);
|
|
37
|
+
const id = itemId(params);
|
|
38
|
+
if (owner.threadId || owner.turnId) {
|
|
39
|
+
if (id) owners.set(id, owner);
|
|
40
|
+
return matches(owner);
|
|
41
|
+
}
|
|
42
|
+
return !owners.has(id) || matches(owners.get(id));
|
|
43
|
+
},
|
|
44
|
+
owns(params = {}) {
|
|
45
|
+
const explicit = identity(params);
|
|
46
|
+
const owner = explicit.threadId || explicit.turnId ? explicit : owners.get(itemId(params));
|
|
47
|
+
return Boolean((getTurn() || {}).turnId && owner?.threadId && matches(owner));
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
22
52
|
function createReadProgressGuard(options = {}) {
|
|
23
53
|
const settings = {
|
|
24
54
|
minReadCommands: positiveInteger(options.minReadCommands, DEFAULT_MIN_READ_COMMANDS),
|
|
@@ -37,11 +67,19 @@ function createReadProgressGuard(options = {}) {
|
|
|
37
67
|
let redundantStreak = 0;
|
|
38
68
|
let postSteerRedundantStreak = 0;
|
|
39
69
|
let steerIssued = false;
|
|
70
|
+
let steerAcknowledged = false;
|
|
40
71
|
let abortIssued = false;
|
|
41
72
|
|
|
42
73
|
return {
|
|
43
74
|
observe(item) {
|
|
44
75
|
if (abortIssued) return noAction();
|
|
76
|
+
if (item?.type === 'fileChange') {
|
|
77
|
+
coverageByFile.clear();
|
|
78
|
+
commandCounts.clear();
|
|
79
|
+
readCount = redundantStreak = postSteerRedundantStreak = 0;
|
|
80
|
+
steerIssued = steerAcknowledged = false;
|
|
81
|
+
return noAction();
|
|
82
|
+
}
|
|
45
83
|
const inspection = extractReadInspection(item, settings.workspacePath);
|
|
46
84
|
if (!inspection) {
|
|
47
85
|
if (item?.type === 'commandExecution') {
|
|
@@ -52,8 +90,9 @@ function createReadProgressGuard(options = {}) {
|
|
|
52
90
|
}
|
|
53
91
|
|
|
54
92
|
readCount += 1;
|
|
55
|
-
const
|
|
56
|
-
commandCounts.
|
|
93
|
+
const signatureKey = inspection.fileKey + '\0' + inspection.signature;
|
|
94
|
+
const signatureCount = (commandCounts.get(signatureKey) || 0) + 1;
|
|
95
|
+
commandCounts.set(signatureKey, signatureCount);
|
|
57
96
|
let overlapRatio = 0;
|
|
58
97
|
let redundant = signatureCount > 1;
|
|
59
98
|
|
|
@@ -85,6 +124,7 @@ function createReadProgressGuard(options = {}) {
|
|
|
85
124
|
});
|
|
86
125
|
|
|
87
126
|
if (steerIssued) {
|
|
127
|
+
if (!steerAcknowledged) return noAction(evidence);
|
|
88
128
|
postSteerRedundantStreak = redundant ? postSteerRedundantStreak + 1 : 0;
|
|
89
129
|
if (postSteerRedundantStreak >= settings.postSteerRedundantStreak) {
|
|
90
130
|
abortIssued = true;
|
|
@@ -101,12 +141,20 @@ function createReadProgressGuard(options = {}) {
|
|
|
101
141
|
return noAction(evidence);
|
|
102
142
|
},
|
|
103
143
|
|
|
144
|
+
acknowledgeSteer() {
|
|
145
|
+
if (steerIssued && !abortIssued) {
|
|
146
|
+
steerAcknowledged = true;
|
|
147
|
+
postSteerRedundantStreak = 0;
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
|
|
104
151
|
snapshot() {
|
|
105
152
|
return {
|
|
106
153
|
readCount,
|
|
107
154
|
redundantStreak,
|
|
108
155
|
postSteerRedundantStreak,
|
|
109
156
|
steerIssued,
|
|
157
|
+
steerAcknowledged,
|
|
110
158
|
abortIssued
|
|
111
159
|
};
|
|
112
160
|
}
|
|
@@ -120,9 +168,15 @@ function createReadProgressController({ input = {}, request, fail, getTurn, emit
|
|
|
120
168
|
});
|
|
121
169
|
let pendingSteer = null;
|
|
122
170
|
let steerInFlight = false;
|
|
171
|
+
let generation = 0;
|
|
123
172
|
|
|
124
173
|
return {
|
|
125
174
|
observe(item) {
|
|
175
|
+
if (item?.type === 'fileChange') {
|
|
176
|
+
generation += 1;
|
|
177
|
+
pendingSteer = null;
|
|
178
|
+
steerInFlight = false;
|
|
179
|
+
}
|
|
126
180
|
const decision = guard.observe(item);
|
|
127
181
|
if (decision.action === 'steer') {
|
|
128
182
|
steer(decision);
|
|
@@ -153,6 +207,7 @@ function createReadProgressController({ input = {}, request, fail, getTurn, emit
|
|
|
153
207
|
return;
|
|
154
208
|
}
|
|
155
209
|
steerInFlight = true;
|
|
210
|
+
const steerGeneration = generation;
|
|
156
211
|
emitEvent('codex.no_progress.steered', 'Repeated file reads detected; asking Codex to synthesize', {
|
|
157
212
|
...decision.evidence,
|
|
158
213
|
guardAction: 'steer'
|
|
@@ -165,7 +220,10 @@ function createReadProgressController({ input = {}, request, fail, getTurn, emit
|
|
|
165
220
|
text: buildReadProgressSteerText(decision.evidence, input.mode),
|
|
166
221
|
text_elements: []
|
|
167
222
|
}]
|
|
223
|
+
}).then(() => {
|
|
224
|
+
if (steerGeneration === generation) guard.acknowledgeSteer();
|
|
168
225
|
}).catch(error => {
|
|
226
|
+
if (steerGeneration !== generation) return;
|
|
169
227
|
const progressError = createNoProgressError(decision.evidence);
|
|
170
228
|
progressError.message += ` Automatic steering failed: ${String(error?.message || error).slice(0, 500)}`;
|
|
171
229
|
fail(progressError);
|
|
@@ -175,30 +233,35 @@ function createReadProgressController({ input = {}, request, fail, getTurn, emit
|
|
|
175
233
|
|
|
176
234
|
function extractReadInspection(item = {}, workspacePath = '') {
|
|
177
235
|
if (item.type !== 'commandExecution') return null;
|
|
236
|
+
if (item.status && item.status !== 'completed') return null;
|
|
237
|
+
if (Number.isInteger(item.exitCode) && item.exitCode !== 0) return null;
|
|
178
238
|
const command = String(item.command || '').trim();
|
|
179
239
|
if (!command) return null;
|
|
240
|
+
const cwd = typeof item.cwd === 'string' && item.cwd ? path.resolve(workspacePath || '.', item.cwd) : workspacePath;
|
|
180
241
|
const actions = Array.isArray(item.commandActions) ? item.commandActions : [];
|
|
181
|
-
const
|
|
182
|
-
if (!
|
|
183
|
-
|
|
242
|
+
const readActions = actions.filter(action => action?.type === 'read');
|
|
243
|
+
if (!readActions.length && !looksLikeReadCommand(command)) return null;
|
|
244
|
+
const paths = [...new Set(readActions.map(action => normalizeFileKey(action.path, cwd)).filter(Boolean))];
|
|
245
|
+
if (paths.length > 1) return null;
|
|
184
246
|
const bounded = extractBoundedRange(command);
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
? { startLine: bounded.startLine, endLine: bounded.endLine }
|
|
190
|
-
: null;
|
|
247
|
+
const boundedKey = normalizeFileKey(bounded?.path, cwd);
|
|
248
|
+
if (boundedKey && paths[0] && boundedKey !== paths[0]) return null;
|
|
249
|
+
const fileKey = paths[0] || boundedKey;
|
|
250
|
+
if (!fileKey) return null;
|
|
191
251
|
return {
|
|
192
|
-
fileKey
|
|
193
|
-
displayPath,
|
|
194
|
-
range
|
|
252
|
+
fileKey,
|
|
253
|
+
displayPath: safeDisplayPath(fileKey, workspacePath) || path.basename(fileKey),
|
|
254
|
+
range: bounded && boundedKey === fileKey
|
|
255
|
+
? { startLine: bounded.startLine, endLine: bounded.endLine } : null,
|
|
195
256
|
signature: normalizeCommandSignature(command, workspacePath)
|
|
196
257
|
};
|
|
197
258
|
}
|
|
198
259
|
|
|
199
260
|
function extractBoundedRange(command) {
|
|
200
261
|
const source = String(command || '');
|
|
201
|
-
|
|
262
|
+
// Stream pagination and compound commands do not prove source-file ranges.
|
|
263
|
+
if (/[|;&<>\x60]/.test(source)) return null;
|
|
264
|
+
const sedMatch = source.match(/\bsed\s+(?:-[A-Za-z]+\s+)*["']?(\d+)\s*,\s*(\d+)p["']?\s+((?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s;&|]+))/i);
|
|
202
265
|
if (sedMatch) {
|
|
203
266
|
return normalizeRangeMatch(sedMatch[1], sedMatch[2], sedMatch[3]);
|
|
204
267
|
}
|
|
@@ -212,13 +275,15 @@ function extractBoundedRange(command) {
|
|
|
212
275
|
function normalizeRangeMatch(startValue, endValue, pathValue) {
|
|
213
276
|
const startLine = Number(startValue);
|
|
214
277
|
const endLine = Number(endValue);
|
|
278
|
+
const filePath = unquoteShellWord(pathValue);
|
|
279
|
+
if (!filePath || filePath === '-') return null;
|
|
215
280
|
if (!Number.isSafeInteger(startLine) || !Number.isSafeInteger(endLine) || startLine < 1 || endLine < startLine) {
|
|
216
281
|
return null;
|
|
217
282
|
}
|
|
218
283
|
return {
|
|
219
284
|
startLine,
|
|
220
285
|
endLine,
|
|
221
|
-
path:
|
|
286
|
+
path: filePath
|
|
222
287
|
};
|
|
223
288
|
}
|
|
224
289
|
|
|
@@ -228,8 +293,9 @@ function looksLikeReadCommand(command) {
|
|
|
228
293
|
}
|
|
229
294
|
|
|
230
295
|
function normalizeFileKey(value, workspacePath) {
|
|
296
|
+
if (typeof value !== 'string') return '';
|
|
231
297
|
const source = unquoteShellWord(value);
|
|
232
|
-
if (!source) return '';
|
|
298
|
+
if (!source || source === '-' || source.startsWith('~') || /[$\x60*?\[\]{}]/.test(source)) return '';
|
|
233
299
|
if (path.isAbsolute(source)) return path.normalize(source);
|
|
234
300
|
return path.resolve(workspacePath || '.', source);
|
|
235
301
|
}
|
|
@@ -355,6 +421,7 @@ module.exports = {
|
|
|
355
421
|
createNoProgressError,
|
|
356
422
|
createReadProgressController,
|
|
357
423
|
createReadProgressGuard,
|
|
424
|
+
createRunEventScope,
|
|
358
425
|
extractBoundedRange,
|
|
359
426
|
extractReadInspection
|
|
360
427
|
};
|