@walkhi/code-relax 0.1.0-beta.30 → 0.1.0-beta.32
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 +63 -58
- package/dist/bin/publish-harmony-release.mjs +46 -0
- package/dist/bin/release-upload-server.mjs +174 -0
- 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 +78 -74
- package/dist/shared/plan-pricing.cjs +32 -32
- package/dist/shared/question-attention.cjs +69 -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 +52 -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,164 +1,164 @@
|
|
|
1
|
-
// Event-driven subscription adapter. Normal desktop-tool polling remains in its own adapter.
|
|
2
|
-
import timelineSync from '../shared/timeline-sync.cjs';
|
|
3
|
-
export const threadListNotificationMethods = new Set([
|
|
4
|
-
'thread/started', 'thread/status/changed', 'thread/name/updated',
|
|
5
|
-
'thread/archived', 'thread/unarchived', 'thread/deleted',
|
|
6
|
-
'thread/project/updated', 'project/changed', 'turn/started', 'turn/completed',
|
|
7
|
-
]);
|
|
8
|
-
export async function streamSharedThread(response, threadId, client, normalize, requestedSync) {
|
|
9
|
-
response.startEvents();
|
|
10
|
-
let closed = false, timer, subscription, releaseActivity, streamReady = false, flushing = false;
|
|
11
|
-
let tracePending = false, pendingReset;
|
|
12
|
-
const pending = new Map(), sentTurns = new Map();
|
|
13
|
-
let fromSeq, toSeq, syncId = requestedSync?.id, epoch = requestedSync?.epoch;
|
|
14
|
-
let finish;
|
|
15
|
-
const done = new Promise(resolve => { finish = resolve; });
|
|
16
|
-
const emit = (name, value) => { if (!closed) return response.event(name, value); };
|
|
17
|
-
let historySync;
|
|
18
|
-
const publishHistorySync = () => {
|
|
19
|
-
if (typeof client.isHistorySyncing !== 'function') return;
|
|
20
|
-
const active = client.isHistorySyncing(threadId);
|
|
21
|
-
// End the notice only after the recovered body has been delivered, not
|
|
22
|
-
// when the upstream read finishes while its update is still being flushed.
|
|
23
|
-
if (!active && (!streamReady || flushing || pendingReset || pending.size)) return;
|
|
24
|
-
if (historySync === active) return;
|
|
25
|
-
historySync = active;
|
|
26
|
-
emit('history-sync', { threadId, active });
|
|
27
|
-
};
|
|
28
|
-
const flush = async () => {
|
|
29
|
-
clearTimeout(timer); timer = null;
|
|
30
|
-
const record = client.threads.get(threadId);
|
|
31
|
-
if (closed || flushing || !streamReady || !record || (!pendingReset && !pending.size)) return;
|
|
32
|
-
flushing = true;
|
|
33
|
-
try {
|
|
34
|
-
if (pendingReset) {
|
|
35
|
-
const snapshot = pendingReset;
|
|
36
|
-
pendingReset = undefined;
|
|
37
|
-
for (const [id, change] of pending) if (change.sequence <= snapshot.sync.seq) pending.delete(id);
|
|
38
|
-
fromSeq = pending.size ? Math.max(fromSeq, snapshot.sync.seq + 1) : undefined;
|
|
39
|
-
if (!pending.size) toSeq = undefined;
|
|
40
|
-
tracePending = [...pending.values()].some(change => change.immediate);
|
|
41
|
-
const normalized = await normalize(snapshot);
|
|
42
|
-
if (closed) return;
|
|
43
|
-
await emit('update', normalized);
|
|
44
|
-
syncId = normalized.sync.id; epoch = normalized.sync.epoch;
|
|
45
|
-
sentTurns.clear();
|
|
46
|
-
for (const turn of normalized.turns) if (!turn.cacheStub) sentTurns.set(turn.id, turn);
|
|
47
|
-
client.traceSync?.('stream-reset-sent', threadId, { seq: normalized.sync.seq,
|
|
48
|
-
turnOrder: normalized.turns.map(turn => turn.id) });
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
const batch = structuredClone([...pending.values()].map(change => change.turn));
|
|
52
|
-
const thread = { ...record.thread, turns: [] }, sync = { id: syncId, epoch, fromSeq, toSeq };
|
|
53
|
-
const turnOrder = record.thread.turns.map(turn => turn.id), retained = new Set(turnOrder);
|
|
54
|
-
const trace = tracePending;
|
|
55
|
-
pending.clear(); fromSeq = toSeq = undefined; tracePending = false;
|
|
56
|
-
const base = await normalize({ thread, sync });
|
|
57
|
-
const turns = [], turnPatches = [];
|
|
58
|
-
const normalizedTurns = [];
|
|
59
|
-
for (const turn of batch) {
|
|
60
|
-
if (turn.cacheStub) continue;
|
|
61
|
-
const normalized = (await normalize({ thread: { ...thread, turns: [turn] } })).turns[0];
|
|
62
|
-
const patch = timelineSync.diffTurn(sentTurns.get(turn.id), normalized);
|
|
63
|
-
if (patch) turnPatches.push(patch); else turns.push(normalized);
|
|
64
|
-
normalizedTurns.push(normalized);
|
|
65
|
-
}
|
|
66
|
-
if (closed) return;
|
|
67
|
-
await emit('update', { ...base, turns, ...(turnPatches.length ? { turnPatches } : {}) });
|
|
68
|
-
for (const turn of normalizedTurns) sentTurns.set(turn.id, turn);
|
|
69
|
-
for (const id of sentTurns.keys()) if (!retained.has(id)) sentTurns.delete(id);
|
|
70
|
-
if (trace || turns.length || (base.sync.fromSeq <= record.recoverAfter && record.recoverAfter <= base.sync.toSeq)) {
|
|
71
|
-
client.traceSync?.('stream-update-sent', threadId, { closed, fromSeq: base.sync?.fromSeq, toSeq: base.sync?.toSeq, turnOrder,
|
|
72
|
-
messages: turns.flatMap(turn => turn.entries.map(entry => [turn.id, entry.id])),
|
|
73
|
-
patches: turnPatches.map(patch => ({ turnId: patch.id, entries: (patch.entries || []).map(entry => entry.id), appended: (patch.appended || []).map(entry => entry.id) })) });
|
|
74
|
-
}
|
|
75
|
-
} catch (error) { emit('bridge-error', { error: error.message }); response.end(); finish(); }
|
|
76
|
-
finally {
|
|
77
|
-
flushing = false;
|
|
78
|
-
publishHistorySync();
|
|
79
|
-
if (!closed && (pendingReset || pending.size) && !timer) timer = setTimeout(flush, 0);
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
const onTurn = ({ thread, turn, sequence, immediate }) => {
|
|
83
|
-
if (thread.id !== threadId) return;
|
|
84
|
-
if (!fromSeq) fromSeq = sequence;
|
|
85
|
-
toSeq = sequence;
|
|
86
|
-
pending.set(turn.id, { turn, sequence, immediate: immediate || pending.get(turn.id)?.immediate });
|
|
87
|
-
if (immediate) tracePending = true;
|
|
88
|
-
if (immediate) flush(); else if (!timer) timer = setTimeout(flush, 50);
|
|
89
|
-
};
|
|
90
|
-
const onReset = snapshot => {
|
|
91
|
-
if (snapshot.thread.id !== threadId) return;
|
|
92
|
-
pendingReset = structuredClone(snapshot);
|
|
93
|
-
pendingReset.sync.reset = true;
|
|
94
|
-
flush();
|
|
95
|
-
};
|
|
96
|
-
const onSettings = id => { if (id === threadId) emit('settings', client.context(id)); };
|
|
97
|
-
const onRequests = id => { if (id === threadId) emit('approvals', { requests: client.serverRequests.list(threadId) }); };
|
|
98
|
-
const onQueue = id => { if (id === threadId) emit('queue-changed', {}); };
|
|
99
|
-
const onDisconnected = () => { emit('bridge-error', { error: 'Codex 内核连接已断开,请重新连接。' }); response.end(); finish(); };
|
|
100
|
-
const onInvalidated = id => { if (id === threadId) emit('history-invalidated', {}); };
|
|
101
|
-
const onHistorySync = id => { if (id === threadId) publishHistorySync(); };
|
|
102
|
-
const onChatError = value => { if (value.threadId === threadId) emit('chat-error', value); };
|
|
103
|
-
response.once('close', () => { closed = true; finish(); });
|
|
104
|
-
client.on('turn', onTurn); client.on('settings', onSettings); client.on('disconnected', onDisconnected);
|
|
105
|
-
client.on('timeline-reset', onReset);
|
|
106
|
-
client.on('queue-changed', onQueue);
|
|
107
|
-
client.on('requests-changed', onRequests);
|
|
108
|
-
client.on('history-invalidated', onInvalidated);
|
|
109
|
-
client.on('history-sync', onHistorySync);
|
|
110
|
-
client.on('chat-error', onChatError);
|
|
111
|
-
publishHistorySync();
|
|
112
|
-
try {
|
|
113
|
-
const started = performance.now();
|
|
114
|
-
client.traceSync?.('stream-open', threadId, { requestedSeq: requestedSync?.after });
|
|
115
|
-
subscription = await client.acquire(threadId, requestedSync);
|
|
116
|
-
syncId = subscription.record.sync.id; epoch = subscription.record.sync.epoch;
|
|
117
|
-
if (!closed) {
|
|
118
|
-
const initial = structuredClone(subscription.record), catchup = subscription.catchup && structuredClone(subscription.catchup);
|
|
119
|
-
if (pendingReset?.sync.seq <= initial.sync.seq) pendingReset = undefined;
|
|
120
|
-
// Only discard notifications certified by the acquired snapshot/catchup.
|
|
121
|
-
for (const [id, change] of pending) if (change.sequence <= initial.sync.seq) pending.delete(id);
|
|
122
|
-
fromSeq = pending.size ? Math.max(fromSeq, initial.sync.seq + 1) : undefined;
|
|
123
|
-
if (!pending.size) toSeq = undefined;
|
|
124
|
-
tracePending = [...pending.values()].some(change => change.immediate);
|
|
125
|
-
const normalized = await normalize(subscription.confirmed ? { ...initial, thread: { ...initial.thread, turns: [] } } : initial);
|
|
126
|
-
const delivered = subscription.snapshot ? normalized : catchup ? await normalize(catchup) : null;
|
|
127
|
-
const sent = Boolean(delivered) && !closed;
|
|
128
|
-
if (sent) await emit('update', delivered);
|
|
129
|
-
// A timestamp-confirmed cache only needs the new subscription boundary,
|
|
130
|
-
// never an empty replacement window or a repeated body snapshot.
|
|
131
|
-
if (subscription.confirmed && !closed) await emit('sync-ready', {
|
|
132
|
-
thread: normalized.thread, sync: normalized.sync,
|
|
133
|
-
});
|
|
134
|
-
client.traceSync?.('stream-initial-sent', threadId, { ms: Math.round(performance.now() - started), closed,
|
|
135
|
-
snapshot: subscription.snapshot, catchup: Boolean(catchup), sent, seq: normalized.sync?.seq,
|
|
136
|
-
deliveredSync: sent ? delivered.sync : undefined,
|
|
137
|
-
messages: sent ? delivered.turns.flatMap(turn => turn.entries.map(entry => [turn.id, entry.id])) : [],
|
|
138
|
-
windowMessages: normalized.turns.flatMap(turn => turn.entries.map(entry => [turn.id, entry.id])) });
|
|
139
|
-
for (const turn of normalized.turns) if (!turn.cacheStub) sentTurns.set(turn.id, turn);
|
|
140
|
-
emit('queue-changed', {});
|
|
141
|
-
onRequests(threadId);
|
|
142
|
-
streamReady = true;
|
|
143
|
-
publishHistorySync();
|
|
144
|
-
if (!closed && typeof client.acquireActivity === 'function') {
|
|
145
|
-
releaseActivity = client.acquireActivity(threadId, value => emit('dots-activity', value));
|
|
146
|
-
}
|
|
147
|
-
flush();
|
|
148
|
-
}
|
|
149
|
-
await done;
|
|
150
|
-
} catch (error) { emit('bridge-error', { error: error.message }); response.end(); }
|
|
151
|
-
finally {
|
|
152
|
-
client.traceSync?.('stream-close', threadId, { ready: streamReady });
|
|
153
|
-
clearTimeout(timer);
|
|
154
|
-
releaseActivity?.();
|
|
155
|
-
client.off('turn', onTurn); client.off('settings', onSettings); client.off('disconnected', onDisconnected);
|
|
156
|
-
client.off('timeline-reset', onReset);
|
|
157
|
-
client.off('queue-changed', onQueue);
|
|
158
|
-
client.off('requests-changed', onRequests);
|
|
159
|
-
client.off('history-invalidated', onInvalidated);
|
|
160
|
-
client.off('history-sync', onHistorySync);
|
|
161
|
-
client.off('chat-error', onChatError);
|
|
162
|
-
if (subscription) await subscription.release();
|
|
163
|
-
}
|
|
164
|
-
}
|
|
1
|
+
// Event-driven subscription adapter. Normal desktop-tool polling remains in its own adapter.
|
|
2
|
+
import timelineSync from '../shared/timeline-sync.cjs';
|
|
3
|
+
export const threadListNotificationMethods = new Set([
|
|
4
|
+
'thread/started', 'thread/status/changed', 'thread/name/updated',
|
|
5
|
+
'thread/archived', 'thread/unarchived', 'thread/deleted',
|
|
6
|
+
'thread/project/updated', 'project/changed', 'turn/started', 'turn/completed',
|
|
7
|
+
]);
|
|
8
|
+
export async function streamSharedThread(response, threadId, client, normalize, requestedSync) {
|
|
9
|
+
response.startEvents();
|
|
10
|
+
let closed = false, timer, subscription, releaseActivity, streamReady = false, flushing = false;
|
|
11
|
+
let tracePending = false, pendingReset;
|
|
12
|
+
const pending = new Map(), sentTurns = new Map();
|
|
13
|
+
let fromSeq, toSeq, syncId = requestedSync?.id, epoch = requestedSync?.epoch;
|
|
14
|
+
let finish;
|
|
15
|
+
const done = new Promise(resolve => { finish = resolve; });
|
|
16
|
+
const emit = (name, value) => { if (!closed) return response.event(name, value); };
|
|
17
|
+
let historySync;
|
|
18
|
+
const publishHistorySync = () => {
|
|
19
|
+
if (typeof client.isHistorySyncing !== 'function') return;
|
|
20
|
+
const active = client.isHistorySyncing(threadId);
|
|
21
|
+
// End the notice only after the recovered body has been delivered, not
|
|
22
|
+
// when the upstream read finishes while its update is still being flushed.
|
|
23
|
+
if (!active && (!streamReady || flushing || pendingReset || pending.size)) return;
|
|
24
|
+
if (historySync === active) return;
|
|
25
|
+
historySync = active;
|
|
26
|
+
emit('history-sync', { threadId, active });
|
|
27
|
+
};
|
|
28
|
+
const flush = async () => {
|
|
29
|
+
clearTimeout(timer); timer = null;
|
|
30
|
+
const record = client.threads.get(threadId);
|
|
31
|
+
if (closed || flushing || !streamReady || !record || (!pendingReset && !pending.size)) return;
|
|
32
|
+
flushing = true;
|
|
33
|
+
try {
|
|
34
|
+
if (pendingReset) {
|
|
35
|
+
const snapshot = pendingReset;
|
|
36
|
+
pendingReset = undefined;
|
|
37
|
+
for (const [id, change] of pending) if (change.sequence <= snapshot.sync.seq) pending.delete(id);
|
|
38
|
+
fromSeq = pending.size ? Math.max(fromSeq, snapshot.sync.seq + 1) : undefined;
|
|
39
|
+
if (!pending.size) toSeq = undefined;
|
|
40
|
+
tracePending = [...pending.values()].some(change => change.immediate);
|
|
41
|
+
const normalized = await normalize(snapshot);
|
|
42
|
+
if (closed) return;
|
|
43
|
+
await emit('update', normalized);
|
|
44
|
+
syncId = normalized.sync.id; epoch = normalized.sync.epoch;
|
|
45
|
+
sentTurns.clear();
|
|
46
|
+
for (const turn of normalized.turns) if (!turn.cacheStub) sentTurns.set(turn.id, turn);
|
|
47
|
+
client.traceSync?.('stream-reset-sent', threadId, { seq: normalized.sync.seq,
|
|
48
|
+
turnOrder: normalized.turns.map(turn => turn.id) });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const batch = structuredClone([...pending.values()].map(change => change.turn));
|
|
52
|
+
const thread = { ...record.thread, turns: [] }, sync = { id: syncId, epoch, fromSeq, toSeq };
|
|
53
|
+
const turnOrder = record.thread.turns.map(turn => turn.id), retained = new Set(turnOrder);
|
|
54
|
+
const trace = tracePending;
|
|
55
|
+
pending.clear(); fromSeq = toSeq = undefined; tracePending = false;
|
|
56
|
+
const base = await normalize({ thread, sync });
|
|
57
|
+
const turns = [], turnPatches = [];
|
|
58
|
+
const normalizedTurns = [];
|
|
59
|
+
for (const turn of batch) {
|
|
60
|
+
if (turn.cacheStub) continue;
|
|
61
|
+
const normalized = (await normalize({ thread: { ...thread, turns: [turn] } })).turns[0];
|
|
62
|
+
const patch = timelineSync.diffTurn(sentTurns.get(turn.id), normalized);
|
|
63
|
+
if (patch) turnPatches.push(patch); else turns.push(normalized);
|
|
64
|
+
normalizedTurns.push(normalized);
|
|
65
|
+
}
|
|
66
|
+
if (closed) return;
|
|
67
|
+
await emit('update', { ...base, turns, ...(turnPatches.length ? { turnPatches } : {}) });
|
|
68
|
+
for (const turn of normalizedTurns) sentTurns.set(turn.id, turn);
|
|
69
|
+
for (const id of sentTurns.keys()) if (!retained.has(id)) sentTurns.delete(id);
|
|
70
|
+
if (trace || turns.length || (base.sync.fromSeq <= record.recoverAfter && record.recoverAfter <= base.sync.toSeq)) {
|
|
71
|
+
client.traceSync?.('stream-update-sent', threadId, { closed, fromSeq: base.sync?.fromSeq, toSeq: base.sync?.toSeq, turnOrder,
|
|
72
|
+
messages: turns.flatMap(turn => turn.entries.map(entry => [turn.id, entry.id])),
|
|
73
|
+
patches: turnPatches.map(patch => ({ turnId: patch.id, entries: (patch.entries || []).map(entry => entry.id), appended: (patch.appended || []).map(entry => entry.id) })) });
|
|
74
|
+
}
|
|
75
|
+
} catch (error) { emit('bridge-error', { error: error.message }); response.end(); finish(); }
|
|
76
|
+
finally {
|
|
77
|
+
flushing = false;
|
|
78
|
+
publishHistorySync();
|
|
79
|
+
if (!closed && (pendingReset || pending.size) && !timer) timer = setTimeout(flush, 0);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const onTurn = ({ thread, turn, sequence, immediate }) => {
|
|
83
|
+
if (thread.id !== threadId) return;
|
|
84
|
+
if (!fromSeq) fromSeq = sequence;
|
|
85
|
+
toSeq = sequence;
|
|
86
|
+
pending.set(turn.id, { turn, sequence, immediate: immediate || pending.get(turn.id)?.immediate });
|
|
87
|
+
if (immediate) tracePending = true;
|
|
88
|
+
if (immediate) flush(); else if (!timer) timer = setTimeout(flush, 50);
|
|
89
|
+
};
|
|
90
|
+
const onReset = snapshot => {
|
|
91
|
+
if (snapshot.thread.id !== threadId) return;
|
|
92
|
+
pendingReset = structuredClone(snapshot);
|
|
93
|
+
pendingReset.sync.reset = true;
|
|
94
|
+
flush();
|
|
95
|
+
};
|
|
96
|
+
const onSettings = id => { if (id === threadId) emit('settings', client.context(id)); };
|
|
97
|
+
const onRequests = id => { if (id === threadId) emit('approvals', { requests: client.serverRequests.list(threadId) }); };
|
|
98
|
+
const onQueue = id => { if (id === threadId) emit('queue-changed', {}); };
|
|
99
|
+
const onDisconnected = () => { emit('bridge-error', { error: 'Codex 内核连接已断开,请重新连接。' }); response.end(); finish(); };
|
|
100
|
+
const onInvalidated = id => { if (id === threadId) emit('history-invalidated', {}); };
|
|
101
|
+
const onHistorySync = id => { if (id === threadId) publishHistorySync(); };
|
|
102
|
+
const onChatError = value => { if (value.threadId === threadId) emit('chat-error', value); };
|
|
103
|
+
response.once('close', () => { closed = true; finish(); });
|
|
104
|
+
client.on('turn', onTurn); client.on('settings', onSettings); client.on('disconnected', onDisconnected);
|
|
105
|
+
client.on('timeline-reset', onReset);
|
|
106
|
+
client.on('queue-changed', onQueue);
|
|
107
|
+
client.on('requests-changed', onRequests);
|
|
108
|
+
client.on('history-invalidated', onInvalidated);
|
|
109
|
+
client.on('history-sync', onHistorySync);
|
|
110
|
+
client.on('chat-error', onChatError);
|
|
111
|
+
publishHistorySync();
|
|
112
|
+
try {
|
|
113
|
+
const started = performance.now();
|
|
114
|
+
client.traceSync?.('stream-open', threadId, { requestedSeq: requestedSync?.after });
|
|
115
|
+
subscription = await client.acquire(threadId, requestedSync);
|
|
116
|
+
syncId = subscription.record.sync.id; epoch = subscription.record.sync.epoch;
|
|
117
|
+
if (!closed) {
|
|
118
|
+
const initial = structuredClone(subscription.record), catchup = subscription.catchup && structuredClone(subscription.catchup);
|
|
119
|
+
if (pendingReset?.sync.seq <= initial.sync.seq) pendingReset = undefined;
|
|
120
|
+
// Only discard notifications certified by the acquired snapshot/catchup.
|
|
121
|
+
for (const [id, change] of pending) if (change.sequence <= initial.sync.seq) pending.delete(id);
|
|
122
|
+
fromSeq = pending.size ? Math.max(fromSeq, initial.sync.seq + 1) : undefined;
|
|
123
|
+
if (!pending.size) toSeq = undefined;
|
|
124
|
+
tracePending = [...pending.values()].some(change => change.immediate);
|
|
125
|
+
const normalized = await normalize(subscription.confirmed ? { ...initial, thread: { ...initial.thread, turns: [] } } : initial);
|
|
126
|
+
const delivered = subscription.snapshot ? normalized : catchup ? await normalize(catchup) : null;
|
|
127
|
+
const sent = Boolean(delivered) && !closed;
|
|
128
|
+
if (sent) await emit('update', delivered);
|
|
129
|
+
// A timestamp-confirmed cache only needs the new subscription boundary,
|
|
130
|
+
// never an empty replacement window or a repeated body snapshot.
|
|
131
|
+
if (subscription.confirmed && !closed) await emit('sync-ready', {
|
|
132
|
+
thread: normalized.thread, sync: normalized.sync,
|
|
133
|
+
});
|
|
134
|
+
client.traceSync?.('stream-initial-sent', threadId, { ms: Math.round(performance.now() - started), closed,
|
|
135
|
+
snapshot: subscription.snapshot, catchup: Boolean(catchup), sent, seq: normalized.sync?.seq,
|
|
136
|
+
deliveredSync: sent ? delivered.sync : undefined,
|
|
137
|
+
messages: sent ? delivered.turns.flatMap(turn => turn.entries.map(entry => [turn.id, entry.id])) : [],
|
|
138
|
+
windowMessages: normalized.turns.flatMap(turn => turn.entries.map(entry => [turn.id, entry.id])) });
|
|
139
|
+
for (const turn of normalized.turns) if (!turn.cacheStub) sentTurns.set(turn.id, turn);
|
|
140
|
+
emit('queue-changed', {});
|
|
141
|
+
onRequests(threadId);
|
|
142
|
+
streamReady = true;
|
|
143
|
+
publishHistorySync();
|
|
144
|
+
if (!closed && typeof client.acquireActivity === 'function') {
|
|
145
|
+
releaseActivity = client.acquireActivity(threadId, value => emit('dots-activity', value));
|
|
146
|
+
}
|
|
147
|
+
flush();
|
|
148
|
+
}
|
|
149
|
+
await done;
|
|
150
|
+
} catch (error) { emit('bridge-error', { error: error.message }); response.end(); }
|
|
151
|
+
finally {
|
|
152
|
+
client.traceSync?.('stream-close', threadId, { ready: streamReady });
|
|
153
|
+
clearTimeout(timer);
|
|
154
|
+
releaseActivity?.();
|
|
155
|
+
client.off('turn', onTurn); client.off('settings', onSettings); client.off('disconnected', onDisconnected);
|
|
156
|
+
client.off('timeline-reset', onReset);
|
|
157
|
+
client.off('queue-changed', onQueue);
|
|
158
|
+
client.off('requests-changed', onRequests);
|
|
159
|
+
client.off('history-invalidated', onInvalidated);
|
|
160
|
+
client.off('history-sync', onHistorySync);
|
|
161
|
+
client.off('chat-error', onChatError);
|
|
162
|
+
if (subscription) await subscription.release();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import { createHash } from 'node:crypto';
|
|
5
|
-
import { readRecord, writeRecord } from './service-lifecycle.mjs';
|
|
6
|
-
|
|
7
|
-
const owner = '@walkhi/code-relax';
|
|
8
|
-
const markerName = '.code-relax-install.json';
|
|
9
|
-
const digest = value => createHash('sha256').update(value).digest('hex');
|
|
10
|
-
export const skillDirectory = (home = os.homedir()) => path.join(home, '.agents/skills/code-relax');
|
|
11
|
-
|
|
12
|
-
// Refuse junctions/symlinks along managed paths before writing or removing files.
|
|
13
|
-
export function assertPlainPath(target) {
|
|
14
|
-
for (let item = path.resolve(target); ; item = path.dirname(item)) {
|
|
15
|
-
try { if (fs.lstatSync(item).isSymbolicLink()) throw new Error(`不自动修改链接路径:${item}`); }
|
|
16
|
-
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
17
|
-
if (path.dirname(item) === item) break;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function inspectSkill(target = skillDirectory()) {
|
|
22
|
-
assertPlainPath(target);
|
|
23
|
-
if (!fs.existsSync(target)) return { installed: false, managed: false, target };
|
|
24
|
-
const markerPath = path.join(target, markerName), file = path.join(target, 'SKILL.md');
|
|
25
|
-
assertPlainPath(markerPath); assertPlainPath(file);
|
|
26
|
-
const marker = readRecord(markerPath);
|
|
27
|
-
const managed = marker?.owner === owner && fs.existsSync(file)
|
|
28
|
-
&& marker.sha256 === digest(fs.readFileSync(file))
|
|
29
|
-
&& fs.readdirSync(target).every(name => ['SKILL.md', markerName].includes(name));
|
|
30
|
-
return { installed: true, managed, target };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function installSkill(source, target = skillDirectory()) {
|
|
34
|
-
const state = inspectSkill(target);
|
|
35
|
-
if (state.installed && !state.managed) throw new Error(`同名 Skill 非本包管理或已被修改,请保留并手动处理:${target}`);
|
|
36
|
-
const content = fs.readFileSync(source);
|
|
37
|
-
fs.mkdirSync(target, { recursive: true });
|
|
38
|
-
fs.writeFileSync(path.join(target, 'SKILL.md'), content);
|
|
39
|
-
writeRecord(path.join(target, markerName), { owner, sha256: digest(content) });
|
|
40
|
-
return { installed: true, target, message: 'Skill 已安装/更新;若 Codex 尚未显示,请重新打开 Codex。' };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function removeSkill(target = skillDirectory()) {
|
|
44
|
-
const state = inspectSkill(target);
|
|
45
|
-
if (!state.installed) return { removed: false, target, message: 'Skill 未安装。' };
|
|
46
|
-
if (!state.managed) throw new Error(`同名 Skill 非本包管理或已被修改,未删除:${target}`);
|
|
47
|
-
fs.unlinkSync(path.join(target, 'SKILL.md'));
|
|
48
|
-
fs.unlinkSync(path.join(target, markerName));
|
|
49
|
-
fs.rmdirSync(target);
|
|
50
|
-
return { removed: true, target, message: '已移除本包安装的 Skill。' };
|
|
51
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { readRecord, writeRecord } from './service-lifecycle.mjs';
|
|
6
|
+
|
|
7
|
+
const owner = '@walkhi/code-relax';
|
|
8
|
+
const markerName = '.code-relax-install.json';
|
|
9
|
+
const digest = value => createHash('sha256').update(value).digest('hex');
|
|
10
|
+
export const skillDirectory = (home = os.homedir()) => path.join(home, '.agents/skills/code-relax');
|
|
11
|
+
|
|
12
|
+
// Refuse junctions/symlinks along managed paths before writing or removing files.
|
|
13
|
+
export function assertPlainPath(target) {
|
|
14
|
+
for (let item = path.resolve(target); ; item = path.dirname(item)) {
|
|
15
|
+
try { if (fs.lstatSync(item).isSymbolicLink()) throw new Error(`不自动修改链接路径:${item}`); }
|
|
16
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
17
|
+
if (path.dirname(item) === item) break;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function inspectSkill(target = skillDirectory()) {
|
|
22
|
+
assertPlainPath(target);
|
|
23
|
+
if (!fs.existsSync(target)) return { installed: false, managed: false, target };
|
|
24
|
+
const markerPath = path.join(target, markerName), file = path.join(target, 'SKILL.md');
|
|
25
|
+
assertPlainPath(markerPath); assertPlainPath(file);
|
|
26
|
+
const marker = readRecord(markerPath);
|
|
27
|
+
const managed = marker?.owner === owner && fs.existsSync(file)
|
|
28
|
+
&& marker.sha256 === digest(fs.readFileSync(file))
|
|
29
|
+
&& fs.readdirSync(target).every(name => ['SKILL.md', markerName].includes(name));
|
|
30
|
+
return { installed: true, managed, target };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function installSkill(source, target = skillDirectory()) {
|
|
34
|
+
const state = inspectSkill(target);
|
|
35
|
+
if (state.installed && !state.managed) throw new Error(`同名 Skill 非本包管理或已被修改,请保留并手动处理:${target}`);
|
|
36
|
+
const content = fs.readFileSync(source);
|
|
37
|
+
fs.mkdirSync(target, { recursive: true });
|
|
38
|
+
fs.writeFileSync(path.join(target, 'SKILL.md'), content);
|
|
39
|
+
writeRecord(path.join(target, markerName), { owner, sha256: digest(content) });
|
|
40
|
+
return { installed: true, target, message: 'Skill 已安装/更新;若 Codex 尚未显示,请重新打开 Codex。' };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function removeSkill(target = skillDirectory()) {
|
|
44
|
+
const state = inspectSkill(target);
|
|
45
|
+
if (!state.installed) return { removed: false, target, message: 'Skill 未安装。' };
|
|
46
|
+
if (!state.managed) throw new Error(`同名 Skill 非本包管理或已被修改,未删除:${target}`);
|
|
47
|
+
fs.unlinkSync(path.join(target, 'SKILL.md'));
|
|
48
|
+
fs.unlinkSync(path.join(target, markerName));
|
|
49
|
+
fs.rmdirSync(target);
|
|
50
|
+
return { removed: true, target, message: '已移除本包安装的 Skill。' };
|
|
51
|
+
}
|