@walkhi/code-relax 0.1.0-beta.50 → 0.1.0-beta.51

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.
Files changed (89) hide show
  1. package/README.md +105 -105
  2. package/dist/bin/cleanup-accounts.mjs +63 -63
  3. package/dist/bin/codex-remote.mjs +403 -403
  4. package/dist/bin/publish-android-release.mjs +63 -63
  5. package/dist/bin/release-upload-server.mjs +190 -190
  6. package/dist/build-platform.json +1 -1
  7. package/dist/get-shared-service-url.ps1 +14 -14
  8. package/dist/shared/code-browser.cjs +159 -159
  9. package/dist/shared/file-changes.cjs +31 -31
  10. package/dist/shared/file-citations.cjs +117 -117
  11. package/dist/shared/file-mentions.cjs +92 -92
  12. package/dist/shared/message-delivery.cjs +115 -115
  13. package/dist/shared/p2p-file-channel.cjs +87 -87
  14. package/dist/shared/server-requests.cjs +311 -311
  15. package/dist/shared/timeline-sync.cjs +173 -173
  16. package/dist/skills/code-relax/SKILL.md +22 -22
  17. package/dist/src/bounded-process-log.mjs +63 -63
  18. package/dist/src/celsius-client.mjs +107 -107
  19. package/dist/src/dots-approval-timeline.mjs +99 -99
  20. package/dist/src/dots-avatar.mjs +42 -42
  21. package/dist/src/dots-chat-projection.mjs +50 -50
  22. package/dist/src/dots-chat-server.mjs +483 -483
  23. package/dist/src/dots-message-images.mjs +319 -319
  24. package/dist/src/dots-profile.mjs +77 -77
  25. package/dist/src/dots-room-activity.mjs +172 -172
  26. package/dist/src/dots-room-client.mjs +135 -135
  27. package/dist/src/dots-room-projection.mjs +94 -94
  28. package/dist/src/dots-schedules.mjs +66 -66
  29. package/dist/src/dots-summary-tasks.mjs +32 -32
  30. package/dist/src/dots-task-catalog.mjs +430 -430
  31. package/dist/src/file-transfer-range.mjs +22 -22
  32. package/dist/src/file-upload-session.mjs +130 -130
  33. package/dist/src/hosted-history-app-server.mjs +103 -103
  34. package/dist/src/hosted-history-cache.mjs +113 -113
  35. package/dist/src/lan-socket-server.mjs +266 -266
  36. package/dist/src/managed-app-server.mjs +373 -373
  37. package/dist/src/managed-endpoint.mjs +25 -25
  38. package/dist/src/message-upload-files.mjs +109 -109
  39. package/dist/src/onboarding.mjs +12 -12
  40. package/dist/src/platform/README.md +1 -1
  41. package/dist/src/platform/codex-protocol.mjs +46 -46
  42. package/dist/src/platform/desktop-project-store.mjs +208 -208
  43. package/dist/src/platform/linux/desktop-host.mjs +87 -87
  44. package/dist/src/platform/linux/process.mjs +53 -53
  45. package/dist/src/platform/linux/system.mjs +50 -50
  46. package/dist/src/platform/linux/user-environment.mjs +9 -9
  47. package/dist/src/platform/macos/codex-runtime.mjs +125 -125
  48. package/dist/src/platform/macos/desktop-host.mjs +86 -86
  49. package/dist/src/platform/macos/user-environment.mjs +9 -9
  50. package/dist/src/platform/native-package.mjs +58 -58
  51. package/dist/src/platform/windows/activate-desktop.ps1 +49 -49
  52. package/dist/src/platform/windows/codex-runtime.mjs +215 -215
  53. package/dist/src/platform/windows/credential-store.mjs +27 -27
  54. package/dist/src/platform/windows/credential-store.ps1 +49 -49
  55. package/dist/src/platform/windows/desktop-host.mjs +65 -65
  56. package/dist/src/platform/windows/desktop-info.ps1 +38 -38
  57. package/dist/src/platform/windows/desktop-project-registration.mjs +4 -4
  58. package/dist/src/platform/windows/desktop-shortcuts.ps1 +14 -14
  59. package/dist/src/platform/windows/dots-profile.mjs +49 -49
  60. package/dist/src/platform/windows/launch-worker.mjs +41 -41
  61. package/dist/src/platform/windows/project-locations.mjs +16 -16
  62. package/dist/src/platform/windows/service-host.mjs +35 -35
  63. package/dist/src/platform/windows/user-environment.mjs +52 -52
  64. package/dist/src/project-management.mjs +58 -58
  65. package/dist/src/runtime-discovery.mjs +47 -47
  66. package/dist/src/self-relay/account-cleanup.mjs +30 -30
  67. package/dist/src/self-relay/admin-page.mjs +80 -80
  68. package/dist/src/self-relay/admin-style.mjs +14 -14
  69. package/dist/src/self-relay/connector.mjs +439 -439
  70. package/dist/src/self-relay/demo.mjs +31 -31
  71. package/dist/src/self-relay/diagnostics.mjs +54 -54
  72. package/dist/src/self-relay/email-accounts.mjs +183 -183
  73. package/dist/src/self-relay/file-wire.mjs +13 -13
  74. package/dist/src/self-relay/p2p-probe.mjs +168 -168
  75. package/dist/src/self-relay/privacy-page.mjs +26 -26
  76. package/dist/src/self-relay/public-releases.mjs +67 -67
  77. package/dist/src/self-relay/rtc-file-bridge.mjs +41 -41
  78. package/dist/src/self-relay/usage-meter.mjs +141 -141
  79. package/dist/src/server.mjs +5 -4
  80. package/dist/src/service-doctor.mjs +148 -148
  81. package/dist/src/service-install.mjs +97 -97
  82. package/dist/src/service-lifecycle.mjs +139 -139
  83. package/dist/src/shared-app-server.mjs +666 -666
  84. package/dist/src/shared-catalog.mjs +132 -132
  85. package/dist/src/shared-thread-stream.mjs +165 -165
  86. package/dist/src/usage-reset.mjs +17 -17
  87. package/dist/src/workstation-downloads.mjs +120 -120
  88. package/package.json +4 -4
  89. package/tools/postinstall.mjs +186 -186
@@ -1,132 +1,132 @@
1
- import events from '../shared/app-server-events.cjs';
2
- import { projectSummary } from './project-management.mjs';
3
-
4
- export function sharedThreadProjectId(thread, projects) {
5
- return events.catalogThreadProjectId(thread, projects);
6
- }
7
-
8
- export function dotsThreadProjectId(thread, projects) {
9
- if (thread.projectId && projects.some(project => project.id === thread.projectId)) return thread.projectId;
10
- if (thread.environments?.length) {
11
- for (const environment of thread.environments) {
12
- const roots = Array.isArray(environment.runtimeWorkspaceRoots) ? environment.runtimeWorkspaceRoots : [];
13
- for (const root of roots) {
14
- if (!root) continue;
15
- const projectId = events.threadProjectId({ cwd: root }, projects);
16
- if (projectId) return projectId;
17
- }
18
- }
19
- // Display grouping may use the execution directory when workspace roots
20
- // do not map to a known project. This never changes the thread's workspace
21
- // permissions or its native project assignment.
22
- for (const environment of thread.environments) {
23
- if (!environment.cwd) continue;
24
- const projectId = events.threadProjectId({ cwd: environment.cwd }, projects);
25
- if (projectId) return projectId;
26
- }
27
- }
28
- return events.threadProjectId({ cwd: thread.cwd }, projects);
29
- }
30
-
31
- // Mobile catalog order comes entirely from app-server authority, independent of Desktop UI state.
32
- export class SharedCatalog {
33
- constructor(server, dotsServer, dotsChat) {
34
- this.server = server; this.dotsServer = dotsServer; this.dotsChat = dotsChat;
35
- this.states = new Map();
36
- dotsServer?.on('disconnected', () => this.states.clear());
37
- }
38
-
39
- threadState(thread, refresh = false) {
40
- const client = thread.threadSource === 'aeon' ? this.dotsChat : this.dotsServer;
41
- const observed = client?.threads.get(thread.id);
42
- if (client?.ready && observed && !observed.needsReload && !observed.hydrating && !observed.refresh) {
43
- const current = observed.thread;
44
- return { chatRevision: current.chatRevision,
45
- ...(thread.threadSource === 'aeon' ? { chatMessageRevision: current.chatMessageRevision, updatedAt: current.updatedAt }
46
- : { lastTurnStatus: current.turns.at(-1)?.status || null }) };
47
- }
48
- const identity = this.dotsServer.profileIdentity;
49
- let entry = this.states.get(thread.id);
50
- const version = JSON.stringify([thread.updatedAt, thread.status]);
51
- if (!entry) { entry = { value: {} }; this.states.set(thread.id, entry); }
52
- if (!entry.work && (refresh || entry.version !== version || Date.now() - (entry.at || 0) >= 30000)) {
53
- const work = (async () => {
54
- try {
55
- const value = thread.threadSource === 'aeon'
56
- ? await this.dotsChat.latestChatState(thread.id)
57
- : await this.dotsServer.latestTaskState(thread.id, refresh || entry.version !== version);
58
- if (identity !== this.dotsServer.profileIdentity || this.states.get(thread.id) !== entry) return;
59
- const changed = JSON.stringify(entry.value) !== JSON.stringify(value);
60
- entry.value = value; entry.version = version; entry.at = Date.now();
61
- if (changed) this.dotsServer.emit('notification', {
62
- method: 'thread/status/changed', params: { threadId: thread.id, ...value },
63
- });
64
- } catch (error) {
65
- entry.version = version; entry.at = Date.now();
66
- console.error('云端目录修订读取失败:', thread.id, error.message);
67
- }
68
- })().finally(() => { if (entry.work === work) entry.work = null; });
69
- entry.work = work;
70
- }
71
- // The live chat updates its revision before notifying stream listeners.
72
- // A directory refresh can still be in flight with an older cached value;
73
- // never return that value after the rendered timeline has advanced.
74
- if (thread.threadSource === 'aeon') {
75
- const current = this.dotsChat.threads.get(thread.id)?.thread;
76
- return current?.chatRevision === undefined ? {} : {
77
- chatRevision: current.chatRevision, chatMessageRevision: current.chatMessageRevision, updatedAt: current.updatedAt,
78
- };
79
- }
80
- return entry.value;
81
- }
82
-
83
- async projects() {
84
- const native = await this.server.projects();
85
- const projects = native.map(projectSummary);
86
- return { projects, native };
87
- }
88
-
89
- async threads(limit, archived = false, scope = 'all', refresh = false) {
90
- const { projects, native } = await this.projects();
91
- const threads = []; let cursor;
92
- if (scope !== 'durable') do {
93
- const page = await this.server.request('thread/list', { limit: Math.min(100, limit - threads.length),
94
- sortKey: 'recency_at', sortDirection: 'desc', archived, ...(cursor ? { cursor } : {}) });
95
- threads.push(...page.data); cursor = page.nextCursor;
96
- } while (cursor && threads.length < limit);
97
- const summaries = threads.map(thread => ({ id: thread.id, kind: 'codex', hostId: 'local',
98
- ...(thread.model ? { model: thread.model, reasoningEffort: thread.reasoningEffort } : {}),
99
- title: thread.name || thread.preview || '', cwd: thread.cwd,
100
- projectId: sharedThreadProjectId(thread, native), updatedAt: thread.updatedAt,
101
- recencyAt: thread.recencyAt ?? thread.updatedAt,
102
- status: typeof thread.status === 'string' ? thread.status : thread.status?.type || 'unknown',
103
- activeFlags: threadActiveFlags(thread.status) }));
104
- let catalogError;
105
- if (scope !== 'local' && this.dotsServer) {
106
- try {
107
- const dots = await this.dotsServer.catalog(archived, refresh);
108
- summaries.push(...dots.map(thread => ({ id: thread.id, kind: thread.threadSource === 'aeon' ? 'dots' : 'codex', hostId: 'durable',
109
- threadSource: thread.threadSource,
110
- ...(thread.threadSource === 'aeon_child' && thread.model ? { model: thread.model, reasoningEffort: thread.reasoningEffort } : {}),
111
- ...this.threadState(thread, refresh),
112
- ...(thread.threadSource === 'aeon' ? { avatar: thread.avatar || '', color: thread.color || '' } : {}),
113
- title: thread.name || (thread.threadSource === 'aeon' ? 'Dots' : thread.preview || ''), cwd: thread.cwd,
114
- projectId: thread.threadSource === 'aeon' ? null : dotsThreadProjectId(thread, native),
115
- updatedAt: thread.threadSource === 'aeon' ? this.dotsChat.threads.get(thread.id)?.thread.updatedAt || 0 : thread.updatedAt,
116
- recencyAt: thread.recencyAt ?? thread.updatedAt,
117
- status: thread.threadSource === 'aeon' ? this.dotsChat?.threads.get(thread.id)?.thread.status || 'unknown'
118
- : typeof thread.status === 'string' ? thread.status : thread.status?.type || 'unknown',
119
- activeFlags: thread.threadSource === 'aeon' ? [] : threadActiveFlags(thread.status) })));
120
- } catch (error) { catalogError = `Dots 对话读取失败:${error.message}`; }
121
- }
122
- summaries.sort((a, b) => b.recencyAt - a.recencyAt);
123
- return { catalogScope: scope, projects, pinnedThreads: [], threads: [...summaries.filter(thread => thread.kind === 'dots'),
124
- ...summaries.filter(thread => thread.kind !== 'dots').slice(0, limit)], ...(catalogError ? { catalogError } : {}) };
125
- }
126
- }
127
-
128
- function threadActiveFlags(status) {
129
- return status && typeof status === 'object' && Array.isArray(status.activeFlags)
130
- ? status.activeFlags.filter(flag => typeof flag === 'string')
131
- : [];
132
- }
1
+ import events from '../shared/app-server-events.cjs';
2
+ import { projectSummary } from './project-management.mjs';
3
+
4
+ export function sharedThreadProjectId(thread, projects) {
5
+ return events.catalogThreadProjectId(thread, projects);
6
+ }
7
+
8
+ export function dotsThreadProjectId(thread, projects) {
9
+ if (thread.projectId && projects.some(project => project.id === thread.projectId)) return thread.projectId;
10
+ if (thread.environments?.length) {
11
+ for (const environment of thread.environments) {
12
+ const roots = Array.isArray(environment.runtimeWorkspaceRoots) ? environment.runtimeWorkspaceRoots : [];
13
+ for (const root of roots) {
14
+ if (!root) continue;
15
+ const projectId = events.threadProjectId({ cwd: root }, projects);
16
+ if (projectId) return projectId;
17
+ }
18
+ }
19
+ // Display grouping may use the execution directory when workspace roots
20
+ // do not map to a known project. This never changes the thread's workspace
21
+ // permissions or its native project assignment.
22
+ for (const environment of thread.environments) {
23
+ if (!environment.cwd) continue;
24
+ const projectId = events.threadProjectId({ cwd: environment.cwd }, projects);
25
+ if (projectId) return projectId;
26
+ }
27
+ }
28
+ return events.threadProjectId({ cwd: thread.cwd }, projects);
29
+ }
30
+
31
+ // Mobile catalog order comes entirely from app-server authority, independent of Desktop UI state.
32
+ export class SharedCatalog {
33
+ constructor(server, dotsServer, dotsChat) {
34
+ this.server = server; this.dotsServer = dotsServer; this.dotsChat = dotsChat;
35
+ this.states = new Map();
36
+ dotsServer?.on('disconnected', () => this.states.clear());
37
+ }
38
+
39
+ threadState(thread, refresh = false) {
40
+ const client = thread.threadSource === 'aeon' ? this.dotsChat : this.dotsServer;
41
+ const observed = client?.threads.get(thread.id);
42
+ if (client?.ready && observed && !observed.needsReload && !observed.hydrating && !observed.refresh) {
43
+ const current = observed.thread;
44
+ return { chatRevision: current.chatRevision,
45
+ ...(thread.threadSource === 'aeon' ? { chatMessageRevision: current.chatMessageRevision, updatedAt: current.updatedAt }
46
+ : { lastTurnStatus: current.turns.at(-1)?.status || null }) };
47
+ }
48
+ const identity = this.dotsServer.profileIdentity;
49
+ let entry = this.states.get(thread.id);
50
+ const version = JSON.stringify([thread.updatedAt, thread.status]);
51
+ if (!entry) { entry = { value: {} }; this.states.set(thread.id, entry); }
52
+ if (!entry.work && (refresh || entry.version !== version || Date.now() - (entry.at || 0) >= 30000)) {
53
+ const work = (async () => {
54
+ try {
55
+ const value = thread.threadSource === 'aeon'
56
+ ? await this.dotsChat.latestChatState(thread.id)
57
+ : await this.dotsServer.latestTaskState(thread.id, refresh || entry.version !== version);
58
+ if (identity !== this.dotsServer.profileIdentity || this.states.get(thread.id) !== entry) return;
59
+ const changed = JSON.stringify(entry.value) !== JSON.stringify(value);
60
+ entry.value = value; entry.version = version; entry.at = Date.now();
61
+ if (changed) this.dotsServer.emit('notification', {
62
+ method: 'thread/status/changed', params: { threadId: thread.id, ...value },
63
+ });
64
+ } catch (error) {
65
+ entry.version = version; entry.at = Date.now();
66
+ console.error('云端目录修订读取失败:', thread.id, error.message);
67
+ }
68
+ })().finally(() => { if (entry.work === work) entry.work = null; });
69
+ entry.work = work;
70
+ }
71
+ // The live chat updates its revision before notifying stream listeners.
72
+ // A directory refresh can still be in flight with an older cached value;
73
+ // never return that value after the rendered timeline has advanced.
74
+ if (thread.threadSource === 'aeon') {
75
+ const current = this.dotsChat.threads.get(thread.id)?.thread;
76
+ return current?.chatRevision === undefined ? {} : {
77
+ chatRevision: current.chatRevision, chatMessageRevision: current.chatMessageRevision, updatedAt: current.updatedAt,
78
+ };
79
+ }
80
+ return entry.value;
81
+ }
82
+
83
+ async projects() {
84
+ const native = await this.server.projects();
85
+ const projects = native.map(projectSummary);
86
+ return { projects, native };
87
+ }
88
+
89
+ async threads(limit, archived = false, scope = 'all', refresh = false) {
90
+ const { projects, native } = await this.projects();
91
+ const threads = []; let cursor;
92
+ if (scope !== 'durable') do {
93
+ const page = await this.server.request('thread/list', { limit: Math.min(100, limit - threads.length),
94
+ sortKey: 'recency_at', sortDirection: 'desc', archived, ...(cursor ? { cursor } : {}) });
95
+ threads.push(...page.data); cursor = page.nextCursor;
96
+ } while (cursor && threads.length < limit);
97
+ const summaries = threads.map(thread => ({ id: thread.id, kind: 'codex', hostId: 'local',
98
+ ...(thread.model ? { model: thread.model, reasoningEffort: thread.reasoningEffort } : {}),
99
+ title: thread.name || thread.preview || '', cwd: thread.cwd,
100
+ projectId: sharedThreadProjectId(thread, native), updatedAt: thread.updatedAt,
101
+ recencyAt: thread.recencyAt ?? thread.updatedAt,
102
+ status: typeof thread.status === 'string' ? thread.status : thread.status?.type || 'unknown',
103
+ activeFlags: threadActiveFlags(thread.status) }));
104
+ let catalogError;
105
+ if (scope !== 'local' && this.dotsServer) {
106
+ try {
107
+ const dots = await this.dotsServer.catalog(archived, refresh);
108
+ summaries.push(...dots.map(thread => ({ id: thread.id, kind: thread.threadSource === 'aeon' ? 'dots' : 'codex', hostId: 'durable',
109
+ threadSource: thread.threadSource,
110
+ ...(thread.threadSource === 'aeon_child' && thread.model ? { model: thread.model, reasoningEffort: thread.reasoningEffort } : {}),
111
+ ...this.threadState(thread, refresh),
112
+ ...(thread.threadSource === 'aeon' ? { avatar: thread.avatar || '', color: thread.color || '' } : {}),
113
+ title: thread.name || (thread.threadSource === 'aeon' ? 'Dots' : thread.preview || ''), cwd: thread.cwd,
114
+ projectId: thread.threadSource === 'aeon' ? null : dotsThreadProjectId(thread, native),
115
+ updatedAt: thread.threadSource === 'aeon' ? this.dotsChat.threads.get(thread.id)?.thread.updatedAt || 0 : thread.updatedAt,
116
+ recencyAt: thread.recencyAt ?? thread.updatedAt,
117
+ status: thread.threadSource === 'aeon' ? this.dotsChat?.threads.get(thread.id)?.thread.status || 'unknown'
118
+ : typeof thread.status === 'string' ? thread.status : thread.status?.type || 'unknown',
119
+ activeFlags: thread.threadSource === 'aeon' ? [] : threadActiveFlags(thread.status) })));
120
+ } catch (error) { catalogError = `Dots 对话读取失败:${error.message}`; }
121
+ }
122
+ summaries.sort((a, b) => b.recencyAt - a.recencyAt);
123
+ return { catalogScope: scope, projects, pinnedThreads: [], threads: [...summaries.filter(thread => thread.kind === 'dots'),
124
+ ...summaries.filter(thread => thread.kind !== 'dots').slice(0, limit)], ...(catalogError ? { catalogError } : {}) };
125
+ }
126
+ }
127
+
128
+ function threadActiveFlags(status) {
129
+ return status && typeof status === 'object' && Array.isArray(status.activeFlags)
130
+ ? status.activeFlags.filter(flag => typeof flag === 'string')
131
+ : [];
132
+ }
@@ -1,165 +1,165 @@
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
- // Confirm every handoff after its catchup/snapshot has been delivered.
130
- // This also releases a cached selection with a valid, unchanged token;
131
- // transport subscription alone does not confirm the message boundary.
132
- if (!closed) await emit('sync-ready', {
133
- thread: { ...normalized.thread, turns: [] }, sync: normalized.sync,
134
- });
135
- client.traceSync?.('stream-initial-sent', threadId, { ms: Math.round(performance.now() - started), closed,
136
- snapshot: subscription.snapshot, catchup: Boolean(catchup), sent, seq: normalized.sync?.seq,
137
- deliveredSync: sent ? delivered.sync : undefined,
138
- messages: sent ? delivered.turns.flatMap(turn => turn.entries.map(entry => [turn.id, entry.id])) : [],
139
- windowMessages: normalized.turns.flatMap(turn => turn.entries.map(entry => [turn.id, entry.id])) });
140
- for (const turn of normalized.turns) if (!turn.cacheStub) sentTurns.set(turn.id, turn);
141
- emit('queue-changed', {});
142
- onRequests(threadId);
143
- streamReady = true;
144
- publishHistorySync();
145
- if (!closed && typeof client.acquireActivity === 'function') {
146
- releaseActivity = client.acquireActivity(threadId, value => emit('dots-activity', value));
147
- }
148
- flush();
149
- }
150
- await done;
151
- } catch (error) { emit('bridge-error', { error: error.message }); response.end(); }
152
- finally {
153
- client.traceSync?.('stream-close', threadId, { ready: streamReady });
154
- clearTimeout(timer);
155
- releaseActivity?.();
156
- client.off('turn', onTurn); client.off('settings', onSettings); client.off('disconnected', onDisconnected);
157
- client.off('timeline-reset', onReset);
158
- client.off('queue-changed', onQueue);
159
- client.off('requests-changed', onRequests);
160
- client.off('history-invalidated', onInvalidated);
161
- client.off('history-sync', onHistorySync);
162
- client.off('chat-error', onChatError);
163
- if (subscription) await subscription.release();
164
- }
165
- }
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
+ // Confirm every handoff after its catchup/snapshot has been delivered.
130
+ // This also releases a cached selection with a valid, unchanged token;
131
+ // transport subscription alone does not confirm the message boundary.
132
+ if (!closed) await emit('sync-ready', {
133
+ thread: { ...normalized.thread, turns: [] }, sync: normalized.sync,
134
+ });
135
+ client.traceSync?.('stream-initial-sent', threadId, { ms: Math.round(performance.now() - started), closed,
136
+ snapshot: subscription.snapshot, catchup: Boolean(catchup), sent, seq: normalized.sync?.seq,
137
+ deliveredSync: sent ? delivered.sync : undefined,
138
+ messages: sent ? delivered.turns.flatMap(turn => turn.entries.map(entry => [turn.id, entry.id])) : [],
139
+ windowMessages: normalized.turns.flatMap(turn => turn.entries.map(entry => [turn.id, entry.id])) });
140
+ for (const turn of normalized.turns) if (!turn.cacheStub) sentTurns.set(turn.id, turn);
141
+ emit('queue-changed', {});
142
+ onRequests(threadId);
143
+ streamReady = true;
144
+ publishHistorySync();
145
+ if (!closed && typeof client.acquireActivity === 'function') {
146
+ releaseActivity = client.acquireActivity(threadId, value => emit('dots-activity', value));
147
+ }
148
+ flush();
149
+ }
150
+ await done;
151
+ } catch (error) { emit('bridge-error', { error: error.message }); response.end(); }
152
+ finally {
153
+ client.traceSync?.('stream-close', threadId, { ready: streamReady });
154
+ clearTimeout(timer);
155
+ releaseActivity?.();
156
+ client.off('turn', onTurn); client.off('settings', onSettings); client.off('disconnected', onDisconnected);
157
+ client.off('timeline-reset', onReset);
158
+ client.off('queue-changed', onQueue);
159
+ client.off('requests-changed', onRequests);
160
+ client.off('history-invalidated', onInvalidated);
161
+ client.off('history-sync', onHistorySync);
162
+ client.off('chat-error', onChatError);
163
+ if (subscription) await subscription.release();
164
+ }
165
+ }
@@ -1,17 +1,17 @@
1
- export async function consumeUsageReset(client, body) {
2
- if (typeof body?.idempotencyKey !== 'string' || !body.idempotencyKey.trim() || body.idempotencyKey.length > 200
3
- || typeof body.creditId !== 'string' || !body.creditId.trim() || body.creditId.length > 200) {
4
- throw Object.assign(new Error('请选择重置卡并确认使用。'), { statusCode: 400, code: 'usage_reset_not_submitted' });
5
- }
6
- let usage;
7
- try { usage = await client.usage(); }
8
- catch (error) { throw Object.assign(new Error('用量尚未确认,请恢复连接后重新选择重置卡。', { cause: error }),
9
- { statusCode: 503, code: 'usage_reset_not_submitted' }); }
10
- if ((usage.accountId || null) !== (body.accountId || null)) {
11
- throw Object.assign(new Error('Codex 账号已变化,请重新打开用量详情。'), { statusCode: 409, code: 'usage_reset_not_submitted' });
12
- }
13
- // The core owns eligibility and the idempotent redemption outcome.
14
- return client.request('account/rateLimitResetCredit/consume', {
15
- idempotencyKey: body.idempotencyKey, creditId: body.creditId,
16
- });
17
- }
1
+ export async function consumeUsageReset(client, body) {
2
+ if (typeof body?.idempotencyKey !== 'string' || !body.idempotencyKey.trim() || body.idempotencyKey.length > 200
3
+ || typeof body.creditId !== 'string' || !body.creditId.trim() || body.creditId.length > 200) {
4
+ throw Object.assign(new Error('请选择重置卡并确认使用。'), { statusCode: 400, code: 'usage_reset_not_submitted' });
5
+ }
6
+ let usage;
7
+ try { usage = await client.usage(); }
8
+ catch (error) { throw Object.assign(new Error('用量尚未确认,请恢复连接后重新选择重置卡。', { cause: error }),
9
+ { statusCode: 503, code: 'usage_reset_not_submitted' }); }
10
+ if ((usage.accountId || null) !== (body.accountId || null)) {
11
+ throw Object.assign(new Error('Codex 账号已变化,请重新打开用量详情。'), { statusCode: 409, code: 'usage_reset_not_submitted' });
12
+ }
13
+ // The core owns eligibility and the idempotent redemption outcome.
14
+ return client.request('account/rateLimitResetCredit/consume', {
15
+ idempotencyKey: body.idempotencyKey, creditId: body.creditId,
16
+ });
17
+ }