@walkhi/code-relax 0.1.0-beta.30 → 0.1.0-beta.31
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 +95 -89
- package/dist/bin/cleanup-accounts.mjs +63 -63
- package/dist/bin/codex-remote.mjs +374 -367
- package/dist/bin/manage-relay-admin.mjs +20 -20
- package/dist/bin/migrate-relay-db.mjs +109 -109
- package/dist/bin/publish-android-release.mjs +62 -58
- package/dist/bin/self-relay-admin.mjs +8 -8
- package/dist/bin/self-relay-demo.mjs +33 -33
- package/dist/bin/self-relay-server.mjs +66 -66
- package/dist/build-platform.json +1 -1
- package/dist/get-lan-networks.ps1 +40 -40
- package/dist/get-shared-service-url.ps1 +6 -6
- package/dist/shared/app-server-events.cjs +422 -412
- package/dist/shared/code-browser.cjs +159 -159
- package/dist/shared/file-changes.cjs +31 -31
- package/dist/shared/file-citations.cjs +117 -117
- package/dist/shared/file-mentions.cjs +92 -92
- package/dist/shared/message-delivery.cjs +54 -54
- package/dist/shared/p2p-data-channel.cjs +72 -72
- package/dist/shared/p2p-diagnostics.cjs +28 -28
- package/dist/shared/p2p-file-channel.cjs +74 -74
- package/dist/shared/plan-pricing.cjs +32 -32
- package/dist/shared/question-attention.cjs +40 -0
- package/dist/shared/server-requests.cjs +219 -219
- package/dist/shared/timeline-sync.cjs +173 -173
- package/dist/skills/code-relax/SKILL.md +22 -22
- package/dist/src/archived-threads.mjs +22 -0
- package/dist/src/background-process.mjs +18 -18
- package/dist/src/bounded-process-log.mjs +63 -63
- package/dist/src/celsius-client.mjs +107 -107
- package/dist/src/code-browser.mjs +100 -100
- package/dist/src/desktop-launcher.mjs +46 -46
- package/dist/src/dots-approval-timeline.mjs +99 -99
- package/dist/src/dots-avatar.mjs +42 -42
- package/dist/src/dots-chat-projection.mjs +50 -50
- package/dist/src/dots-chat-server.mjs +461 -461
- package/dist/src/dots-message-images.mjs +319 -319
- package/dist/src/dots-profile.mjs +77 -77
- package/dist/src/dots-room-activity.mjs +94 -94
- package/dist/src/dots-room-client.mjs +135 -135
- package/dist/src/dots-room-projection.mjs +94 -94
- package/dist/src/dots-schedules.mjs +66 -66
- package/dist/src/dots-summary-tasks.mjs +32 -32
- package/dist/src/dots-task-catalog.mjs +430 -430
- package/dist/src/dots-task-files.mjs +52 -52
- package/dist/src/execution-health.mjs +26 -26
- package/dist/src/file-transfer-range.mjs +22 -22
- package/dist/src/file-upload-session.mjs +130 -130
- package/dist/src/hosted-history-app-server.mjs +103 -103
- package/dist/src/hosted-history-cache.mjs +113 -113
- package/dist/src/lan-socket-server.mjs +265 -263
- package/dist/src/managed-app-server.mjs +244 -244
- package/dist/src/message-images.mjs +9 -9
- package/dist/src/message-upload-files.mjs +74 -74
- package/dist/src/onboarding.mjs +12 -12
- package/dist/src/plan-pricing.mjs +47 -47
- package/dist/src/platform/README.md +12 -12
- package/dist/src/platform/codex-protocol.mjs +46 -46
- package/dist/src/platform/codex-runtime.mjs +3 -3
- package/dist/src/platform/credential-store.mjs +3 -3
- package/dist/src/platform/desktop-host.mjs +3 -3
- package/dist/src/platform/desktop-project-registration.mjs +3 -3
- package/dist/src/platform/desktop-project-store.mjs +208 -149
- package/dist/src/platform/file-opener.mjs +9 -0
- package/dist/src/platform/linux/README.md +3 -3
- package/dist/src/platform/linux/codex-runtime.mjs +56 -0
- package/dist/src/platform/linux/credential-store.mjs +28 -0
- package/dist/src/platform/linux/desktop-host.mjs +83 -0
- package/dist/src/platform/linux/desktop-installation.mjs +36 -0
- package/dist/src/platform/linux/desktop-project-registration.mjs +11 -0
- package/dist/src/platform/linux/network-info.mjs +11 -0
- package/dist/src/platform/linux/process.mjs +42 -0
- package/dist/src/platform/linux/project-locations.mjs +17 -0
- package/dist/src/platform/linux/resident-startup.mjs +52 -0
- package/dist/src/platform/linux/retained-runtime.mjs +76 -0
- package/dist/src/platform/linux/service-host.mjs +18 -0
- package/dist/src/platform/linux/system.mjs +49 -0
- package/dist/src/platform/linux/user-environment.mjs +7 -0
- package/dist/src/platform/macos/README.md +3 -3
- package/dist/src/platform/macos/codex-runtime.mjs +125 -125
- package/dist/src/platform/macos/credential-store.mjs +10 -10
- package/dist/src/platform/macos/desktop-host.mjs +82 -82
- package/dist/src/platform/macos/desktop-project-registration.mjs +11 -10
- package/dist/src/platform/macos/network-info.mjs +8 -28
- package/dist/src/platform/macos/project-locations.mjs +8 -8
- package/dist/src/platform/macos/resident-startup.mjs +84 -84
- package/dist/src/platform/macos/retained-runtime.mjs +127 -127
- package/dist/src/platform/macos/service-host.mjs +18 -18
- package/dist/src/platform/macos/system.mjs +29 -29
- package/dist/src/platform/macos/user-environment.mjs +7 -7
- package/dist/src/platform/native-package.mjs +58 -46
- package/dist/src/platform/network-info.mjs +3 -3
- package/dist/src/platform/path-identity.mjs +5 -5
- package/dist/src/platform/posix/network-candidates.mjs +23 -0
- package/dist/src/platform/posix/runtime-paths.mjs +25 -25
- package/dist/src/platform/project-locations.mjs +3 -3
- package/dist/src/platform/runtime-paths.mjs +8 -8
- package/dist/src/platform/select.mjs +5 -5
- package/dist/src/platform/service-host.mjs +3 -3
- package/dist/src/platform/user-environment.mjs +3 -3
- package/dist/src/platform/windows/IsolatedProcess.cs +79 -79
- package/dist/src/platform/windows/activate-desktop.ps1 +48 -48
- package/dist/src/platform/windows/background-process.mjs +30 -30
- package/dist/src/platform/windows/codex-runtime.mjs +215 -215
- package/dist/src/platform/windows/credential-store.mjs +27 -27
- package/dist/src/platform/windows/credential-store.ps1 +49 -49
- package/dist/src/platform/windows/desktop-helper.mjs +179 -179
- package/dist/src/platform/windows/desktop-host.mjs +58 -58
- package/dist/src/platform/windows/desktop-info.ps1 +14 -14
- package/dist/src/platform/windows/desktop-project-registration.mjs +4 -4
- package/dist/src/platform/windows/desktop-runtime.ps1 +10 -10
- package/dist/src/platform/windows/desktop-shortcuts.ps1 +21 -21
- package/dist/src/platform/windows/dots-profile.mjs +49 -49
- package/dist/src/platform/windows/launch-worker.mjs +41 -41
- package/dist/src/platform/windows/network-info.mjs +34 -34
- package/dist/src/platform/windows/project-locations.mjs +16 -16
- package/dist/src/platform/windows/resident-start.js +20 -20
- package/dist/src/platform/windows/resident-start.ps1 +7 -7
- package/dist/src/platform/windows/resident-startup.ps1 +83 -83
- package/dist/src/platform/windows/resolve-state-root.ps1 +14 -14
- package/dist/src/platform/windows/runtime-paths.mjs +20 -20
- package/dist/src/platform/windows/rustdesk-video-probe.mjs +167 -167
- package/dist/src/platform/windows/service-host.mjs +31 -31
- package/dist/src/platform/windows/start-hidden-console.ps1 +49 -49
- package/dist/src/platform/windows/stop-desktop.ps1 +22 -22
- package/dist/src/platform/windows/user-environment.mjs +50 -50
- package/dist/src/platform/windows/user-environment.ps1 +41 -41
- package/dist/src/project-archive.mjs +30 -0
- package/dist/src/project-create.mjs +121 -119
- package/dist/src/project-management.mjs +59 -69
- package/dist/src/project-remove.mjs +55 -0
- package/dist/src/project-update.mjs +56 -0
- package/dist/src/runtime-discovery.mjs +47 -47
- package/dist/src/self-relay/account-cleanup.mjs +30 -30
- package/dist/src/self-relay/account-connections.mjs +52 -52
- package/dist/src/self-relay/admin-cloud.mjs +101 -101
- package/dist/src/self-relay/admin-page.mjs +80 -80
- package/dist/src/self-relay/admin-server.mjs +74 -74
- package/dist/src/self-relay/admin-state.mjs +127 -127
- package/dist/src/self-relay/admin-style.mjs +14 -14
- package/dist/src/self-relay/android-releases.mjs +61 -61
- package/dist/src/self-relay/client.mjs +61 -61
- package/dist/src/self-relay/connector.mjs +439 -438
- package/dist/src/self-relay/debug-allowlist.mjs +33 -33
- package/dist/src/self-relay/demo.mjs +37 -37
- package/dist/src/self-relay/device-info.mjs +27 -0
- package/dist/src/self-relay/device-store.mjs +92 -84
- package/dist/src/self-relay/diagnostics.mjs +54 -54
- package/dist/src/self-relay/email-accounts.mjs +183 -183
- package/dist/src/self-relay/file-wire.mjs +13 -13
- package/dist/src/self-relay/lan-authorizations.mjs +71 -42
- package/dist/src/self-relay/lifecycle.mjs +264 -250
- package/dist/src/self-relay/long-pairing.mjs +40 -40
- package/dist/src/self-relay/mysql-state.mjs +118 -105
- package/dist/src/self-relay/p2p-probe.mjs +159 -159
- package/dist/src/self-relay/privacy-page.mjs +26 -26
- package/dist/src/self-relay/public-releases.mjs +34 -0
- package/dist/src/self-relay/rtc-file-bridge.mjs +41 -41
- package/dist/src/self-relay/server.mjs +1006 -932
- package/dist/src/self-relay/usage-daily.mjs +55 -55
- package/dist/src/self-relay/usage-meter.mjs +141 -141
- package/dist/src/self-relay/windows-route-helper.mjs +75 -75
- package/dist/src/self-relay/wire.mjs +36 -36
- package/dist/src/server.mjs +1861 -1778
- package/dist/src/service-control.mjs +82 -82
- package/dist/src/service-doctor.mjs +110 -110
- package/dist/src/service-health.mjs +33 -33
- package/dist/src/service-install.mjs +91 -91
- package/dist/src/service-lifecycle.mjs +138 -138
- package/dist/src/service-restart.mjs +81 -81
- package/dist/src/shared-app-server.mjs +615 -611
- package/dist/src/shared-catalog.mjs +132 -132
- package/dist/src/shared-recovery.mjs +146 -146
- package/dist/src/shared-thread-stream.mjs +164 -164
- package/dist/src/skill-install.mjs +51 -51
- package/dist/src/thread-title-generation.mjs +150 -150
- package/dist/src/turn-model-history.mjs +47 -47
- package/dist/src/usage-reset.mjs +17 -17
- package/dist/src/workstation-downloads.mjs +120 -120
- package/dist/tools/rtc-route-helper.ps1 +99 -99
- package/dist/tools/start-rtc-route-helper.ps1 +15 -15
- package/dist/web/vendor/markdown-it.LICENSE.txt +22 -22
- package/dist/web/vendor/markdown-it.umd.min.js +13 -13
- package/package.json +7 -6
- package/tools/postinstall.mjs +127 -127
|
@@ -1,173 +1,173 @@
|
|
|
1
|
-
(function (root, factory) {
|
|
2
|
-
if (typeof module === 'object' && module.exports) module.exports = factory();
|
|
3
|
-
else root.CodexTimelineSync = factory();
|
|
4
|
-
})(globalThis, function () {
|
|
5
|
-
const equal = (left, right) => left === right || JSON.stringify(left) === JSON.stringify(right);
|
|
6
|
-
function changedFields(previous, current, ignored) {
|
|
7
|
-
const set = {}, append = {}, unset = [];
|
|
8
|
-
for (const key of new Set([...Object.keys(previous || {}), ...Object.keys(current || {})])) {
|
|
9
|
-
if (ignored.has(key) || equal(previous?.[key], current?.[key])) continue;
|
|
10
|
-
if (!Object.hasOwn(current, key)) { unset.push(key); continue; }
|
|
11
|
-
const before = previous?.[key], after = current[key];
|
|
12
|
-
if (typeof before === 'string' && typeof after === 'string' && after.startsWith(before)) append[key] = after.slice(before.length);
|
|
13
|
-
else set[key] = after;
|
|
14
|
-
}
|
|
15
|
-
return { set, append, unset };
|
|
16
|
-
}
|
|
17
|
-
const nonempty = changes => Object.keys(changes.set).length || Object.keys(changes.append).length || changes.unset.length;
|
|
18
|
-
// An ordered history range owns the order of its shared IDs. Keep entries
|
|
19
|
-
// outside that range at their existing shared anchors, never by timestamps.
|
|
20
|
-
function mergeOrderedEntries(existing, incoming, merge = (_previous, next) => next, incomingIsOlder = false) {
|
|
21
|
-
const incomingIds = new Set(incoming.map(entry => entry.id));
|
|
22
|
-
const existingById = new Map(existing.map(entry => [entry.id, entry]));
|
|
23
|
-
const before = new Map();
|
|
24
|
-
let pending = [], lastShared;
|
|
25
|
-
for (const entry of existing) {
|
|
26
|
-
if (incomingIds.has(entry.id)) {
|
|
27
|
-
before.set(entry.id, pending); pending = []; lastShared = entry.id;
|
|
28
|
-
} else pending.push(entry);
|
|
29
|
-
}
|
|
30
|
-
if (lastShared === undefined) return incomingIsOlder ? [...incoming, ...existing] : [...existing, ...incoming];
|
|
31
|
-
return incoming.flatMap(entry => [ ...(before.get(entry.id) || []),
|
|
32
|
-
existingById.has(entry.id) ? merge(existingById.get(entry.id), entry) : entry,
|
|
33
|
-
...(entry.id === lastShared ? pending : []) ]);
|
|
34
|
-
}
|
|
35
|
-
function diffTurn(previous, current) {
|
|
36
|
-
if (!previous || !current || previous.id !== current.id) return null;
|
|
37
|
-
const before = Array.isArray(previous.entries) ? previous.entries : [];
|
|
38
|
-
const after = Array.isArray(current.entries) ? current.entries : [];
|
|
39
|
-
if (after.length < before.length) return null;
|
|
40
|
-
const entries = [];
|
|
41
|
-
for (let index = 0; index < before.length; index++) {
|
|
42
|
-
if (before[index]?.id !== after[index]?.id || before[index]?.type !== after[index]?.type ||
|
|
43
|
-
before[index]?.role !== after[index]?.role || before[index]?.kind !== after[index]?.kind) return null;
|
|
44
|
-
const changes = changedFields(before[index], after[index], new Set(['id', 'type', 'role', 'kind']));
|
|
45
|
-
if (nonempty(changes)) entries.push({ id: after[index].id, index, ...changes });
|
|
46
|
-
}
|
|
47
|
-
const turn = changedFields(previous, current, new Set(['id', 'entries']));
|
|
48
|
-
const appended = after.slice(before.length);
|
|
49
|
-
return { id: current.id, ...turn, entries, appended };
|
|
50
|
-
}
|
|
51
|
-
function applyChanges(target, changes) {
|
|
52
|
-
const result = { ...target, ...(changes.set || {}) };
|
|
53
|
-
for (const key of changes.unset || []) delete result[key];
|
|
54
|
-
for (const [key, suffix] of Object.entries(changes.append || {})) {
|
|
55
|
-
if (typeof result[key] !== 'string' || typeof suffix !== 'string') throw new Error('增量字段类型不匹配');
|
|
56
|
-
result[key] += suffix;
|
|
57
|
-
}
|
|
58
|
-
return result;
|
|
59
|
-
}
|
|
60
|
-
function applyTurnPatch(turn, patch) {
|
|
61
|
-
if (!turn || turn.id !== patch?.id) throw new Error('增量轮次不存在');
|
|
62
|
-
const next = applyChanges(turn, patch), entries = [...(turn.entries || [])];
|
|
63
|
-
for (const entryPatch of patch.entries || []) {
|
|
64
|
-
const index = entries.findIndex(entry => entry.id === entryPatch.id);
|
|
65
|
-
const entry = entries[index];
|
|
66
|
-
if (!entry || entry.id !== entryPatch.id) throw new Error('增量条目边界不匹配');
|
|
67
|
-
entries[index] = applyChanges(entry, entryPatch);
|
|
68
|
-
}
|
|
69
|
-
for (const entry of patch.appended || []) {
|
|
70
|
-
if (entries.some(existing => existing.id === entry.id)) throw new Error('增量条目重复');
|
|
71
|
-
entries.push(entry);
|
|
72
|
-
}
|
|
73
|
-
if (turn.itemPage && next.itemPage) next.itemPage = { ...next.itemPage,
|
|
74
|
-
olderCursor: turn.itemPage.olderCursor, hasLoadedOldest: turn.itemPage.hasLoadedOldest,
|
|
75
|
-
oldestItemId: turn.itemPage.oldestItemId };
|
|
76
|
-
next.entries = entries; return next;
|
|
77
|
-
}
|
|
78
|
-
function acceptSyncUpdate(current, incoming) {
|
|
79
|
-
if (incoming.reset) return { reset: true, next: incoming };
|
|
80
|
-
const valid = current && incoming.id === current.id && incoming.epoch === current.epoch &&
|
|
81
|
-
Number.isSafeInteger(incoming.fromSeq) && incoming.fromSeq >= 1 &&
|
|
82
|
-
Number.isSafeInteger(incoming.toSeq) && incoming.toSeq >= incoming.fromSeq;
|
|
83
|
-
if (!valid || incoming.fromSeq > current.seq + 1) throw new Error('实时更新序号不连续');
|
|
84
|
-
if (incoming.toSeq <= current.seq) return { duplicate: true, next: current };
|
|
85
|
-
return { next: { ...current, seq: incoming.toSeq } };
|
|
86
|
-
}
|
|
87
|
-
// A batch is acknowledged after consumption. Only an individual record is
|
|
88
|
-
// reassembled; the timeline itself is never serialized into one large string.
|
|
89
|
-
function* timelineBatches(timeline) {
|
|
90
|
-
function* entryRecords(value, kind, textField = '') {
|
|
91
|
-
const text = textField ? value[textField]?.text : value.text;
|
|
92
|
-
if (typeof text !== 'string' || text.length <= 4096) { yield { kind, value }; return; }
|
|
93
|
-
const metadata = textField ? { ...value, [textField]: { ...value[textField], text: '' } } : { ...value, text: '' };
|
|
94
|
-
yield { kind: 'entry-start', value: metadata, target: kind, textField };
|
|
95
|
-
for (let offset = 0; offset < text.length; offset += 4096) yield { kind: 'entry-text', value: text.slice(offset, offset + 4096) };
|
|
96
|
-
yield { kind: 'entry-end' };
|
|
97
|
-
}
|
|
98
|
-
function* records() {
|
|
99
|
-
const { turns, turnPatches, ...metadata } = timeline;
|
|
100
|
-
yield { kind: 'timeline', value: metadata };
|
|
101
|
-
for (const turn of turns) {
|
|
102
|
-
const { entries, ...metadata } = turn;
|
|
103
|
-
yield { kind: 'turn', value: metadata };
|
|
104
|
-
for (const entry of entries || []) yield* entryRecords(entry, 'entry');
|
|
105
|
-
}
|
|
106
|
-
for (const patch of turnPatches || []) {
|
|
107
|
-
const { entries, appended, ...metadata } = patch;
|
|
108
|
-
yield { kind: 'patch', value: metadata };
|
|
109
|
-
for (const entry of entries || []) yield* entryRecords(entry, 'patch-entry', entry.append?.text ? 'append' : 'set');
|
|
110
|
-
for (const entry of appended || []) yield* entryRecords(entry, 'patch-appended');
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
const encoder = new TextEncoder();
|
|
114
|
-
let batch = [], bytes = 0;
|
|
115
|
-
for (const record of records()) {
|
|
116
|
-
const json = JSON.stringify(record);
|
|
117
|
-
for (let offset = 0; offset < json.length; offset += 4096) {
|
|
118
|
-
const piece = { text: json.slice(offset, offset + 4096), end: offset + 4096 >= json.length };
|
|
119
|
-
const size = encoder.encode(JSON.stringify(piece)).length;
|
|
120
|
-
if (batch.length && bytes + size > 48 * 1024) { yield batch; batch = []; bytes = 0; }
|
|
121
|
-
batch.push(piece); bytes += size;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
if (batch.length) yield batch;
|
|
125
|
-
}
|
|
126
|
-
function createTimelineReceiver() {
|
|
127
|
-
let timeline, turn, patch, entry, entryTarget, textField, textLength = 0, textParts = [], sequence = 0, parts = [], length = 0;
|
|
128
|
-
return {
|
|
129
|
-
accept(batch, pieces) {
|
|
130
|
-
if (batch !== ++sequence || !Array.isArray(pieces)) throw new Error('历史条目批次不连续');
|
|
131
|
-
const added = [];
|
|
132
|
-
const commitEntry = (target, value) => {
|
|
133
|
-
if (target === 'entry' && turn) { turn.entries.push(value); added.push({ turn, entry: value }); }
|
|
134
|
-
else if (target === 'patch-entry' && patch) patch.entries.push(value);
|
|
135
|
-
else if (target === 'patch-appended' && patch) patch.appended.push(value);
|
|
136
|
-
else throw new Error('历史条目目标无效');
|
|
137
|
-
};
|
|
138
|
-
for (const piece of pieces) {
|
|
139
|
-
if (typeof piece.text !== 'string' || typeof piece.end !== 'boolean') throw new Error('历史条目格式无效');
|
|
140
|
-
parts.push(piece.text); length += piece.text.length;
|
|
141
|
-
if (length > 32 * 1024 * 1024) throw new Error('单条历史内容超过接收限制');
|
|
142
|
-
if (!piece.end) continue;
|
|
143
|
-
const record = JSON.parse(parts.join('')); parts = []; length = 0;
|
|
144
|
-
if (entry && !['entry-text', 'entry-end'].includes(record.kind)) throw new Error('历史正文传输未完成');
|
|
145
|
-
if (record.kind === 'timeline' && !timeline) timeline = { ...record.value, turns: [] };
|
|
146
|
-
else if (record.kind === 'turn' && timeline) {
|
|
147
|
-
turn = { ...record.value, entries: [] }; timeline.turns.push(turn);
|
|
148
|
-
} else if (['entry', 'patch-entry', 'patch-appended'].includes(record.kind)) {
|
|
149
|
-
commitEntry(record.kind, record.value);
|
|
150
|
-
} else if (record.kind === 'entry-start' && !entry) {
|
|
151
|
-
entry = record.value; entryTarget = record.target; textField = record.textField; textParts = []; textLength = 0;
|
|
152
|
-
} else if (record.kind === 'entry-text' && entry && typeof record.value === 'string') {
|
|
153
|
-
textLength += record.value.length;
|
|
154
|
-
if (textLength > 32 * 1024 * 1024) throw new Error('单条历史正文超过接收限制');
|
|
155
|
-
textParts.push(record.value);
|
|
156
|
-
} else if (record.kind === 'entry-end' && entry) {
|
|
157
|
-
if (textField) entry[textField].text = textParts.join(''); else entry.text = textParts.join('');
|
|
158
|
-
textParts = []; commitEntry(entryTarget, entry); entry = null;
|
|
159
|
-
} else if (record.kind === 'patch' && timeline) {
|
|
160
|
-
patch = { ...record.value, entries: [], appended: [] };
|
|
161
|
-
(timeline.turnPatches ||= []).push(patch);
|
|
162
|
-
} else throw new Error('历史条目顺序无效');
|
|
163
|
-
}
|
|
164
|
-
return { timeline, added };
|
|
165
|
-
},
|
|
166
|
-
finish() {
|
|
167
|
-
if (!timeline || parts.length || entry) throw new Error('历史条目传输未完成');
|
|
168
|
-
return timeline;
|
|
169
|
-
},
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
return { mergeOrderedEntries, diffTurn, applyTurnPatch, acceptSyncUpdate, timelineBatches, createTimelineReceiver };
|
|
173
|
-
});
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
if (typeof module === 'object' && module.exports) module.exports = factory();
|
|
3
|
+
else root.CodexTimelineSync = factory();
|
|
4
|
+
})(globalThis, function () {
|
|
5
|
+
const equal = (left, right) => left === right || JSON.stringify(left) === JSON.stringify(right);
|
|
6
|
+
function changedFields(previous, current, ignored) {
|
|
7
|
+
const set = {}, append = {}, unset = [];
|
|
8
|
+
for (const key of new Set([...Object.keys(previous || {}), ...Object.keys(current || {})])) {
|
|
9
|
+
if (ignored.has(key) || equal(previous?.[key], current?.[key])) continue;
|
|
10
|
+
if (!Object.hasOwn(current, key)) { unset.push(key); continue; }
|
|
11
|
+
const before = previous?.[key], after = current[key];
|
|
12
|
+
if (typeof before === 'string' && typeof after === 'string' && after.startsWith(before)) append[key] = after.slice(before.length);
|
|
13
|
+
else set[key] = after;
|
|
14
|
+
}
|
|
15
|
+
return { set, append, unset };
|
|
16
|
+
}
|
|
17
|
+
const nonempty = changes => Object.keys(changes.set).length || Object.keys(changes.append).length || changes.unset.length;
|
|
18
|
+
// An ordered history range owns the order of its shared IDs. Keep entries
|
|
19
|
+
// outside that range at their existing shared anchors, never by timestamps.
|
|
20
|
+
function mergeOrderedEntries(existing, incoming, merge = (_previous, next) => next, incomingIsOlder = false) {
|
|
21
|
+
const incomingIds = new Set(incoming.map(entry => entry.id));
|
|
22
|
+
const existingById = new Map(existing.map(entry => [entry.id, entry]));
|
|
23
|
+
const before = new Map();
|
|
24
|
+
let pending = [], lastShared;
|
|
25
|
+
for (const entry of existing) {
|
|
26
|
+
if (incomingIds.has(entry.id)) {
|
|
27
|
+
before.set(entry.id, pending); pending = []; lastShared = entry.id;
|
|
28
|
+
} else pending.push(entry);
|
|
29
|
+
}
|
|
30
|
+
if (lastShared === undefined) return incomingIsOlder ? [...incoming, ...existing] : [...existing, ...incoming];
|
|
31
|
+
return incoming.flatMap(entry => [ ...(before.get(entry.id) || []),
|
|
32
|
+
existingById.has(entry.id) ? merge(existingById.get(entry.id), entry) : entry,
|
|
33
|
+
...(entry.id === lastShared ? pending : []) ]);
|
|
34
|
+
}
|
|
35
|
+
function diffTurn(previous, current) {
|
|
36
|
+
if (!previous || !current || previous.id !== current.id) return null;
|
|
37
|
+
const before = Array.isArray(previous.entries) ? previous.entries : [];
|
|
38
|
+
const after = Array.isArray(current.entries) ? current.entries : [];
|
|
39
|
+
if (after.length < before.length) return null;
|
|
40
|
+
const entries = [];
|
|
41
|
+
for (let index = 0; index < before.length; index++) {
|
|
42
|
+
if (before[index]?.id !== after[index]?.id || before[index]?.type !== after[index]?.type ||
|
|
43
|
+
before[index]?.role !== after[index]?.role || before[index]?.kind !== after[index]?.kind) return null;
|
|
44
|
+
const changes = changedFields(before[index], after[index], new Set(['id', 'type', 'role', 'kind']));
|
|
45
|
+
if (nonempty(changes)) entries.push({ id: after[index].id, index, ...changes });
|
|
46
|
+
}
|
|
47
|
+
const turn = changedFields(previous, current, new Set(['id', 'entries']));
|
|
48
|
+
const appended = after.slice(before.length);
|
|
49
|
+
return { id: current.id, ...turn, entries, appended };
|
|
50
|
+
}
|
|
51
|
+
function applyChanges(target, changes) {
|
|
52
|
+
const result = { ...target, ...(changes.set || {}) };
|
|
53
|
+
for (const key of changes.unset || []) delete result[key];
|
|
54
|
+
for (const [key, suffix] of Object.entries(changes.append || {})) {
|
|
55
|
+
if (typeof result[key] !== 'string' || typeof suffix !== 'string') throw new Error('增量字段类型不匹配');
|
|
56
|
+
result[key] += suffix;
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
function applyTurnPatch(turn, patch) {
|
|
61
|
+
if (!turn || turn.id !== patch?.id) throw new Error('增量轮次不存在');
|
|
62
|
+
const next = applyChanges(turn, patch), entries = [...(turn.entries || [])];
|
|
63
|
+
for (const entryPatch of patch.entries || []) {
|
|
64
|
+
const index = entries.findIndex(entry => entry.id === entryPatch.id);
|
|
65
|
+
const entry = entries[index];
|
|
66
|
+
if (!entry || entry.id !== entryPatch.id) throw new Error('增量条目边界不匹配');
|
|
67
|
+
entries[index] = applyChanges(entry, entryPatch);
|
|
68
|
+
}
|
|
69
|
+
for (const entry of patch.appended || []) {
|
|
70
|
+
if (entries.some(existing => existing.id === entry.id)) throw new Error('增量条目重复');
|
|
71
|
+
entries.push(entry);
|
|
72
|
+
}
|
|
73
|
+
if (turn.itemPage && next.itemPage) next.itemPage = { ...next.itemPage,
|
|
74
|
+
olderCursor: turn.itemPage.olderCursor, hasLoadedOldest: turn.itemPage.hasLoadedOldest,
|
|
75
|
+
oldestItemId: turn.itemPage.oldestItemId };
|
|
76
|
+
next.entries = entries; return next;
|
|
77
|
+
}
|
|
78
|
+
function acceptSyncUpdate(current, incoming) {
|
|
79
|
+
if (incoming.reset) return { reset: true, next: incoming };
|
|
80
|
+
const valid = current && incoming.id === current.id && incoming.epoch === current.epoch &&
|
|
81
|
+
Number.isSafeInteger(incoming.fromSeq) && incoming.fromSeq >= 1 &&
|
|
82
|
+
Number.isSafeInteger(incoming.toSeq) && incoming.toSeq >= incoming.fromSeq;
|
|
83
|
+
if (!valid || incoming.fromSeq > current.seq + 1) throw new Error('实时更新序号不连续');
|
|
84
|
+
if (incoming.toSeq <= current.seq) return { duplicate: true, next: current };
|
|
85
|
+
return { next: { ...current, seq: incoming.toSeq } };
|
|
86
|
+
}
|
|
87
|
+
// A batch is acknowledged after consumption. Only an individual record is
|
|
88
|
+
// reassembled; the timeline itself is never serialized into one large string.
|
|
89
|
+
function* timelineBatches(timeline) {
|
|
90
|
+
function* entryRecords(value, kind, textField = '') {
|
|
91
|
+
const text = textField ? value[textField]?.text : value.text;
|
|
92
|
+
if (typeof text !== 'string' || text.length <= 4096) { yield { kind, value }; return; }
|
|
93
|
+
const metadata = textField ? { ...value, [textField]: { ...value[textField], text: '' } } : { ...value, text: '' };
|
|
94
|
+
yield { kind: 'entry-start', value: metadata, target: kind, textField };
|
|
95
|
+
for (let offset = 0; offset < text.length; offset += 4096) yield { kind: 'entry-text', value: text.slice(offset, offset + 4096) };
|
|
96
|
+
yield { kind: 'entry-end' };
|
|
97
|
+
}
|
|
98
|
+
function* records() {
|
|
99
|
+
const { turns, turnPatches, ...metadata } = timeline;
|
|
100
|
+
yield { kind: 'timeline', value: metadata };
|
|
101
|
+
for (const turn of turns) {
|
|
102
|
+
const { entries, ...metadata } = turn;
|
|
103
|
+
yield { kind: 'turn', value: metadata };
|
|
104
|
+
for (const entry of entries || []) yield* entryRecords(entry, 'entry');
|
|
105
|
+
}
|
|
106
|
+
for (const patch of turnPatches || []) {
|
|
107
|
+
const { entries, appended, ...metadata } = patch;
|
|
108
|
+
yield { kind: 'patch', value: metadata };
|
|
109
|
+
for (const entry of entries || []) yield* entryRecords(entry, 'patch-entry', entry.append?.text ? 'append' : 'set');
|
|
110
|
+
for (const entry of appended || []) yield* entryRecords(entry, 'patch-appended');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const encoder = new TextEncoder();
|
|
114
|
+
let batch = [], bytes = 0;
|
|
115
|
+
for (const record of records()) {
|
|
116
|
+
const json = JSON.stringify(record);
|
|
117
|
+
for (let offset = 0; offset < json.length; offset += 4096) {
|
|
118
|
+
const piece = { text: json.slice(offset, offset + 4096), end: offset + 4096 >= json.length };
|
|
119
|
+
const size = encoder.encode(JSON.stringify(piece)).length;
|
|
120
|
+
if (batch.length && bytes + size > 48 * 1024) { yield batch; batch = []; bytes = 0; }
|
|
121
|
+
batch.push(piece); bytes += size;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (batch.length) yield batch;
|
|
125
|
+
}
|
|
126
|
+
function createTimelineReceiver() {
|
|
127
|
+
let timeline, turn, patch, entry, entryTarget, textField, textLength = 0, textParts = [], sequence = 0, parts = [], length = 0;
|
|
128
|
+
return {
|
|
129
|
+
accept(batch, pieces) {
|
|
130
|
+
if (batch !== ++sequence || !Array.isArray(pieces)) throw new Error('历史条目批次不连续');
|
|
131
|
+
const added = [];
|
|
132
|
+
const commitEntry = (target, value) => {
|
|
133
|
+
if (target === 'entry' && turn) { turn.entries.push(value); added.push({ turn, entry: value }); }
|
|
134
|
+
else if (target === 'patch-entry' && patch) patch.entries.push(value);
|
|
135
|
+
else if (target === 'patch-appended' && patch) patch.appended.push(value);
|
|
136
|
+
else throw new Error('历史条目目标无效');
|
|
137
|
+
};
|
|
138
|
+
for (const piece of pieces) {
|
|
139
|
+
if (typeof piece.text !== 'string' || typeof piece.end !== 'boolean') throw new Error('历史条目格式无效');
|
|
140
|
+
parts.push(piece.text); length += piece.text.length;
|
|
141
|
+
if (length > 32 * 1024 * 1024) throw new Error('单条历史内容超过接收限制');
|
|
142
|
+
if (!piece.end) continue;
|
|
143
|
+
const record = JSON.parse(parts.join('')); parts = []; length = 0;
|
|
144
|
+
if (entry && !['entry-text', 'entry-end'].includes(record.kind)) throw new Error('历史正文传输未完成');
|
|
145
|
+
if (record.kind === 'timeline' && !timeline) timeline = { ...record.value, turns: [] };
|
|
146
|
+
else if (record.kind === 'turn' && timeline) {
|
|
147
|
+
turn = { ...record.value, entries: [] }; timeline.turns.push(turn);
|
|
148
|
+
} else if (['entry', 'patch-entry', 'patch-appended'].includes(record.kind)) {
|
|
149
|
+
commitEntry(record.kind, record.value);
|
|
150
|
+
} else if (record.kind === 'entry-start' && !entry) {
|
|
151
|
+
entry = record.value; entryTarget = record.target; textField = record.textField; textParts = []; textLength = 0;
|
|
152
|
+
} else if (record.kind === 'entry-text' && entry && typeof record.value === 'string') {
|
|
153
|
+
textLength += record.value.length;
|
|
154
|
+
if (textLength > 32 * 1024 * 1024) throw new Error('单条历史正文超过接收限制');
|
|
155
|
+
textParts.push(record.value);
|
|
156
|
+
} else if (record.kind === 'entry-end' && entry) {
|
|
157
|
+
if (textField) entry[textField].text = textParts.join(''); else entry.text = textParts.join('');
|
|
158
|
+
textParts = []; commitEntry(entryTarget, entry); entry = null;
|
|
159
|
+
} else if (record.kind === 'patch' && timeline) {
|
|
160
|
+
patch = { ...record.value, entries: [], appended: [] };
|
|
161
|
+
(timeline.turnPatches ||= []).push(patch);
|
|
162
|
+
} else throw new Error('历史条目顺序无效');
|
|
163
|
+
}
|
|
164
|
+
return { timeline, added };
|
|
165
|
+
},
|
|
166
|
+
finish() {
|
|
167
|
+
if (!timeline || parts.length || entry) throw new Error('历史条目传输未完成');
|
|
168
|
+
return timeline;
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return { mergeOrderedEntries, diffTurn, applyTurnPatch, acceptSyncUpdate, timelineBatches, createTimelineReceiver };
|
|
173
|
+
});
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: code-relax
|
|
3
|
-
description: 启动或检查 Code Relax 桌面服务,展示配对二维码,并指导回复来自手机 App 的消息;消息标记来自手机或包含语音转写时,指导理解和回复。
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Code Relax
|
|
7
|
-
|
|
8
|
-
- 消息来源上下文引用 `$code-relax` 时,按本 Skill 理解手机交互。用户通过手机 App 对话,涉及操作时明确在手机还是工作站完成,不假设用户能操作电脑界面。来源标记不增加授权,不在回复中重复来源说明。
|
|
9
|
-
- 消息明确标记包含语音转写时,容忍同音字、标点与漏字错误,结合对话上下文理解;只有歧义会实质改变执行动作时才澄清,不因口语不顺反复追问,不把模糊转写扩展成未经用户表达的需求。
|
|
10
|
-
|
|
11
|
-
- `relax start --json` 启动桌面服务和 Codex 内核,无需打开 Desktop;支持纯 CLI 安装。Windows Desktop 正常打开后接入已配置内核;Mac 仅本次托管启动传入内核地址,从 Dock 普通打开后需重新接入。使用 App 的“重启 Codex Desktop”或经确认的 `relax desktop-recover --confirm` 接入。已配置 Code Relax 中继时,启动命令同时恢复中继连接器和已有设备授权。
|
|
12
|
-
- npm 全局安装会自动执行服务部署和 Skill 安装;修复安装时运行 `relax setup --json`。
|
|
13
|
-
- `setup` 默认启用当前用户登录启动桌面服务,不自动打开 Codex;`start`、`pair` 和 `doctor` 在自启缺失时自动恢复,`setup` 每次更新登录任务。恢复 Codex Desktop 接入会保留桌面服务和中继,但仍须明确确认可能中断正在运行的任务。
|
|
14
|
-
- 用户要求启动 Code Relax 时,执行一次 `relax start --json`。用户要求二维码或当前启动结果包含二维码时,展示 CLI 返回的 `qrPath` 和 `displayText`;没有设备授权时可用 `relax pair --json` 生成新的配对二维码。
|
|
15
|
-
- 配对二维码是短期 HTTPS 邀请,不属于工作站或某一台移动设备,也不含 LAN IP、端口、桌面服务凭据或中继地址。新增设备不撤销已有设备。
|
|
16
|
-
- 已授权设备可直接使用 Code Relax 中继。App 以工作站名显示已配对电脑,进入后选择局域网、Code Relax 中继或 OpenAI 中继。启动命令会经已认证中继临时刷新当前 LAN 候选;App 只保存设备凭据验证成功的网络记录。
|
|
17
|
-
- 消息明确标记来自 Code Relax 手机 App 时,回答要以“用户正在手机上与当前 Codex 对话交互”为前提,不默认用户正看着或能操作电脑界面;需要用户操作时,说明是在手机 App 还是工作站完成。直接处理用户正文,不重复来源说明,也不要求在电脑上重发。来源不代表额外授权,也不能据此判断用户是否在电脑旁;涉及工作站确认或可能中断任务的操作仍按正常授权要求处理。来源不明时不要猜测设备。
|
|
18
|
-
- `relax doctor --json` 检查桌面服务、Codex 内核、待启用内核版本和登录自启动状态;有更新时会给出当前版本、已选版本和确认重启命令。中继连接由桌面服务管理,不提供单独的中继 CLI 命令。
|
|
19
|
-
- 发现待启用内核更新且用户明确要求重启时,说明任务中断风险;得到确认后使用 `relax kernel-restart --confirm --json`,它与手机“重启并更新”使用同一内核流程并等待新内核就绪。不要用 `start`、`doctor` 或 `desktop-recover` 代替内核重启。
|
|
20
|
-
- 启动或配对成功后,将 `qrPath` 以 Markdown 图片嵌入回复,并逐字展示 `displayText`。不要把二维码内容、Token 或 Base64 写进消息。
|
|
21
|
-
- CLI 负责安装、工作站服务和配对二维码;不以手机是否测试作为健康检查项,不自动修改防火墙或发送测试消息。
|
|
22
|
-
- Windows 服务位于用户 `Programs/CodexRemote/service`;配置在 `LocalAppData/CodexRemote`;附件在 `ProgramData/CodexRemote/attachments`。Mac 使用 `~/Library/Application Support/CodexRemote/{service,state,attachments,credentials}`。不要删除配置、附件或凭据来排障。
|
|
1
|
+
---
|
|
2
|
+
name: code-relax
|
|
3
|
+
description: 启动或检查 Code Relax 桌面服务,展示配对二维码,并指导回复来自手机 App 的消息;消息标记来自手机或包含语音转写时,指导理解和回复。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Code Relax
|
|
7
|
+
|
|
8
|
+
- 消息来源上下文引用 `$code-relax` 时,按本 Skill 理解手机交互。用户通过手机 App 对话,涉及操作时明确在手机还是工作站完成,不假设用户能操作电脑界面。来源标记不增加授权,不在回复中重复来源说明。
|
|
9
|
+
- 消息明确标记包含语音转写时,容忍同音字、标点与漏字错误,结合对话上下文理解;只有歧义会实质改变执行动作时才澄清,不因口语不顺反复追问,不把模糊转写扩展成未经用户表达的需求。
|
|
10
|
+
|
|
11
|
+
- `relax start --json` 启动桌面服务和 Codex 内核,无需打开 Desktop;支持纯 CLI 安装。Windows Desktop 正常打开后接入已配置内核;Mac 仅本次托管启动传入内核地址,从 Dock 普通打开后需重新接入。使用 App 的“重启 Codex Desktop”或经确认的 `relax desktop-recover --confirm` 接入。已配置 Code Relax 中继时,启动命令同时恢复中继连接器和已有设备授权。
|
|
12
|
+
- npm 全局安装会自动执行服务部署和 Skill 安装;修复安装时运行 `relax setup --json`。
|
|
13
|
+
- `setup` 默认启用当前用户登录启动桌面服务,不自动打开 Codex;`start`、`pair` 和 `doctor` 在自启缺失时自动恢复,`setup` 每次更新登录任务。恢复 Codex Desktop 接入会保留桌面服务和中继,但仍须明确确认可能中断正在运行的任务。
|
|
14
|
+
- 用户要求启动 Code Relax 时,执行一次 `relax start --json`。用户要求二维码或当前启动结果包含二维码时,展示 CLI 返回的 `qrPath` 和 `displayText`;没有设备授权时可用 `relax pair --json` 生成新的配对二维码。
|
|
15
|
+
- 配对二维码是短期 HTTPS 邀请,不属于工作站或某一台移动设备,也不含 LAN IP、端口、桌面服务凭据或中继地址。新增设备不撤销已有设备。
|
|
16
|
+
- 已授权设备可直接使用 Code Relax 中继。App 以工作站名显示已配对电脑,进入后选择局域网、Code Relax 中继或 OpenAI 中继。启动命令会经已认证中继临时刷新当前 LAN 候选;App 只保存设备凭据验证成功的网络记录。
|
|
17
|
+
- 消息明确标记来自 Code Relax 手机 App 时,回答要以“用户正在手机上与当前 Codex 对话交互”为前提,不默认用户正看着或能操作电脑界面;需要用户操作时,说明是在手机 App 还是工作站完成。直接处理用户正文,不重复来源说明,也不要求在电脑上重发。来源不代表额外授权,也不能据此判断用户是否在电脑旁;涉及工作站确认或可能中断任务的操作仍按正常授权要求处理。来源不明时不要猜测设备。
|
|
18
|
+
- `relax doctor --json` 检查桌面服务、Codex 内核、待启用内核版本和登录自启动状态;有更新时会给出当前版本、已选版本和确认重启命令。中继连接由桌面服务管理,不提供单独的中继 CLI 命令。
|
|
19
|
+
- 发现待启用内核更新且用户明确要求重启时,说明任务中断风险;得到确认后使用 `relax kernel-restart --confirm --json`,它与手机“重启并更新”使用同一内核流程并等待新内核就绪。不要用 `start`、`doctor` 或 `desktop-recover` 代替内核重启。
|
|
20
|
+
- 启动或配对成功后,将 `qrPath` 以 Markdown 图片嵌入回复,并逐字展示 `displayText`。不要把二维码内容、Token 或 Base64 写进消息。
|
|
21
|
+
- CLI 负责安装、工作站服务和配对二维码;不以手机是否测试作为健康检查项,不自动修改防火墙或发送测试消息。
|
|
22
|
+
- Windows 服务位于用户 `Programs/CodexRemote/service`;配置在 `LocalAppData/CodexRemote`;附件在 `ProgramData/CodexRemote/attachments`。Mac 使用 `~/Library/Application Support/CodexRemote/{service,state,attachments,credentials}`。不要删除配置、附件或凭据来排障。
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { sharedThreadProjectId, dotsThreadProjectId } from './shared-catalog.mjs';
|
|
2
|
+
|
|
3
|
+
// One metadata page per request. Local and hosted cursors must never be mixed.
|
|
4
|
+
export async function archivedThreadsPage(server, localServer, hostId, cursor) {
|
|
5
|
+
await server.connect();
|
|
6
|
+
const identity = server.profileIdentity;
|
|
7
|
+
const [page, projects] = await Promise.all([
|
|
8
|
+
server.request('thread/list', { archived: true, limit: 100, sortKey: 'updated_at',
|
|
9
|
+
sortDirection: 'desc', ...(hostId === 'local' ? { useStateDbOnly: true } : {}),
|
|
10
|
+
...(cursor ? { cursor } : {}) }),
|
|
11
|
+
localServer.projects(),
|
|
12
|
+
]);
|
|
13
|
+
if (hostId === 'durable' && identity !== server.profileIdentity) throw new Error('云端账号已变化,请重新读取归档列表。');
|
|
14
|
+
const group = hostId === 'durable' ? dotsThreadProjectId : sharedThreadProjectId;
|
|
15
|
+
return { nextCursor: page.nextCursor || null,
|
|
16
|
+
threads: page.data.filter(thread => hostId === 'durable' ? thread.threadSource === 'aeon_child' : thread.threadSource !== 'aeon')
|
|
17
|
+
.map(thread => ({ id: thread.id, hostId, kind: 'codex', threadSource: thread.threadSource,
|
|
18
|
+
title: thread.name || thread.preview || '', projectId: group(thread, projects),
|
|
19
|
+
cwd: thread.cwd, updatedAt: thread.updatedAt, recencyAt: thread.recencyAt ?? thread.updatedAt,
|
|
20
|
+
status: typeof thread.status === 'string' ? thread.status : thread.status?.type || 'unknown',
|
|
21
|
+
activeFlags: Array.isArray(thread.status?.activeFlags) ? thread.status.activeFlags : [] })) };
|
|
22
|
+
}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
3
|
-
import { startWindowsProcess } from './platform/windows/background-process.mjs';
|
|
4
|
-
|
|
5
|
-
export const processAlive = pid => {
|
|
6
|
-
try { process.kill(pid, 0); return true; }
|
|
7
|
-
catch (error) { if (error.code === 'ESRCH') return false; throw error; }
|
|
8
|
-
};
|
|
9
|
-
export async function startBackground(executable, args, environment, logs = []) {
|
|
10
|
-
if (process.platform === 'win32') return startWindowsProcess(executable, args, environment, logs);
|
|
11
|
-
const descriptors = logs.map(file => fs.openSync(file, 'a'));
|
|
12
|
-
try {
|
|
13
|
-
const child = spawn(executable, args, { detached: true, env: environment,
|
|
14
|
-
stdio: ['ignore', descriptors[0] ?? 'ignore', descriptors[1] ?? 'ignore'] });
|
|
15
|
-
await new Promise((resolve, reject) => { child.once('spawn', resolve); child.once('error', reject); });
|
|
16
|
-
child.unref(); return child.pid;
|
|
17
|
-
} finally { descriptors.forEach(fd => fs.closeSync(fd)); }
|
|
18
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { startWindowsProcess } from './platform/windows/background-process.mjs';
|
|
4
|
+
|
|
5
|
+
export const processAlive = pid => {
|
|
6
|
+
try { process.kill(pid, 0); return true; }
|
|
7
|
+
catch (error) { if (error.code === 'ESRCH') return false; throw error; }
|
|
8
|
+
};
|
|
9
|
+
export async function startBackground(executable, args, environment, logs = []) {
|
|
10
|
+
if (process.platform === 'win32') return startWindowsProcess(executable, args, environment, logs);
|
|
11
|
+
const descriptors = logs.map(file => fs.openSync(file, 'a'));
|
|
12
|
+
try {
|
|
13
|
+
const child = spawn(executable, args, { detached: true, env: environment,
|
|
14
|
+
stdio: ['ignore', descriptors[0] ?? 'ignore', descriptors[1] ?? 'ignore'] });
|
|
15
|
+
await new Promise((resolve, reject) => { child.once('spawn', resolve); child.once('error', reject); });
|
|
16
|
+
child.unref(); return child.pid;
|
|
17
|
+
} finally { descriptors.forEach(fd => fs.closeSync(fd)); }
|
|
18
|
+
}
|
|
@@ -1,63 +1,63 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
|
|
3
|
-
export const processLogMaxBytes = 8 * 1024 * 1024;
|
|
4
|
-
const retryMs = 30_000;
|
|
5
|
-
|
|
6
|
-
// The broker drains output even when storage fails, so logging cannot crash
|
|
7
|
-
// the child or build an unbounded in-memory queue.
|
|
8
|
-
export function attachProcessLog(stream, file, { maxBytes = processLogMaxBytes, now = Date.now } = {}) {
|
|
9
|
-
let retryAt = 0, dropped = 0, lastError = '';
|
|
10
|
-
function append(bytes) {
|
|
11
|
-
let size = 0;
|
|
12
|
-
try { size = fs.statSync(file).size; }
|
|
13
|
-
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
14
|
-
// Apply the same retention bound to a log left by an earlier launch.
|
|
15
|
-
if (size > maxBytes) {
|
|
16
|
-
const fd = fs.openSync(file, 'r+');
|
|
17
|
-
try {
|
|
18
|
-
const tail = Buffer.alloc(maxBytes);
|
|
19
|
-
const count = fs.readSync(fd, tail, 0, maxBytes, size - maxBytes);
|
|
20
|
-
fs.ftruncateSync(fd, 0);
|
|
21
|
-
writeAll(fd, tail.subarray(0, count));
|
|
22
|
-
size = count;
|
|
23
|
-
} finally { fs.closeSync(fd); }
|
|
24
|
-
}
|
|
25
|
-
for (let offset = 0; offset < bytes.length;) {
|
|
26
|
-
if (size >= maxBytes) {
|
|
27
|
-
fs.rmSync(`${file}.2`, { force: true });
|
|
28
|
-
try { fs.renameSync(`${file}.1`, `${file}.2`); }
|
|
29
|
-
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
30
|
-
fs.renameSync(file, `${file}.1`);
|
|
31
|
-
size = 0;
|
|
32
|
-
}
|
|
33
|
-
const part = bytes.subarray(offset, offset + maxBytes - size);
|
|
34
|
-
const fd = fs.openSync(file, 'a');
|
|
35
|
-
try { writeAll(fd, part); } finally { fs.closeSync(fd); }
|
|
36
|
-
offset += part.length;
|
|
37
|
-
size += part.length;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
stream.on('data', chunk => {
|
|
41
|
-
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
42
|
-
if (now() < retryAt) { dropped += bytes.length; return; }
|
|
43
|
-
try {
|
|
44
|
-
if (dropped) append(Buffer.from(`\n[log recovered ${new Date(now()).toISOString()}; previous error: ${lastError}; discarded output bytes: ${dropped}]\n`));
|
|
45
|
-
append(bytes);
|
|
46
|
-
dropped = 0;
|
|
47
|
-
retryAt = 0;
|
|
48
|
-
} catch (error) {
|
|
49
|
-
dropped += bytes.length;
|
|
50
|
-
lastError = error.code || error.message;
|
|
51
|
-
retryAt = now() + retryMs;
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function writeAll(fd, bytes) {
|
|
57
|
-
let offset = 0;
|
|
58
|
-
while (offset < bytes.length) {
|
|
59
|
-
const count = fs.writeSync(fd, bytes, offset, bytes.length - offset);
|
|
60
|
-
if (!count) throw new Error('Log write made no progress.');
|
|
61
|
-
offset += count;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
|
|
3
|
+
export const processLogMaxBytes = 8 * 1024 * 1024;
|
|
4
|
+
const retryMs = 30_000;
|
|
5
|
+
|
|
6
|
+
// The broker drains output even when storage fails, so logging cannot crash
|
|
7
|
+
// the child or build an unbounded in-memory queue.
|
|
8
|
+
export function attachProcessLog(stream, file, { maxBytes = processLogMaxBytes, now = Date.now } = {}) {
|
|
9
|
+
let retryAt = 0, dropped = 0, lastError = '';
|
|
10
|
+
function append(bytes) {
|
|
11
|
+
let size = 0;
|
|
12
|
+
try { size = fs.statSync(file).size; }
|
|
13
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
14
|
+
// Apply the same retention bound to a log left by an earlier launch.
|
|
15
|
+
if (size > maxBytes) {
|
|
16
|
+
const fd = fs.openSync(file, 'r+');
|
|
17
|
+
try {
|
|
18
|
+
const tail = Buffer.alloc(maxBytes);
|
|
19
|
+
const count = fs.readSync(fd, tail, 0, maxBytes, size - maxBytes);
|
|
20
|
+
fs.ftruncateSync(fd, 0);
|
|
21
|
+
writeAll(fd, tail.subarray(0, count));
|
|
22
|
+
size = count;
|
|
23
|
+
} finally { fs.closeSync(fd); }
|
|
24
|
+
}
|
|
25
|
+
for (let offset = 0; offset < bytes.length;) {
|
|
26
|
+
if (size >= maxBytes) {
|
|
27
|
+
fs.rmSync(`${file}.2`, { force: true });
|
|
28
|
+
try { fs.renameSync(`${file}.1`, `${file}.2`); }
|
|
29
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
30
|
+
fs.renameSync(file, `${file}.1`);
|
|
31
|
+
size = 0;
|
|
32
|
+
}
|
|
33
|
+
const part = bytes.subarray(offset, offset + maxBytes - size);
|
|
34
|
+
const fd = fs.openSync(file, 'a');
|
|
35
|
+
try { writeAll(fd, part); } finally { fs.closeSync(fd); }
|
|
36
|
+
offset += part.length;
|
|
37
|
+
size += part.length;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
stream.on('data', chunk => {
|
|
41
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
42
|
+
if (now() < retryAt) { dropped += bytes.length; return; }
|
|
43
|
+
try {
|
|
44
|
+
if (dropped) append(Buffer.from(`\n[log recovered ${new Date(now()).toISOString()}; previous error: ${lastError}; discarded output bytes: ${dropped}]\n`));
|
|
45
|
+
append(bytes);
|
|
46
|
+
dropped = 0;
|
|
47
|
+
retryAt = 0;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
dropped += bytes.length;
|
|
50
|
+
lastError = error.code || error.message;
|
|
51
|
+
retryAt = now() + retryMs;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function writeAll(fd, bytes) {
|
|
57
|
+
let offset = 0;
|
|
58
|
+
while (offset < bytes.length) {
|
|
59
|
+
const count = fs.writeSync(fd, bytes, offset, bytes.length - offset);
|
|
60
|
+
if (!count) throw new Error('Log write made no progress.');
|
|
61
|
+
offset += count;
|
|
62
|
+
}
|
|
63
|
+
}
|