codex-overleaf-link 2.3.0 → 2.3.2
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 +23 -23
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/src/background.js +20 -0
- package/extension/src/content/assetTransferBroker.js +169 -0
- package/extension/src/content/composerPanel.js +0 -2
- package/extension/src/content/contentRuntime.js +135 -111
- package/extension/src/content/generated/content.bundle.js +1066 -345
- package/extension/src/content/generated/content.bundle.meta.json +7 -3
- package/extension/src/content/legacyGlobalRegistry.js +3 -0
- package/extension/src/content/markdownText.js +12 -10
- package/extension/src/content/mathText.js +155 -9
- package/extension/src/content/modelPicker.js +2 -2
- package/extension/src/content/modelPickerSupport.js +3 -3
- package/extension/src/content/nativeCompatibilityController.js +2 -2
- package/extension/src/content/otWarmMirror.js +1 -1
- package/extension/src/content/pageBridgeClient.js +1 -0
- package/extension/src/content/runResultActions.js +114 -0
- package/extension/src/content/runTimelineView.js +23 -0
- package/extension/src/content/sessionForkController.js +120 -0
- package/extension/src/content/writebackController.js +3 -0
- package/extension/src/content/writebackOrchestrator.js +5 -50
- package/extension/src/page/binaryAssetUploader.js +343 -0
- package/extension/src/page/trackedChangesLifecycle.js +12 -4
- package/extension/src/page/writebackRouter.js +6 -2
- package/extension/src/pageBridge.js +30 -1
- package/extension/src/shared/agentTranscript.js +10 -3
- package/extension/src/shared/compatibility.js +4 -2
- package/extension/src/shared/i18n.js +18 -4
- package/extension/src/shared/models.js +13 -2
- package/extension/src/shared/pageRpcContract.js +6 -2
- package/extension/src/shared/runExecutionSnapshot.js +14 -4
- package/extension/src/shared/runExecutionSnapshotCodec.js +5 -2
- package/extension/src/shared/scopedPersistenceQueuePolicy.js +20 -1
- package/extension/src/shared/scopedPersistenceReceipt.js +60 -0
- package/extension/src/shared/scopedPersistenceTransaction.js +50 -47
- package/extension/src/shared/sessionState.js +58 -15
- package/extension/src/shared/storageDb.js +6 -5
- package/extension/src/shared/storageRunActions.js +11 -1
- package/extension/src/shared/writebackOperationEvidence.js +16 -6
- package/extension/src/shared/writebackSettlement.js +28 -6
- package/extension/styles/panel.css +79 -8
- package/native-host/src/codexModels.js +6 -2
- package/native-host/src/codexPromptAssembly.js +1 -1
- package/native-host/src/codexSessionRunner.js +5 -1
- package/native-host/src/codexThreadFork.js +103 -0
- package/native-host/src/mirrorWorkspace.js +5 -19
- package/native-host/src/nativeAssetTransfer.js +183 -0
- package/native-host/src/taskRunnerRuntime.js +33 -81
- package/native-host/src/updateManager.js +36 -1
- package/package.json +1 -1
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { getProjectMirror } = require('./mirrorWorkspace');
|
|
7
|
+
|
|
8
|
+
const MAX_ASSET_BYTES = 10 * 1024 * 1024;
|
|
9
|
+
const ASSET_CHUNK_BYTES = 192 * 1024;
|
|
10
|
+
const ASSET_TOKEN_TTL_MS = 15 * 60 * 1000;
|
|
11
|
+
const transfers = new Map();
|
|
12
|
+
|
|
13
|
+
function prepareBinaryAssetChanges({ projectId, rootDir, changes = [] } = {}) {
|
|
14
|
+
const prepared = [];
|
|
15
|
+
const unsupportedChanges = [];
|
|
16
|
+
for (const change of changes) {
|
|
17
|
+
if (change?.type !== 'binary-create' && change?.type !== 'overwrite-binary') {
|
|
18
|
+
prepared.push(change);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
prepared.push(registerAssetChange({ projectId, rootDir, change }));
|
|
23
|
+
} catch (error) {
|
|
24
|
+
unsupportedChanges.push({
|
|
25
|
+
type: 'unsupported-local-file',
|
|
26
|
+
path: change?.path || '',
|
|
27
|
+
reason: error.code || 'binary_asset_registration_failed',
|
|
28
|
+
size: change?.size,
|
|
29
|
+
attemptedChangeType: change?.type || 'binary-create'
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { changes: prepared, unsupportedChanges };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function registerAssetChange({ projectId, rootDir, change }) {
|
|
37
|
+
cleanupExpiredTransfers();
|
|
38
|
+
const mirror = getProjectMirror(projectId, { rootDir });
|
|
39
|
+
const relativePath = normalizeProjectPath(change.assetSourcePath || change.path);
|
|
40
|
+
const target = resolveSafeWorkspaceFile(mirror.workspacePath, relativePath);
|
|
41
|
+
const stat = fs.statSync(target);
|
|
42
|
+
if (!stat.isFile()) throw codedError('binary_asset_source_invalid', 'Asset source is not a file.');
|
|
43
|
+
if (stat.size > MAX_ASSET_BYTES) throw codedError('binary_file_too_large', 'Asset exceeds the 10 MiB writeback limit.');
|
|
44
|
+
const sha256 = hashFile(target);
|
|
45
|
+
if (change.sha256 && change.sha256 !== sha256) {
|
|
46
|
+
throw codedError('binary_asset_source_changed', 'Asset changed before transfer registration.');
|
|
47
|
+
}
|
|
48
|
+
const token = crypto.randomUUID();
|
|
49
|
+
const expiresAtMs = Date.now() + ASSET_TOKEN_TTL_MS;
|
|
50
|
+
transfers.set(token, {
|
|
51
|
+
token,
|
|
52
|
+
path: relativePath,
|
|
53
|
+
target,
|
|
54
|
+
size: stat.size,
|
|
55
|
+
sha256,
|
|
56
|
+
expiresAtMs
|
|
57
|
+
});
|
|
58
|
+
const { contentBase64, assetSourcePath, ...safeChange } = change;
|
|
59
|
+
return {
|
|
60
|
+
...safeChange,
|
|
61
|
+
size: stat.size,
|
|
62
|
+
assetRef: {
|
|
63
|
+
token,
|
|
64
|
+
path: relativePath,
|
|
65
|
+
size: stat.size,
|
|
66
|
+
sha256,
|
|
67
|
+
mimeType: inferMimeType(relativePath),
|
|
68
|
+
chunkSize: ASSET_CHUNK_BYTES,
|
|
69
|
+
expiresAt: new Date(expiresAtMs).toISOString()
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readAssetChunk(params = {}) {
|
|
75
|
+
cleanupExpiredTransfers();
|
|
76
|
+
const transfer = getTransfer(params.token);
|
|
77
|
+
const offset = Number(params.offset || 0);
|
|
78
|
+
const requestedLength = Number(params.length || ASSET_CHUNK_BYTES);
|
|
79
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || offset > transfer.size) {
|
|
80
|
+
throw codedError('asset_chunk_offset_invalid', 'Asset chunk offset is invalid.');
|
|
81
|
+
}
|
|
82
|
+
const length = Math.min(
|
|
83
|
+
ASSET_CHUNK_BYTES,
|
|
84
|
+
Number.isSafeInteger(requestedLength) && requestedLength > 0 ? requestedLength : ASSET_CHUNK_BYTES,
|
|
85
|
+
transfer.size - offset
|
|
86
|
+
);
|
|
87
|
+
assertSourceStable(transfer);
|
|
88
|
+
const handle = fs.openSync(transfer.target, 'r');
|
|
89
|
+
try {
|
|
90
|
+
const buffer = Buffer.alloc(length);
|
|
91
|
+
const bytesRead = length ? fs.readSync(handle, buffer, 0, length, offset) : 0;
|
|
92
|
+
const nextOffset = offset + bytesRead;
|
|
93
|
+
return {
|
|
94
|
+
token: transfer.token,
|
|
95
|
+
path: transfer.path,
|
|
96
|
+
offset,
|
|
97
|
+
nextOffset,
|
|
98
|
+
size: transfer.size,
|
|
99
|
+
sha256: transfer.sha256,
|
|
100
|
+
contentBase64: buffer.subarray(0, bytesRead).toString('base64'),
|
|
101
|
+
eof: nextOffset >= transfer.size
|
|
102
|
+
};
|
|
103
|
+
} finally {
|
|
104
|
+
fs.closeSync(handle);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function releaseAsset(params = {}) {
|
|
109
|
+
return { released: transfers.delete(String(params.token || '')) };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getTransfer(tokenValue) {
|
|
113
|
+
const token = String(tokenValue || '');
|
|
114
|
+
const transfer = transfers.get(token);
|
|
115
|
+
if (!transfer) throw codedError('asset_token_invalid', 'Asset transfer token is invalid or expired.');
|
|
116
|
+
if (transfer.expiresAtMs <= Date.now()) {
|
|
117
|
+
transfers.delete(token);
|
|
118
|
+
throw codedError('asset_token_expired', 'Asset transfer token expired.');
|
|
119
|
+
}
|
|
120
|
+
return transfer;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function assertSourceStable(transfer) {
|
|
124
|
+
let stat;
|
|
125
|
+
try { stat = fs.statSync(transfer.target); } catch (_error) {
|
|
126
|
+
throw codedError('binary_asset_source_missing', 'Asset source disappeared during transfer.');
|
|
127
|
+
}
|
|
128
|
+
if (!stat.isFile() || stat.size !== transfer.size) {
|
|
129
|
+
throw codedError('binary_asset_source_changed', 'Asset source changed during transfer.');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function resolveSafeWorkspaceFile(workspacePath, relativePath) {
|
|
134
|
+
const root = path.resolve(workspacePath);
|
|
135
|
+
const target = path.resolve(root, relativePath);
|
|
136
|
+
if (target === root || !target.startsWith(`${root}${path.sep}`)) {
|
|
137
|
+
throw codedError('binary_asset_path_unsafe', 'Asset path escapes the project workspace.');
|
|
138
|
+
}
|
|
139
|
+
if (fs.lstatSync(target).isSymbolicLink()) {
|
|
140
|
+
throw codedError('binary_asset_path_unsafe', 'Symbolic-link assets cannot be transferred.');
|
|
141
|
+
}
|
|
142
|
+
return target;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function normalizeProjectPath(value) {
|
|
146
|
+
const normalized = String(value || '').replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
|
147
|
+
if (!normalized || normalized.split('/').some(part => !part || part === '.' || part === '..')) {
|
|
148
|
+
throw codedError('binary_asset_path_invalid', 'Asset path is not a safe project-relative path.');
|
|
149
|
+
}
|
|
150
|
+
return normalized;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function hashFile(target) {
|
|
154
|
+
return crypto.createHash('sha256').update(fs.readFileSync(target)).digest('hex');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function inferMimeType(filePath) {
|
|
158
|
+
return {
|
|
159
|
+
'.pdf': 'application/pdf', '.png': 'image/png', '.jpg': 'image/jpeg',
|
|
160
|
+
'.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml',
|
|
161
|
+
'.webp': 'image/webp', '.eps': 'application/postscript'
|
|
162
|
+
}[path.extname(filePath).toLowerCase()] || 'application/octet-stream';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function cleanupExpiredTransfers() {
|
|
166
|
+
const now = Date.now();
|
|
167
|
+
for (const [token, transfer] of transfers) if (transfer.expiresAtMs <= now) transfers.delete(token);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function codedError(code, message) {
|
|
171
|
+
const error = new Error(message);
|
|
172
|
+
error.code = code;
|
|
173
|
+
return error;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = {
|
|
177
|
+
ASSET_CHUNK_BYTES,
|
|
178
|
+
ASSET_TOKEN_TTL_MS,
|
|
179
|
+
MAX_ASSET_BYTES,
|
|
180
|
+
prepareBinaryAssetChanges,
|
|
181
|
+
readAssetChunk,
|
|
182
|
+
releaseAsset
|
|
183
|
+
};
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
const { buildCodexRuntimeEvent } = require('./codexRuntimeIdentity');
|
|
4
4
|
|
|
5
5
|
const { spawn } = require('node:child_process');
|
|
6
|
-
const crypto = require('node:crypto');
|
|
7
6
|
const { buildOperationSummary, splitDeletePlan } = require('../../extension/src/shared/summary');
|
|
8
7
|
const {
|
|
9
8
|
MIN_COMPATIBLE_EXTENSION_VERSION,
|
|
@@ -20,6 +19,8 @@ const {
|
|
|
20
19
|
resolveRunProviderForRun
|
|
21
20
|
} = require('./providerRuntime');
|
|
22
21
|
const { clearPluginCodexHistory } = require('./codexHome');
|
|
22
|
+
const { forkCodexThread } = require('./codexThreadFork');
|
|
23
|
+
const { readAssetChunk, releaseAsset } = require('./nativeAssetTransfer');
|
|
23
24
|
const { logDebug, truncateText } = require('./debugLog');
|
|
24
25
|
const { HOST_NAME } = require('./manifest');
|
|
25
26
|
const { getNativeRuntimePlatform, summarizeNativeEnvironment } = require('./nativeEnvironment');
|
|
@@ -40,8 +41,6 @@ const activeRunEntries = new Map();
|
|
|
40
41
|
// Overleaf tab was reloaded — the requestId lived in content-side JS state
|
|
41
42
|
// and is gone, but the native-host-side controller is still running).
|
|
42
43
|
const activeRunByProject = new Map();
|
|
43
|
-
const pendingPlans = new Map();
|
|
44
|
-
const PENDING_PLAN_TTL_MS = 30 * 60 * 1000;
|
|
45
44
|
const CODEX_RUN_PASSTHROUGH_ERROR_CODES = new Set(['thread_resume_failed', 'codex_no_usable_result']);
|
|
46
45
|
|
|
47
46
|
async function handleRequest(request, env = process.env, emit = () => {}) {
|
|
@@ -119,6 +118,26 @@ async function handleRequest(request, env = process.env, emit = () => {}) {
|
|
|
119
118
|
return handleCodexHistoryClear(request, env);
|
|
120
119
|
}
|
|
121
120
|
|
|
121
|
+
if (request.method === 'codex.thread.fork') {
|
|
122
|
+
try {
|
|
123
|
+
return okResponse(request.id, await forkCodexThread(request.params || {}, env));
|
|
124
|
+
} catch (error) {
|
|
125
|
+
return errorResponse(request.id, error.code || 'codex_thread_fork_failed', error.message);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (request.method === 'asset.readChunk') {
|
|
130
|
+
try {
|
|
131
|
+
return okResponse(request.id, readAssetChunk(request.params || {}));
|
|
132
|
+
} catch (error) {
|
|
133
|
+
return errorResponse(request.id, error.code || 'asset_chunk_read_failed', error.message);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (request.method === 'asset.release') {
|
|
138
|
+
return okResponse(request.id, releaseAsset(request.params || {}));
|
|
139
|
+
}
|
|
140
|
+
|
|
122
141
|
if (request.method === 'skills.list') {
|
|
123
142
|
return handleSkillsList(request, env);
|
|
124
143
|
}
|
|
@@ -136,7 +155,7 @@ async function handleRequest(request, env = process.env, emit = () => {}) {
|
|
|
136
155
|
}
|
|
137
156
|
|
|
138
157
|
if (request.method === 'task.confirm') {
|
|
139
|
-
return
|
|
158
|
+
return errorResponse(request.id, 'suggest_mode_removed', 'Suggest mode has been removed. Reload the extension and choose Ask or Auto.');
|
|
140
159
|
}
|
|
141
160
|
|
|
142
161
|
return errorResponse(request.id, 'method_not_found', `Unknown method: ${request.method}`);
|
|
@@ -716,8 +735,11 @@ async function handleTaskRun(request, env, emit) {
|
|
|
716
735
|
const params = request.params || {};
|
|
717
736
|
const mode = params.mode;
|
|
718
737
|
|
|
719
|
-
if (
|
|
720
|
-
return errorResponse(request.id, '
|
|
738
|
+
if (mode === 'confirm') {
|
|
739
|
+
return errorResponse(request.id, 'suggest_mode_removed', 'Suggest mode has been removed. Choose Ask or Auto.');
|
|
740
|
+
}
|
|
741
|
+
if (!['ask', 'auto'].includes(mode)) {
|
|
742
|
+
return errorResponse(request.id, 'invalid_mode', 'Mode must be "ask" or "auto"');
|
|
721
743
|
}
|
|
722
744
|
|
|
723
745
|
if (mode === 'auto' && !params.checkpoint?.ok && !isVerifiedReviewing(params.reviewing)) {
|
|
@@ -805,24 +827,6 @@ function isVerifiedReviewing(reviewing) {
|
|
|
805
827
|
return reviewing?.ok === true && reviewing.status !== 'manual-override';
|
|
806
828
|
}
|
|
807
829
|
|
|
808
|
-
async function handleTaskConfirm(request) {
|
|
809
|
-
const planId = request.params?.planId;
|
|
810
|
-
purgeExpiredPendingPlans();
|
|
811
|
-
if (!planId || !pendingPlans.has(planId)) {
|
|
812
|
-
return errorResponse(request.id, 'plan_not_found', 'No pending task plan matched the supplied planId');
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
const plan = pendingPlans.get(planId);
|
|
816
|
-
pendingPlans.delete(planId);
|
|
817
|
-
|
|
818
|
-
return okResponse(request.id, {
|
|
819
|
-
status: 'confirmed',
|
|
820
|
-
notes: plan.notes || '',
|
|
821
|
-
userReport: plan.userReport,
|
|
822
|
-
operations: plan.operations
|
|
823
|
-
});
|
|
824
|
-
}
|
|
825
|
-
|
|
826
830
|
function buildDefaultTaskResult(mode, operations, params = {}) {
|
|
827
831
|
if (mode === 'ask') {
|
|
828
832
|
return {
|
|
@@ -836,16 +840,6 @@ function buildDefaultTaskResult(mode, operations, params = {}) {
|
|
|
836
840
|
|
|
837
841
|
const summary = buildOperationSummary(operations);
|
|
838
842
|
|
|
839
|
-
if (mode === 'confirm') {
|
|
840
|
-
return {
|
|
841
|
-
status: 'requires_task_confirmation',
|
|
842
|
-
summary,
|
|
843
|
-
notes: '',
|
|
844
|
-
userReport: buildDefaultUserReport(mode, operations, params),
|
|
845
|
-
operations
|
|
846
|
-
};
|
|
847
|
-
}
|
|
848
|
-
|
|
849
843
|
const split = splitDeletePlan(operations);
|
|
850
844
|
if (split.needsConfirmation.length > 0) {
|
|
851
845
|
return {
|
|
@@ -889,7 +883,7 @@ function normalizeAgentResult(mode, result, params = {}) {
|
|
|
889
883
|
};
|
|
890
884
|
|
|
891
885
|
if (!normalized.status) {
|
|
892
|
-
normalized.status =
|
|
886
|
+
normalized.status = 'completed';
|
|
893
887
|
}
|
|
894
888
|
|
|
895
889
|
return normalized;
|
|
@@ -911,40 +905,7 @@ function prepareResultForResponse(mode, result) {
|
|
|
911
905
|
}
|
|
912
906
|
}
|
|
913
907
|
|
|
914
|
-
|
|
915
|
-
if (confirmOperations.length > 0) {
|
|
916
|
-
result = {
|
|
917
|
-
...result,
|
|
918
|
-
status: 'requires_task_confirmation',
|
|
919
|
-
summary: buildOperationSummary(confirmOperations),
|
|
920
|
-
operations: confirmOperations
|
|
921
|
-
};
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
if (mode !== 'confirm' || result.status !== 'requires_task_confirmation') {
|
|
925
|
-
return result;
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
purgeExpiredPendingPlans();
|
|
929
|
-
const planId = `plan_${crypto.randomUUID()}`;
|
|
930
|
-
pendingPlans.set(planId, {
|
|
931
|
-
createdAt: Date.now(),
|
|
932
|
-
expiresAt: Date.now() + PENDING_PLAN_TTL_MS,
|
|
933
|
-
notes: result.notes || '',
|
|
934
|
-
userReport: result.userReport,
|
|
935
|
-
operations: Array.isArray(result.operations) ? result.operations : []
|
|
936
|
-
});
|
|
937
|
-
|
|
938
|
-
const {
|
|
939
|
-
operations: _operations,
|
|
940
|
-
pendingOperations: _pendingOperations,
|
|
941
|
-
deletePlan: _deletePlan,
|
|
942
|
-
...redacted
|
|
943
|
-
} = result;
|
|
944
|
-
return {
|
|
945
|
-
...redacted,
|
|
946
|
-
planId
|
|
947
|
-
};
|
|
908
|
+
return result;
|
|
948
909
|
}
|
|
949
910
|
|
|
950
911
|
function validateTaskResultOperationQuotas(result = {}) {
|
|
@@ -965,7 +926,7 @@ function buildDefaultUserReport(mode, operations = [], params = {}) {
|
|
|
965
926
|
|
|
966
927
|
return {
|
|
967
928
|
conclusion: hasOperations
|
|
968
|
-
? reportText(locale, 'Codex prepared
|
|
929
|
+
? reportText(locale, 'Codex prepared changes for writeback.', 'Codex 已准备好写回修改。')
|
|
969
930
|
: reportText(locale, 'This task completed without writing Overleaf files.', '这轮任务已完成,没有写入 Overleaf 文件。'),
|
|
970
931
|
checked,
|
|
971
932
|
findings: [],
|
|
@@ -975,7 +936,7 @@ function buildDefaultUserReport(mode, operations = [], params = {}) {
|
|
|
975
936
|
? ''
|
|
976
937
|
: (mode === 'ask' ? reportText(locale, 'This run was Ask mode.', '这轮是只问不改。') : ''),
|
|
977
938
|
nextStep: hasOperations
|
|
978
|
-
? reportText(locale, 'Review
|
|
939
|
+
? reportText(locale, 'Review the written changes in Overleaf.', '请在 Overleaf 中检查写入的修改。')
|
|
979
940
|
: reportText(locale, 'Continue the conversation, or add more @context and run another check.', '可以继续追问,或加入更多 @context 后再检查。')
|
|
980
941
|
};
|
|
981
942
|
}
|
|
@@ -1193,14 +1154,6 @@ function runExternalAgent(agentSpec, params, emit = () => {}, options = {}) {
|
|
|
1193
1154
|
});
|
|
1194
1155
|
}
|
|
1195
1156
|
|
|
1196
|
-
function purgeExpiredPendingPlans(now = Date.now()) {
|
|
1197
|
-
for (const [planId, plan] of pendingPlans.entries()) {
|
|
1198
|
-
if (Number.isFinite(plan?.expiresAt) && plan.expiresAt <= now) {
|
|
1199
|
-
pendingPlans.delete(planId);
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
1157
|
function parsePositiveInteger(value, fallback) {
|
|
1205
1158
|
const parsed = Number.parseInt(value, 10);
|
|
1206
1159
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
@@ -1295,6 +1248,5 @@ module.exports = {
|
|
|
1295
1248
|
buildDefaultTaskResult,
|
|
1296
1249
|
getActiveNativeWorkState,
|
|
1297
1250
|
handleRequest,
|
|
1298
|
-
parseAgentEventLines
|
|
1299
|
-
purgeExpiredPendingPlans
|
|
1251
|
+
parseAgentEventLines
|
|
1300
1252
|
};
|
|
@@ -743,6 +743,8 @@ function cleanupOrphanStageRoots(updatesRoot, retainedRoots = []) {
|
|
|
743
743
|
|
|
744
744
|
function recoverAndReadStatus(context) {
|
|
745
745
|
let journal = readJournal(context);
|
|
746
|
+
const alignedVersion = readAlignedManagedVersion(context);
|
|
747
|
+
journal = supersedeObsoleteJournal(context, journal, alignedVersion);
|
|
746
748
|
if (journal?.state === 'activation_pending') {
|
|
747
749
|
journal = { ...journal, state: 'staged', reasonCode: 'update_activation_interrupted', updatedAt: new Date().toISOString() };
|
|
748
750
|
writeJournal(context, journal);
|
|
@@ -753,7 +755,7 @@ function recoverAndReadStatus(context) {
|
|
|
753
755
|
writeJournal(context, journal);
|
|
754
756
|
settleAuthorization(context, journal.authorizationId, 'revoked');
|
|
755
757
|
}
|
|
756
|
-
if (journal?.state === 'rolled_back') {
|
|
758
|
+
if (journal?.state === 'rolled_back' || journal?.state === 'superseded') {
|
|
757
759
|
pruneNativeVersions(context.nativeRoot, new Set([
|
|
758
760
|
readVersionPointer(context.nativeRoot, 'active-version'),
|
|
759
761
|
readVersionPointer(context.nativeRoot, 'previous-version')
|
|
@@ -768,6 +770,36 @@ function recoverAndReadStatus(context) {
|
|
|
768
770
|
};
|
|
769
771
|
}
|
|
770
772
|
|
|
773
|
+
function readAlignedManagedVersion(context) {
|
|
774
|
+
try {
|
|
775
|
+
return assertManagedLayout(context);
|
|
776
|
+
} catch (_error) {
|
|
777
|
+
return '';
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function supersedeObsoleteJournal(context, journal, activeVersion) {
|
|
782
|
+
if (!journal || journal.state === 'superseded' || !parseSemver(activeVersion) ||
|
|
783
|
+
!parseSemver(journal.sourceVersion) || !parseSemver(journal.targetVersion) ||
|
|
784
|
+
compareSemver(activeVersion, journal.sourceVersion) <= 0 ||
|
|
785
|
+
compareSemver(activeVersion, journal.targetVersion) <= 0) {
|
|
786
|
+
return journal;
|
|
787
|
+
}
|
|
788
|
+
const timestamp = new Date().toISOString();
|
|
789
|
+
const superseded = {
|
|
790
|
+
...journal,
|
|
791
|
+
state: 'superseded',
|
|
792
|
+
supersededFromState: journal.state,
|
|
793
|
+
supersededByVersion: activeVersion,
|
|
794
|
+
supersededAt: timestamp,
|
|
795
|
+
updatedAt: timestamp
|
|
796
|
+
};
|
|
797
|
+
writeJournal(context, superseded);
|
|
798
|
+
cleanupStage(journal.stageRoot);
|
|
799
|
+
cleanupOrphanStageRoots(context.updatesRoot);
|
|
800
|
+
return superseded;
|
|
801
|
+
}
|
|
802
|
+
|
|
771
803
|
function pruneNativeVersions(nativeRoot, retainedVersions) {
|
|
772
804
|
const versionsRoot = path.join(nativeRoot, 'versions');
|
|
773
805
|
let entries = [];
|
|
@@ -960,6 +992,9 @@ function publicTransaction(journal) {
|
|
|
960
992
|
appliedAt: journal.appliedAt,
|
|
961
993
|
confirmedAt: journal.confirmedAt,
|
|
962
994
|
rolledBackAt: journal.rolledBackAt,
|
|
995
|
+
supersededAt: journal.supersededAt,
|
|
996
|
+
supersededByVersion: journal.supersededByVersion,
|
|
997
|
+
supersededFromState: journal.supersededFromState,
|
|
963
998
|
reasonCode: journal.reasonCode || ''
|
|
964
999
|
};
|
|
965
1000
|
}
|