@logicseek/hello-cc 1.0.5 → 1.1.0-rc.3
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/CHANGELOG.md +72 -0
- package/docs/codex-account.zh-CN.md +38 -0
- package/docs/codex-protocol.md +59 -0
- package/docs/commands.md +9 -0
- package/docs/commands.zh-CN.md +7 -0
- package/docs/macos-process-identity.md +2 -0
- package/docs/native.md +179 -14
- package/docs/native.zh-CN.md +136 -6
- package/docs/web-handoff.zh-CN.md +118 -8
- package/lib/cli/app.mjs +47 -4
- package/lib/cli/commands/coordination.mjs +14 -4
- package/lib/cli/commands/dsh.mjs +1 -1
- package/lib/cli/commands/gc.mjs +22 -15
- package/lib/cli/commands/hook.mjs +19 -3
- package/lib/cli/commands/install.mjs +80 -10
- package/lib/cli/commands/misc.mjs +36 -9
- package/lib/cli/commands/native.mjs +4 -38
- package/lib/cli/commands/peer.mjs +3 -0
- package/lib/cli/commands/run.mjs +13 -6
- package/lib/cli/commands/tmux.mjs +1 -2
- package/lib/cli-runtime.mjs +19 -2
- package/lib/core/coordination/results.mjs +27 -10
- package/lib/core/sessions/hook-root-identity.mjs +47 -0
- package/lib/core/sessions/launch.mjs +7 -1
- package/lib/db/connection.mjs +140 -42
- package/lib/db/schema.mjs +59 -0
- package/lib/diagnostics/codex-protocol.mjs +105 -0
- package/lib/guidance.mjs +21 -36
- package/lib/integrations/codex-account.mjs +143 -0
- package/lib/integrations/codex-interactions.mjs +13 -11
- package/lib/integrations/dsh-collaboration.mjs +6 -12
- package/lib/integrations/dsh-cordis.mjs +17 -13
- package/lib/integrations/dsh.mjs +16 -11
- package/lib/integrations/mcp-elicitation.mjs +116 -0
- package/lib/integrations/mcp-url-elicitation.mjs +25 -0
- package/lib/integrations/native/claude.mjs +24 -6
- package/lib/integrations/native/codex.mjs +38 -7
- package/lib/integrations/native/dsh-acp.mjs +67 -24
- package/lib/integrations/native/index.mjs +4 -1
- package/lib/integrations/native/interactions.mjs +2 -1
- package/lib/integrations/native/jsonrpc.mjs +37 -5
- package/lib/integrations/native/telemetry.mjs +30 -0
- package/lib/process/identity.mjs +4 -0
- package/lib/process/pinned-cwd.mjs +302 -0
- package/lib/process/pinned-guidance.mjs +138 -0
- package/lib/process/pinned-project-state.mjs +105 -0
- package/lib/process/selected-cwd-identity.mjs +95 -0
- package/lib/runtime/buffer-evidence.mjs +2 -1
- package/lib/runtime/client.mjs +9 -0
- package/lib/runtime/native/client.mjs +4 -1
- package/lib/runtime/native/history.mjs +140 -0
- package/lib/runtime/native/launcher.mjs +106 -0
- package/lib/runtime/native/service.mjs +64 -8
- package/lib/runtime/native/store.mjs +83 -58
- package/lib/runtime/paths.mjs +17 -2
- package/lib/runtime/private-file.mjs +76 -0
- package/lib/runtime/private-state.mjs +546 -0
- package/lib/runtime/project-context.mjs +2 -1
- package/lib/runtime/project-path.mjs +92 -20
- package/lib/runtime/project-trust.mjs +79 -0
- package/lib/runtime/projects.mjs +9 -3
- package/lib/runtime/state-migration.mjs +697 -0
- package/lib/runtime/state.mjs +69 -27
- package/lib/shared/automatic-result-key.mjs +6 -0
- package/lib/shared/json-file.mjs +55 -6
- package/lib/shared/redact.mjs +2 -1
- package/lib/ui/help.mjs +6 -4
- package/lib/web/agent-defaults.mjs +110 -0
- package/lib/web/auto-attach.mjs +3 -2
- package/lib/web/browser/contracts.d.ts +10 -0
- package/lib/web/browser/core.mjs +1997 -0
- package/lib/web/browser/project-data.mjs +158 -0
- package/lib/web/browser/session-sync.mjs +82 -0
- package/lib/web/browser-assets.mjs +19 -0
- package/lib/web/buffer-gc-runtime.mjs +2 -2
- package/lib/web/codex-app-server.mjs +111 -37
- package/lib/web/codex-sessions.mjs +61 -12
- package/lib/web/context-files.mjs +82 -0
- package/lib/web/cookie-auth.mjs +51 -3
- package/lib/web/external-sessions.mjs +76 -9
- package/lib/web/http-routes.mjs +238 -25
- package/lib/web/http.mjs +50 -4
- package/lib/web/liveness-reaper.mjs +2 -2
- package/lib/web/native-sessions.mjs +323 -14
- package/lib/web/project-contexts.mjs +236 -44
- package/lib/web/project-file-write.mjs +249 -0
- package/lib/web/project-files.mjs +268 -0
- package/lib/web/pty-sessions.mjs +11 -3
- package/lib/web/runtime-main.mjs +132 -80
- package/lib/web/session-serialize.mjs +87 -8
- package/lib/web/session-state-sync.mjs +98 -0
- package/lib/web/shutdown-diagnostics.mjs +38 -0
- package/lib/web/shutdown-response.mjs +21 -0
- package/lib/web/shutdown-state.mjs +91 -0
- package/lib/web/startup.mjs +103 -35
- package/lib/web/task-results.mjs +19 -19
- package/lib/web/tls.mjs +84 -33
- package/lib/web/tmux-sessions.mjs +73 -9
- package/lib/web/tmux-stream.mjs +36 -8
- package/lib/web/ui-agent-defaults.mjs +149 -0
- package/lib/web/ui-agent-start.mjs +330 -0
- package/lib/web/ui-codex-account.mjs +81 -0
- package/lib/web/ui-codex.mjs +105 -85
- package/lib/web/ui-command-palette.mjs +21 -5
- package/lib/web/ui-file-writes.mjs +220 -0
- package/lib/web/ui-files.mjs +231 -0
- package/lib/web/ui-handoff.mjs +10 -2
- package/lib/web/ui-history.mjs +175 -105
- package/lib/web/ui-i18n.mjs +280 -8
- package/lib/web/ui-interactions.mjs +107 -6
- package/lib/web/ui-native-timeline.mjs +326 -0
- package/lib/web/ui-native.mjs +93 -43
- package/lib/web/ui-review.mjs +5 -3
- package/lib/web/ui-safe-markdown.mjs +79 -0
- package/lib/web/ui-session-tools.mjs +224 -0
- package/lib/web/ui-template.mjs +29 -1968
- package/lib/web/ui-terminal-find.mjs +5 -3
- package/lib/web/ui-workbench.mjs +5 -3
- package/lib/web/ui-workspace.mjs +3 -0
- package/package.json +8 -2
- package/scripts/benchmark-session-sync.mjs +53 -0
- package/scripts/codex-protocol.mjs +88 -0
- package/scripts/dsh-approval-layers-acceptance.mjs +172 -0
- package/scripts/dsh-package-provenance.mjs +118 -0
- package/scripts/mcp-form-live-fixture.mjs +81 -0
- package/scripts/native-bounded-live-acceptance.mjs +193 -0
- package/scripts/native-fixture-root.mjs +49 -0
- package/scripts/native-installed-acceptance.mjs +611 -0
- package/scripts/native-stability-checks.mjs +40 -0
- package/scripts/regression-cookie-expiry.mjs +70 -0
- package/scripts/regression-http-transport.mjs +9 -0
- package/scripts/regression-terminal-marker.mjs +95 -0
- package/scripts/regression.mjs +481 -219
- package/scripts/shim-reentry-probe.mjs +105 -0
- package/scripts/web-acceptance-diagnostics.mjs +90 -0
- package/scripts/web-codex-account-acceptance.mjs +178 -0
- package/scripts/web-file-preview-checks.mjs +302 -0
- package/scripts/web-mcp-form-acceptance.mjs +134 -0
- package/scripts/web-mcp-form-live-acceptance.mjs +368 -0
- package/scripts/web-native-handoff-checks.mjs +128 -0
- package/scripts/web-native-interaction-acceptance.mjs +219 -18
- package/scripts/web-native-interaction-diagnostics.mjs +80 -0
- package/scripts/web-native-model-acceptance.mjs +22 -7
- package/scripts/web-session-tools-checks.mjs +112 -0
- package/scripts/web-workbench-acceptance.mjs +435 -0
- package/scripts/web-workbench-installed-acceptance.mjs +122 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/** Read-only project data broker. Pane drafts, selection and control tokens never enter it. */
|
|
2
|
+
const SHARED_PATHS = new Set(['/api/projects', '/api/sessions', '/api/detected', '/api/state']);
|
|
3
|
+
const aborted = () => Object.assign(new Error('Request superseded'), { name: 'AbortError', code: 'REQUEST_SUPERSEDED' });
|
|
4
|
+
|
|
5
|
+
export function createReadBroker({ fetchJson, now = Date.now, ttlMs = 2400, maxEntries = 64 } = {}) {
|
|
6
|
+
if (typeof fetchJson !== 'function' || !Number.isSafeInteger(maxEntries) || maxEntries < 1 || !Number.isFinite(ttlMs) || ttlMs < 0) throw new TypeError('Invalid read broker options');
|
|
7
|
+
const entries = new Map();
|
|
8
|
+
const pending = new Set();
|
|
9
|
+
function read(url, options = {}) {
|
|
10
|
+
const parsed = new URL(url, 'http://hcc.local');
|
|
11
|
+
if (!SHARED_PATHS.has(parsed.pathname)) throw new TypeError('Only project read endpoints can be shared');
|
|
12
|
+
const { signal, headers = {} } = options;
|
|
13
|
+
if (signal?.aborted) return Promise.reject(aborted());
|
|
14
|
+
const requestHeaders = Object.fromEntries(new globalThis.Headers(headers));
|
|
15
|
+
const key = JSON.stringify([url, Object.entries(requestHeaders).sort(([a], [b]) => a.localeCompare(b))]);
|
|
16
|
+
let entry = entries.get(key);
|
|
17
|
+
if (entry && entry.done && now() - entry.completedAt >= ttlMs) { entries.delete(key); entry = null; }
|
|
18
|
+
if (!entry) {
|
|
19
|
+
// Never forget a pending request and silently create unlimited work.
|
|
20
|
+
// Saturation rejects new distinct reads; existing shared consumers finish.
|
|
21
|
+
if (pending.size >= maxEntries) return Promise.reject(Object.assign(new Error('Too many pending project reads'), { code: 'READ_BROKER_BUSY' }));
|
|
22
|
+
if (entries.size >= maxEntries) {
|
|
23
|
+
const completed = [...entries].find(([, value]) => value.done);
|
|
24
|
+
if (completed) entries.delete(completed[0]);
|
|
25
|
+
}
|
|
26
|
+
const controller = new globalThis.AbortController();
|
|
27
|
+
entry = { controller, users: 0, done: false, completedAt: 0 };
|
|
28
|
+
const owned = entry;
|
|
29
|
+
entries.set(key, entry);
|
|
30
|
+
pending.add(entry);
|
|
31
|
+
entry.promise = Promise.resolve().then(() => fetchJson(url, { headers: requestHeaders, signal: controller.signal }))
|
|
32
|
+
.then(value => { pending.delete(owned); owned.done = true; owned.completedAt = now(); return value; }, error => {
|
|
33
|
+
pending.delete(owned);
|
|
34
|
+
owned.done = true;
|
|
35
|
+
if (entries.get(key) === owned) entries.delete(key);
|
|
36
|
+
throw error;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
entry.users++;
|
|
40
|
+
const owned = entry;
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
let finished = false;
|
|
43
|
+
function release() {
|
|
44
|
+
if (finished) return false;
|
|
45
|
+
finished = true; signal?.removeEventListener('abort', cancel); owned.controller.signal.removeEventListener('abort', cancel); owned.users--;
|
|
46
|
+
if (!owned.done && owned.users === 0) {
|
|
47
|
+
owned.controller.abort();
|
|
48
|
+
if (entries.get(key) === owned) entries.delete(key);
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
function cancel() { if (release()) reject(aborted()); }
|
|
53
|
+
signal?.addEventListener('abort', cancel, { once: true });
|
|
54
|
+
owned.controller.signal.addEventListener('abort', cancel, { once: true });
|
|
55
|
+
owned.promise.then(value => { if (release()) resolve(structuredClone(value)); }, error => { if (release()) reject(error); });
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return Object.freeze({ read, invalidate() {
|
|
59
|
+
// Include completed cache entries: their current consumers may still be
|
|
60
|
+
// waiting for a promise microtask when a write invalidates the read.
|
|
61
|
+
const stale = new Set([...entries.values(), ...pending]);
|
|
62
|
+
entries.clear();
|
|
63
|
+
for (const entry of stale) entry.controller.abort();
|
|
64
|
+
} });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function fetchJson(url, options = {}, { fetchImpl = globalThis.fetch, timeoutMs = 30000 } = {}) {
|
|
68
|
+
const controller = new globalThis.AbortController();
|
|
69
|
+
let timedOut = false;
|
|
70
|
+
const cancel = () => controller.abort();
|
|
71
|
+
if (options.signal?.aborted) throw aborted();
|
|
72
|
+
options.signal?.addEventListener('abort', cancel, { once: true });
|
|
73
|
+
const timer = setTimeout(() => { timedOut = true; controller.abort(); }, timeoutMs);
|
|
74
|
+
try {
|
|
75
|
+
const response = await fetchImpl(url, { ...options, signal: controller.signal });
|
|
76
|
+
const json = await response.json();
|
|
77
|
+
if (!response.ok) throw Object.assign(new Error(json.error?.message || json.message || 'Request failed'), {
|
|
78
|
+
code: json.error?.code, detail: json.error?.message, status: response.status,
|
|
79
|
+
...(json.error?.extra && typeof json.error.extra === 'object' && !Array.isArray(json.error.extra) ? { extra: json.error.extra } : {})
|
|
80
|
+
});
|
|
81
|
+
return json;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (timedOut) throw Object.assign(new Error('Request timed out; check the session before retrying a write.'), { code: 'REQUEST_TIMEOUT' });
|
|
84
|
+
if (options.signal?.aborted) throw aborted();
|
|
85
|
+
throw error;
|
|
86
|
+
} finally { clearTimeout(timer); options.signal?.removeEventListener('abort', cancel); }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function sharedProjectBroker(surface = globalThis.window) {
|
|
90
|
+
// Only the existing same-origin parent may lend its broker. No BroadcastChannel
|
|
91
|
+
// or postMessage credential transfer, and no write/lease APIs are exposed.
|
|
92
|
+
try { if (surface.parent !== surface && surface.parent.location.origin === surface.location.origin && surface.parent.hccProjectReads) return surface.parent.hccProjectReads; } catch {}
|
|
93
|
+
if (surface.hccProjectReads) return surface.hccProjectReads;
|
|
94
|
+
const broker = createReadBroker({ fetchJson });
|
|
95
|
+
Object.defineProperty(surface, 'hccProjectReads', { value: broker, configurable: true });
|
|
96
|
+
return broker;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function createProjectRequests({ broker, fetcher = fetchJson, root = '', rootIdentity = '', headers = {}, requireIdentity = false }) {
|
|
100
|
+
let scope = root, selectedIdentity = rootIdentity, generation = 0;
|
|
101
|
+
const pending = new Set();
|
|
102
|
+
const cancel = () => { generation++; for (const controller of pending) controller.abort(); pending.clear(); };
|
|
103
|
+
return {
|
|
104
|
+
rootIdentity() { return selectedIdentity; },
|
|
105
|
+
setRoot(value, identity = scope === value ? selectedIdentity : '') {
|
|
106
|
+
if (scope !== value || selectedIdentity !== identity) { cancel(); scope = value; selectedIdentity = identity; }
|
|
107
|
+
},
|
|
108
|
+
dispose: cancel,
|
|
109
|
+
async request(path, options = {}) {
|
|
110
|
+
const visit = generation, controller = new globalThis.AbortController();
|
|
111
|
+
const url = new URL(path, 'http://hcc.local');
|
|
112
|
+
const { explicitSelection = false, ...fetchOptions } = options;
|
|
113
|
+
if (requireIdentity && !explicitSelection && !selectedIdentity) {
|
|
114
|
+
throw Object.assign(new Error('Select a project before requesting its data'), { code: 'PROJECT_IDENTITY_REQUIRED' });
|
|
115
|
+
}
|
|
116
|
+
if (scope && !explicitSelection) url.searchParams.set('root', scope);
|
|
117
|
+
const target = url.pathname + url.search;
|
|
118
|
+
const read = !fetchOptions.method || fetchOptions.method.toUpperCase() === 'GET';
|
|
119
|
+
const externalAbort = () => controller.abort();
|
|
120
|
+
if (options.signal?.aborted) throw aborted();
|
|
121
|
+
options.signal?.addEventListener('abort', externalAbort, { once: true });
|
|
122
|
+
pending.add(controller);
|
|
123
|
+
const requestHeaders = { 'Content-Type': 'application/json', ...headers, ...options.headers };
|
|
124
|
+
// Never let an auxiliary caller replace the selection attached to this
|
|
125
|
+
// visit. Explicit selection has no prior project identity by design.
|
|
126
|
+
for (const key of Object.keys(requestHeaders)) {
|
|
127
|
+
if (['x-hcc-root-identity', 'x-hcc-browser'].includes(key.toLowerCase())) delete requestHeaders[key];
|
|
128
|
+
}
|
|
129
|
+
if (requireIdentity) requestHeaders['X-HCC-Browser'] = '1';
|
|
130
|
+
if (!explicitSelection && selectedIdentity) requestHeaders['X-HCC-Root-Identity'] = selectedIdentity;
|
|
131
|
+
const requestOptions = { ...fetchOptions, signal: controller.signal, headers: requestHeaders };
|
|
132
|
+
if (!read) broker.invalidate();
|
|
133
|
+
try {
|
|
134
|
+
const data = await (read && SHARED_PATHS.has(url.pathname) ? broker.read(target, requestOptions) : fetcher(target, requestOptions));
|
|
135
|
+
if (visit !== generation || controller.signal.aborted) throw aborted();
|
|
136
|
+
return data;
|
|
137
|
+
} finally {
|
|
138
|
+
if (!read) broker.invalidate();
|
|
139
|
+
pending.delete(controller); options.signal?.removeEventListener('abort', externalAbort);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** No overlapping polls; hidden tabs reduce their refresh rate and wake immediately. */
|
|
146
|
+
export function scheduleVisiblePoll(run, { intervalMs, hiddenMs = 30000, document: doc = globalThis.document, surface = globalThis.window, onError = console.error }) {
|
|
147
|
+
let timer, stopped = false, running = false;
|
|
148
|
+
const schedule = () => { clearTimeout(timer); if (!stopped) timer = setTimeout(tick, doc.hidden ? hiddenMs : intervalMs); };
|
|
149
|
+
async function tick() {
|
|
150
|
+
if (stopped || running) return;
|
|
151
|
+
running = true;
|
|
152
|
+
try { if (surface.navigator?.onLine !== false) await run(); } catch (error) { if (error.name !== 'AbortError') onError(error); }
|
|
153
|
+
finally { running = false; schedule(); }
|
|
154
|
+
}
|
|
155
|
+
function wake() { if (!doc.hidden) { clearTimeout(timer); void tick(); } else schedule(); }
|
|
156
|
+
doc.addEventListener('visibilitychange', wake); surface.addEventListener('online', wake); schedule();
|
|
157
|
+
return () => { stopped = true; clearTimeout(timer); doc.removeEventListener('visibilitychange', wake); surface.removeEventListener('online', wake); };
|
|
158
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** Structured session wire protocol. This module is shared by Node and browsers. */
|
|
2
|
+
export const SESSION_SYNC_PROTOCOL = 1;
|
|
3
|
+
const forbidden = new Set(['__proto__', 'prototype', 'constructor']);
|
|
4
|
+
|
|
5
|
+
/** @typedef {{op:'set'|'delete'|'append'|'splice', path:(string|number)[], value?:unknown, start?:number, deleteCount?:number, values?:unknown[]}} StateOperation */
|
|
6
|
+
|
|
7
|
+
/** Copy only changed ancestors. Never mutate a previously delivered UI state. */
|
|
8
|
+
export function applyStateOperations(state, operations) {
|
|
9
|
+
if (!Array.isArray(operations) || operations.length > 2048) throw new Error('Invalid operations');
|
|
10
|
+
let result = state;
|
|
11
|
+
for (const operation of operations) {
|
|
12
|
+
const path = operation.path;
|
|
13
|
+
if (!Array.isArray(path) || path.length > 32 || path.some(key =>
|
|
14
|
+
!(typeof key === 'string' || Number.isSafeInteger(key) && key >= 0) || forbidden.has(String(key)))) throw new Error('Invalid path');
|
|
15
|
+
const update = (value, depth) => {
|
|
16
|
+
if (depth === path.length) {
|
|
17
|
+
if (operation.op === 'set') return structuredClone(operation.value);
|
|
18
|
+
if (operation.op === 'append' && typeof value === 'string' && typeof operation.value === 'string') return value + operation.value;
|
|
19
|
+
if (operation.op === 'splice' && Array.isArray(value) && Array.isArray(operation.values) &&
|
|
20
|
+
Number.isSafeInteger(operation.start) && operation.start >= 0 && operation.start <= value.length &&
|
|
21
|
+
Number.isSafeInteger(operation.deleteCount) && operation.deleteCount >= 0 && operation.deleteCount <= value.length - operation.start) {
|
|
22
|
+
const copy = value.slice(); copy.splice(operation.start, operation.deleteCount, ...structuredClone(operation.values)); return copy;
|
|
23
|
+
}
|
|
24
|
+
throw new Error('Invalid operation');
|
|
25
|
+
}
|
|
26
|
+
if (!value || typeof value !== 'object') throw new Error('Missing path');
|
|
27
|
+
const key = path[depth], last = depth === path.length - 1;
|
|
28
|
+
if (Array.isArray(value) && (!Number.isSafeInteger(key) || key >= value.length)) throw new Error('Invalid array path');
|
|
29
|
+
if (!Object.hasOwn(value, key) && !(last && operation.op === 'set' && !Array.isArray(value))) throw new Error('Missing path');
|
|
30
|
+
const copy = Array.isArray(value) ? value.slice() : { ...value };
|
|
31
|
+
if (last && operation.op === 'delete' && !Array.isArray(value)) delete copy[key];
|
|
32
|
+
else copy[key] = update(value[key], depth + 1);
|
|
33
|
+
return copy;
|
|
34
|
+
};
|
|
35
|
+
result = update(result, 0);
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* One receiver per WebSocket. Reconnection must create a receiver or reset it.
|
|
42
|
+
* onState receives immutable-by-convention state; recovery never sends input.
|
|
43
|
+
* @param {{root:string,sessionId:string,onState?:(state:object,channel:string)=>void,requestSnapshot?:(frame:object)=>void}} options
|
|
44
|
+
*/
|
|
45
|
+
export function createSessionSync({ root, sessionId, onState = () => {}, requestSnapshot = () => {} }) {
|
|
46
|
+
let state = null, generation = null, executorId = null, revision = -1, channel = null, requestId = null, sequence = 0;
|
|
47
|
+
function recover(reason) {
|
|
48
|
+
if (!requestId) {
|
|
49
|
+
requestId = 'sync-' + ++sequence;
|
|
50
|
+
requestSnapshot({ type: 'state_sync_request', protocol: SESSION_SYNC_PROTOCOL, requestId });
|
|
51
|
+
}
|
|
52
|
+
return { accepted: false, recovery: true, reason };
|
|
53
|
+
}
|
|
54
|
+
function receive(frame) {
|
|
55
|
+
if (frame?.type !== 'state_sync') return { accepted: false, ignored: true };
|
|
56
|
+
if (frame.protocol !== SESSION_SYNC_PROTOCOL || frame.root !== root || frame.sessionId !== sessionId ||
|
|
57
|
+
!['codex', 'native'].includes(frame.channel) || typeof frame.generation !== 'string' || !frame.generation ||
|
|
58
|
+
typeof frame.executorId !== 'string' || !Number.isSafeInteger(frame.revision) || frame.revision < 0) return recover('scope-or-version');
|
|
59
|
+
if (generation && (frame.generation !== generation || frame.executorId !== executorId || frame.channel !== channel)) return recover('executor-or-generation');
|
|
60
|
+
if (frame.mode === 'snapshot') {
|
|
61
|
+
if (!frame.state || typeof frame.state !== 'object' || Array.isArray(frame.state)) return recover('invalid-snapshot');
|
|
62
|
+
if (frame.state.executorId != null && frame.state.executorId !== frame.executorId) return recover('executor-state-mismatch');
|
|
63
|
+
if (generation && (!requestId || frame.requestId !== requestId || frame.revision < revision)) return recover('unsolicited-or-stale-snapshot');
|
|
64
|
+
if (requestId && frame.requestId !== requestId) return { accepted: false, recovery: true, reason: 'waiting-for-recovery' };
|
|
65
|
+
state = structuredClone(frame.state); generation = frame.generation; executorId = frame.executorId; channel = frame.channel;
|
|
66
|
+
} else if (frame.mode === 'patch') {
|
|
67
|
+
if (requestId) return { accepted: false, recovery: true, reason: 'waiting-for-recovery' };
|
|
68
|
+
if (!generation || frame.baseRevision !== revision || frame.revision !== revision + 1) return recover('revision-gap');
|
|
69
|
+
try {
|
|
70
|
+
const next = applyStateOperations(state, frame.operations);
|
|
71
|
+
if (next.executorId != null && next.executorId !== executorId) return recover('executor-state-mismatch');
|
|
72
|
+
state = next;
|
|
73
|
+
} catch { return recover('invalid-patch'); }
|
|
74
|
+
} else return recover('unknown-mode');
|
|
75
|
+
revision = frame.revision; requestId = null;
|
|
76
|
+
onState(state, channel);
|
|
77
|
+
return { accepted: true, revision, generation };
|
|
78
|
+
}
|
|
79
|
+
const api = { receive, reset() { state = null; generation = null; executorId = null; revision = -1; channel = null; requestId = null; } };
|
|
80
|
+
Object.defineProperties(api, { state: { get: () => state }, revision: { get: () => revision }, recovering: { get: () => Boolean(requestId) } });
|
|
81
|
+
return api;
|
|
82
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
|
|
3
|
+
// Explicit public module graph. Never resolve a request path against lib/.
|
|
4
|
+
const names = [
|
|
5
|
+
'browser/core.mjs', 'browser/project-data.mjs', 'browser/session-sync.mjs',
|
|
6
|
+
'api-version.mjs', 'ui-agent-start.mjs', 'ui-agent-defaults.mjs', 'ui-command-palette.mjs', 'ui-files.mjs', 'ui-file-writes.mjs',
|
|
7
|
+
'ui-codex.mjs', 'ui-codex-account.mjs', 'ui-handoff.mjs', 'ui-history.mjs',
|
|
8
|
+
'ui-interactions.mjs', 'ui-i18n.mjs', 'ui-native.mjs', 'ui-native-timeline.mjs',
|
|
9
|
+
'ui-preferences.mjs', 'ui-review.mjs', 'ui-safe-markdown.mjs',
|
|
10
|
+
'ui-session-tools.mjs', 'ui-terminal-find.mjs', 'ui-workbench.mjs', 'ui-workspace.mjs'
|
|
11
|
+
];
|
|
12
|
+
const assets = new Map(names.map(name => ['/assets/web/' + name, fileURLToPath(new URL(name, import.meta.url))]));
|
|
13
|
+
// These shared validators are pure JavaScript and imported by the interaction
|
|
14
|
+
// panel. Keep the two integration paths explicit; other integrations stay private.
|
|
15
|
+
for (const name of ['mcp-elicitation.mjs', 'mcp-url-elicitation.mjs']) {
|
|
16
|
+
assets.set('/assets/integrations/' + name, fileURLToPath(new URL('../integrations/' + name, import.meta.url)));
|
|
17
|
+
}
|
|
18
|
+
export function browserAssetPath(pathname) { return assets.get(pathname) || null; }
|
|
19
|
+
export const BROWSER_ASSET_PATHS = Object.freeze([...assets.keys()]);
|
|
@@ -12,7 +12,7 @@ import { bufferPlanGcCutoffs, planBufferFiles } from '../runtime/buffer-gc.mjs';
|
|
|
12
12
|
|
|
13
13
|
export function createBufferGcRuntime(deps) {
|
|
14
14
|
const {
|
|
15
|
-
sessions,
|
|
15
|
+
sessions, isStopping = () => false,
|
|
16
16
|
connectWebProject, runtimeProjectContexts,
|
|
17
17
|
now, runGc, bufferDirectory,
|
|
18
18
|
canonicalRoot, observePeerEvidence, redactedLogText, sameResolvedPath
|
|
@@ -136,7 +136,7 @@ export function createBufferGcRuntime(deps) {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
function runAutoGc() {
|
|
139
|
-
if (autoGcInFlight) return;
|
|
139
|
+
if (isStopping() || autoGcInFlight) return;
|
|
140
140
|
autoGcInFlight = true;
|
|
141
141
|
try {
|
|
142
142
|
for (const projectCtx of runtimeProjectContexts()) {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnPinnedCwdProcess } from '../process/pinned-cwd.mjs';
|
|
2
|
+
import { captureSelectedCwdIdentity } from '../process/selected-cwd-identity.mjs';
|
|
3
|
+
import { createCodexAccountState } from '../integrations/codex-account.mjs';
|
|
2
4
|
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { redactMcpUrlInteraction } from '../integrations/mcp-url-elicitation.mjs';
|
|
3
6
|
import path from 'node:path';
|
|
4
7
|
import { CODEX_INTERACTION_METHODS, codexInteractionKind, codexInteractionResponse, cancelCodexInteraction, codexInteractiveConfig } from '../integrations/codex-interactions.mjs';
|
|
5
8
|
|
|
@@ -52,13 +55,18 @@ function rpcKey(id) {
|
|
|
52
55
|
|
|
53
56
|
export function createCodexAppServer({
|
|
54
57
|
cwd, env = process.env, onChange = () => {},
|
|
58
|
+
expectedCwdIdentity = null,
|
|
59
|
+
// Internal synchronous observers may consume a borrowed read-only view.
|
|
60
|
+
// Public callers retain isolated snapshots by default; never retain a view.
|
|
61
|
+
changeView = false,
|
|
55
62
|
onSpawn = async () => null,
|
|
56
63
|
requestTimeoutMs = 30000, closeTimeoutMs = 1000, clientVersion = '1.0.1',
|
|
57
64
|
// Test seam. The production command and its arguments are always fixed below.
|
|
58
|
-
spawnProcess =
|
|
65
|
+
spawnProcess = spawnPinnedCwdProcess
|
|
59
66
|
} = {}) {
|
|
60
67
|
const root = path.resolve(cwd || process.cwd());
|
|
61
68
|
if (!Number.isFinite(requestTimeoutMs) || requestTimeoutMs <= 0) throw new TypeError('requestTimeoutMs must be positive');
|
|
69
|
+
const cwdBinding = captureSelectedCwdIdentity(root, expectedCwdIdentity);
|
|
62
70
|
const executorId = randomUUID();
|
|
63
71
|
let status = 'new';
|
|
64
72
|
let child = null;
|
|
@@ -73,6 +81,11 @@ export function createCodexAppServer({
|
|
|
73
81
|
let closePromise = null;
|
|
74
82
|
let streamTimer = null;
|
|
75
83
|
let streamEvent = null;
|
|
84
|
+
let updateSequence = 0;
|
|
85
|
+
const textItems = new Map();
|
|
86
|
+
let textHintsComplete = true;
|
|
87
|
+
const turnSizes = new WeakMap();
|
|
88
|
+
let runtimeMetadata = {}, metadataThreadId = null, metrics = null, metricsThreadId = null;
|
|
76
89
|
const threads = new Map();
|
|
77
90
|
const pending = new Map();
|
|
78
91
|
const approvals = new Map();
|
|
@@ -80,6 +93,8 @@ export function createCodexAppServer({
|
|
|
80
93
|
const submittingThreads = new Set();
|
|
81
94
|
const threadNotificationSequences = new Map();
|
|
82
95
|
const events = [];
|
|
96
|
+
const account = createCodexAccountState({ request: (method, params) => call(method, params),
|
|
97
|
+
onChange: value => changed('account.status', { account: value }) });
|
|
83
98
|
|
|
84
99
|
function bounded(value, limit = MAX_TEXT, depth = 0) {
|
|
85
100
|
if (typeof value === 'string') {
|
|
@@ -100,9 +115,9 @@ export function createCodexAppServer({
|
|
|
100
115
|
return value;
|
|
101
116
|
}
|
|
102
117
|
|
|
103
|
-
function
|
|
118
|
+
function stateView() {
|
|
104
119
|
const current = threads.get(currentThreadId);
|
|
105
|
-
return
|
|
120
|
+
return {
|
|
106
121
|
executorId, backend: 'codex-app-server', status, cwd: root, pid: child?.pid || null,
|
|
107
122
|
processExited: Boolean(child && (child.exitCode != null || child.signalCode != null)),
|
|
108
123
|
metadata, currentThreadId, threadId: currentThreadId, turnId: current?.activeTurnId || null,
|
|
@@ -110,17 +125,24 @@ export function createCodexAppServer({
|
|
|
110
125
|
pendingApprovals: [...approvals.values()].filter((approval) => approval.status === 'pending'),
|
|
111
126
|
uncertainSubmissions: [...uncertain.values()].map(({ params, ...entry }) => entry),
|
|
112
127
|
uncertainRecovery: uncertain.size ? 'stop-executor-and-resume-history' : null,
|
|
113
|
-
events, truncated
|
|
114
|
-
|
|
128
|
+
account: account.snapshot(), events, truncated, updateSequence,
|
|
129
|
+
...(metadataThreadId === currentThreadId && Object.keys(runtimeMetadata).length ? { runtimeMetadata } : {}),
|
|
130
|
+
...(metrics && metricsThreadId === currentThreadId ? { metrics } : {})
|
|
131
|
+
};
|
|
115
132
|
}
|
|
133
|
+
function snapshot() { return structuredClone(stateView()); }
|
|
116
134
|
|
|
117
135
|
function publishChange(event) {
|
|
118
136
|
clearTimeout(streamTimer); streamTimer = null; streamEvent = null;
|
|
119
|
-
|
|
137
|
+
const change = { textOnly: textHintsComplete && event?.method === 'item/agentMessage/delta', textItems: [...textItems.values()] };
|
|
138
|
+
textItems.clear();
|
|
139
|
+
textHintsComplete = true;
|
|
140
|
+
try { onChange(changeView ? stateView() : snapshot(), structuredClone(event), change); } catch { /* UI observers do not own transport. */ }
|
|
120
141
|
}
|
|
121
142
|
|
|
122
143
|
function changed(method, params = {}) {
|
|
123
|
-
|
|
144
|
+
updateSequence++;
|
|
145
|
+
const event = { method, params: bounded(redactMcpUrlInteraction(params), 8192), at: Date.now() };
|
|
124
146
|
events.push(event);
|
|
125
147
|
if (events.length > 100) events.shift();
|
|
126
148
|
// Keep protocol state current on every token, but copy/broadcast its complete
|
|
@@ -128,6 +150,9 @@ export function createCodexAppServer({
|
|
|
128
150
|
// Approval, completion, error and connection changes publish immediately;
|
|
129
151
|
// their snapshot includes all pending text and cancels the trailing timer.
|
|
130
152
|
if (method === 'item/agentMessage/delta') {
|
|
153
|
+
const identity = { threadId: params.threadId, turnId: params.turnId, itemId: params.itemId };
|
|
154
|
+
if (textItems.size < 1024) textItems.set(JSON.stringify(identity), identity);
|
|
155
|
+
else textHintsComplete = false;
|
|
131
156
|
streamEvent = event;
|
|
132
157
|
if (!streamTimer) {
|
|
133
158
|
streamTimer = setTimeout(() => publishChange(streamEvent), STREAM_UPDATE_MS);
|
|
@@ -172,9 +197,21 @@ export function createCodexAppServer({
|
|
|
172
197
|
}
|
|
173
198
|
}
|
|
174
199
|
|
|
175
|
-
function boundItems(turn) {
|
|
176
|
-
|
|
177
|
-
|
|
200
|
+
function boundItems(turn, changedItem = null) {
|
|
201
|
+
let cache = turnSizes.get(turn);
|
|
202
|
+
if (!cache || !changedItem || cache.array !== turn.items) {
|
|
203
|
+
const sizes = new Map(turn.items.map(item => [item, JSON.stringify(item).length]));
|
|
204
|
+
cache = { array: turn.items, sizes, total: 2 + Math.max(0, turn.items.length - 1) + [...sizes.values()].reduce((sum, size) => sum + size, 0) };
|
|
205
|
+
turnSizes.set(turn, cache);
|
|
206
|
+
} else {
|
|
207
|
+
const old = cache.sizes.get(changedItem), size = JSON.stringify(changedItem).length;
|
|
208
|
+
cache.total += size - (old || 0) + (old == null && cache.sizes.size ? 1 : 0);
|
|
209
|
+
cache.sizes.set(changedItem, size);
|
|
210
|
+
}
|
|
211
|
+
while (turn.items.length > 100 || cache.total > 1024 * 1024) {
|
|
212
|
+
const removed = turn.items.shift();
|
|
213
|
+
cache.total -= (cache.sizes.get(removed) || 0) + (turn.items.length ? 1 : 0);
|
|
214
|
+
cache.sizes.delete(removed); truncated = true;
|
|
178
215
|
}
|
|
179
216
|
}
|
|
180
217
|
|
|
@@ -235,6 +272,12 @@ export function createCodexAppServer({
|
|
|
235
272
|
}
|
|
236
273
|
|
|
237
274
|
function applyResult(method, params, result, requestSequence) {
|
|
275
|
+
if (['thread/start', 'thread/resume'].includes(method)) {
|
|
276
|
+
const policy = result?.approvalPolicy;
|
|
277
|
+
metadataThreadId = result?.thread?.id || null;
|
|
278
|
+
runtimeMetadata = { ...(typeof result?.model === 'string' ? { model: result.model } : {}),
|
|
279
|
+
...(policy != null ? { permissionMode: typeof policy === 'string' ? policy : JSON.stringify(policy) } : {}) };
|
|
280
|
+
}
|
|
238
281
|
if (['thread/start', 'thread/resume', 'thread/read'].includes(method)) absorbThread(result?.thread, requestSequence);
|
|
239
282
|
if (method === 'turn/start') absorbTurn(params.threadId, result?.turn);
|
|
240
283
|
}
|
|
@@ -251,6 +294,7 @@ export function createCodexAppServer({
|
|
|
251
294
|
function disconnect(code, message) {
|
|
252
295
|
if (status === 'disconnected' || status === 'closed') return;
|
|
253
296
|
status = 'disconnected';
|
|
297
|
+
account.close();
|
|
254
298
|
for (const entry of pending.values()) {
|
|
255
299
|
clearTimeout(entry.timer);
|
|
256
300
|
const isUncertain = entry.mutation && entry.sent;
|
|
@@ -333,7 +377,8 @@ export function createCodexAppServer({
|
|
|
333
377
|
if (approvals.has(key)) throw new CodexAppServerError('CODEX_PROTOCOL_ERROR', 'server reused an unresolved request id');
|
|
334
378
|
if (approvals.size >= 64) throw new CodexAppServerError('CODEX_PROTOCOL_ERROR', 'too many unresolved approvals');
|
|
335
379
|
thread.activeTurnId = turnId;
|
|
336
|
-
|
|
380
|
+
const inputRequired = codexInteractionKind(message.method) === 'userInput' || (message.method === 'mcpServer/elicitation/request' && params.mode === 'form' && Object.keys(params.requestedSchema?.properties || {}).length > 0);
|
|
381
|
+
thread.status = { type: 'active', activeFlags: [inputRequired ? 'waitingOnUserInput' : 'waitingOnApproval'] };
|
|
337
382
|
const preview = bounded(params);
|
|
338
383
|
approvals.set(key, {
|
|
339
384
|
executorId, requestId: message.id, threadId, turnId, itemId: params.itemId || null,
|
|
@@ -346,9 +391,22 @@ export function createCodexAppServer({
|
|
|
346
391
|
|
|
347
392
|
function notification(method, params) {
|
|
348
393
|
notificationSequence += 1;
|
|
394
|
+
if (account.notification(method, params)) return;
|
|
349
395
|
const notifiedThread = params.threadId || params.thread?.id;
|
|
350
396
|
if (notifiedThread) threadNotificationSequences.set(notifiedThread, notificationSequence);
|
|
351
397
|
if (method === 'thread/started') absorbThread(params.thread);
|
|
398
|
+
if (method === 'thread/tokenUsage/updated' && params.threadId === currentThreadId) {
|
|
399
|
+
const usage = params.tokenUsage?.total;
|
|
400
|
+
if (usage && typeof usage === 'object') {
|
|
401
|
+
const values = {};
|
|
402
|
+
for (const name of ['inputTokens', 'outputTokens', 'cachedInputTokens', 'totalTokens']) {
|
|
403
|
+
if (Number.isFinite(usage[name]) && usage[name] >= 0) values[name] = usage[name];
|
|
404
|
+
}
|
|
405
|
+
if (Number.isFinite(params.tokenUsage.modelContextWindow) && params.tokenUsage.modelContextWindow > 0) values.contextWindow = params.tokenUsage.modelContextWindow;
|
|
406
|
+
metrics = { source: 'codex thread/tokenUsage/updated', scope: 'session', observedAt: Date.now(), ...values };
|
|
407
|
+
metricsThreadId = params.threadId;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
352
410
|
if (method === 'thread/status/changed') {
|
|
353
411
|
const thread = ensureThread(params.threadId);
|
|
354
412
|
thread.status = bounded(params.status);
|
|
@@ -374,7 +432,7 @@ export function createCodexAppServer({
|
|
|
374
432
|
if (!item) { item = { id: itemId, type: 'agentMessage', text: '' }; turn.items.push(item); }
|
|
375
433
|
if (params.item) Object.assign(item, bounded(params.item));
|
|
376
434
|
else item.text = bounded((item.text || '') + (params.delta || ''));
|
|
377
|
-
boundItems(turn);
|
|
435
|
+
boundItems(turn, item);
|
|
378
436
|
}
|
|
379
437
|
changed(method, params);
|
|
380
438
|
}
|
|
@@ -408,7 +466,8 @@ export function createCodexAppServer({
|
|
|
408
466
|
entry?.resolve(message.result);
|
|
409
467
|
}
|
|
410
468
|
uncertain.delete(key);
|
|
411
|
-
changed(late ? 'request.reconciled' : 'request.completed', { requestId: message.id, method: operation.method,
|
|
469
|
+
changed(late ? 'request.reconciled' : 'request.completed', { requestId: message.id, method: operation.method,
|
|
470
|
+
error: operation.method.startsWith('account/') ? (message.error ? { code: message.error.code } : null) : message.error || null });
|
|
412
471
|
}
|
|
413
472
|
|
|
414
473
|
function initialize() {
|
|
@@ -417,8 +476,10 @@ export function createCodexAppServer({
|
|
|
417
476
|
initialization = (async () => {
|
|
418
477
|
status = 'connecting';
|
|
419
478
|
try {
|
|
479
|
+
cwdBinding.assertUnchanged();
|
|
420
480
|
child = spawnProcess('codex', ['app-server', '--listen', 'stdio://'], {
|
|
421
|
-
cwd: root,
|
|
481
|
+
cwd: root, expectedIdentity: cwdBinding,
|
|
482
|
+
env: { ...env }, stdio: ['pipe', 'pipe', 'pipe']
|
|
422
483
|
});
|
|
423
484
|
if (!child?.stdin || !child.stdout) throw new CodexAppServerError('CODEX_START_FAILED', 'Codex process has no stdio transport');
|
|
424
485
|
child.stdout.setEncoding('utf8');
|
|
@@ -442,8 +503,8 @@ export function createCodexAppServer({
|
|
|
442
503
|
child.stdin.on('error', () => disconnect('CODEX_DISCONNECTED', 'Codex input closed'));
|
|
443
504
|
// Drain diagnostics, but never publish environment/auth data or stderr in UI state.
|
|
444
505
|
child.stderr?.on('data', () => {});
|
|
445
|
-
child.on('error', () => disconnect('CODEX_START_FAILED', 'Could not start the installed codex executable'));
|
|
446
|
-
child.on('exit', () => disconnect('CODEX_DISCONNECTED', 'Codex App Server exited'));
|
|
506
|
+
child.on('error', () => { cwdBinding.release(); disconnect('CODEX_START_FAILED', 'Could not start the installed codex executable'); });
|
|
507
|
+
child.on('exit', () => { cwdBinding.release(); disconnect('CODEX_DISCONNECTED', 'Codex App Server exited'); });
|
|
447
508
|
child.stdout.on('end', () => disconnect('CODEX_DISCONNECTED', 'Codex App Server output ended'));
|
|
448
509
|
changed('transport.connecting');
|
|
449
510
|
metadata = bounded(await request('initialize', {
|
|
@@ -473,9 +534,15 @@ export function createCodexAppServer({
|
|
|
473
534
|
|
|
474
535
|
async function call(method, params, mutation = false, absorb = true) {
|
|
475
536
|
await initialize();
|
|
537
|
+
if (method === 'thread/list') assertCwdHandoff();
|
|
476
538
|
return request(method, params, mutation, absorb);
|
|
477
539
|
}
|
|
478
540
|
|
|
541
|
+
function assertCwdHandoff() {
|
|
542
|
+
if (status !== 'ready') throw new CodexAppServerError('CODEX_DISCONNECTED', 'Executor is not ready for a directory handoff');
|
|
543
|
+
cwdBinding.assertUnchanged();
|
|
544
|
+
}
|
|
545
|
+
|
|
479
546
|
function assertActiveTurn(threadId, turnId) {
|
|
480
547
|
requiredId(threadId, 'threadId'); requiredId(turnId, 'turnId');
|
|
481
548
|
if (threads.get(threadId)?.activeTurnId !== turnId) {
|
|
@@ -486,33 +553,38 @@ export function createCodexAppServer({
|
|
|
486
553
|
async function close() {
|
|
487
554
|
if (closePromise) return closePromise;
|
|
488
555
|
closePromise = (async () => {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
556
|
+
try {
|
|
557
|
+
disconnect('CODEX_CLOSED', 'Codex App Server was closed');
|
|
558
|
+
status = 'closed'; approvals.clear();
|
|
559
|
+
changed('transport.closed');
|
|
560
|
+
if (!child || child.exitCode != null || child.signalCode != null) return;
|
|
561
|
+
await new Promise((resolve) => {
|
|
562
|
+
let timer;
|
|
563
|
+
const done = () => { clearTimeout(timer); child.removeListener('exit', done); resolve(); };
|
|
564
|
+
child.once('exit', done);
|
|
565
|
+
timer = setTimeout(() => { try { child.kill('SIGKILL'); } catch {} done(); }, closeTimeoutMs);
|
|
566
|
+
try { child.stdin?.end(); child.kill('SIGTERM'); } catch { done(); }
|
|
567
|
+
});
|
|
568
|
+
} finally { cwdBinding.release(); }
|
|
500
569
|
})();
|
|
501
570
|
return closePromise;
|
|
502
571
|
}
|
|
503
572
|
|
|
504
573
|
return {
|
|
505
574
|
initialize, snapshot, close,
|
|
575
|
+
async readAccount() { await initialize(); return account.refresh(); },
|
|
506
576
|
async startThread(params = {}) {
|
|
507
577
|
await initialize();
|
|
508
|
-
|
|
509
|
-
|
|
578
|
+
assertCwdHandoff();
|
|
579
|
+
return request('thread/start', { sandbox: 'workspace-write', approvalPolicy: 'on-request', approvalsReviewer: 'user',
|
|
580
|
+
...objectParams(params), cwd: root, config: codexInteractiveConfig({ ...objectParams(params).config, ...threadConfig }) }, true);
|
|
510
581
|
},
|
|
511
582
|
async resumeThread(threadId, params = {}) {
|
|
512
583
|
await initialize();
|
|
513
|
-
|
|
584
|
+
assertCwdHandoff();
|
|
585
|
+
return request('thread/resume', { approvalPolicy: 'on-request', approvalsReviewer: 'user',
|
|
514
586
|
...objectParams(params), config: codexInteractiveConfig({ ...objectParams(params).config, ...threadConfig }),
|
|
515
|
-
threadId: requiredId(threadId, 'threadId') }, true);
|
|
587
|
+
cwd: root, threadId: requiredId(threadId, 'threadId') }, true);
|
|
516
588
|
},
|
|
517
589
|
readThread(threadId, params = {}) {
|
|
518
590
|
return call('thread/read', { includeTurns: true, ...objectParams(params), threadId: requiredId(threadId, 'threadId') });
|
|
@@ -523,11 +595,12 @@ export function createCodexAppServer({
|
|
|
523
595
|
async forkThread(threadId, params = {}, beforeSubmit = () => {}) {
|
|
524
596
|
await initialize();
|
|
525
597
|
beforeSubmit();
|
|
526
|
-
|
|
598
|
+
assertCwdHandoff();
|
|
599
|
+
return request('thread/fork', { approvalPolicy: 'on-request', approvalsReviewer: 'user', sandbox: 'workspace-write',
|
|
527
600
|
...objectParams(params), config: codexInteractiveConfig({ ...objectParams(params).config, ...threadConfig }),
|
|
528
|
-
threadId: requiredId(threadId, 'threadId') }, true, false);
|
|
601
|
+
cwd: root, threadId: requiredId(threadId, 'threadId') }, true, false);
|
|
529
602
|
},
|
|
530
|
-
listThreads(params = {}) { return call('thread/list', { limit: 50,
|
|
603
|
+
listThreads(params = {}) { return call('thread/list', { limit: 50, ...objectParams(params), cwd: root }); },
|
|
531
604
|
async startTurn(threadId, input, params = {}) {
|
|
532
605
|
requiredId(threadId, 'threadId');
|
|
533
606
|
const normalized = normalizeInput(input);
|
|
@@ -551,7 +624,7 @@ export function createCodexAppServer({
|
|
|
551
624
|
assertActiveTurn(threadId, turnId);
|
|
552
625
|
return call('turn/interrupt', { threadId, turnId }, true).then(result => { expireApprovals(threadId, turnId); changed('approval.expired', { threadId, turnId }); return result; });
|
|
553
626
|
},
|
|
554
|
-
async approve({ executorId: submittedExecutorId, threadId, turnId, requestId, decision, permissions, scope, answers } = {}) {
|
|
627
|
+
async approve({ executorId: submittedExecutorId, threadId, turnId, requestId, decision, permissions, scope, answers, content } = {}) {
|
|
555
628
|
if (submittedExecutorId !== executorId) throw new CodexAppServerError('CODEX_APPROVAL_MISMATCH', 'approval identity or decision is invalid');
|
|
556
629
|
assertActiveTurn(threadId, turnId);
|
|
557
630
|
const approval = approvals.get(rpcKey(requestId));
|
|
@@ -561,13 +634,14 @@ export function createCodexAppServer({
|
|
|
561
634
|
if (approval.truncated && decision === 'accept') {
|
|
562
635
|
throw new CodexAppServerError('CODEX_APPROVAL_TRUNCATED', 'approval details exceed the preview limit; decline or cancel instead');
|
|
563
636
|
}
|
|
564
|
-
const result = codexInteractionResponse({ ...approval, kind: approval.kind || codexInteractionKind(approval.method) }, { decision, permissions, scope, answers });
|
|
637
|
+
const result = codexInteractionResponse({ ...approval, kind: approval.kind || codexInteractionKind(approval.method) }, { decision, permissions, scope, answers, content });
|
|
565
638
|
await initialize();
|
|
566
639
|
// No await between checking a live request and replying to its exact RPC id.
|
|
567
640
|
assertActiveTurn(threadId, turnId);
|
|
568
641
|
if (approvals.get(rpcKey(requestId)) !== approval) throw new CodexAppServerError('CODEX_APPROVAL_MISMATCH', 'approval was resolved');
|
|
569
642
|
write({ id: requestId, result });
|
|
570
643
|
approval.status = 'submitted';
|
|
644
|
+
approval.params = redactMcpUrlInteraction(approval).params;
|
|
571
645
|
changed('approval.submitted', { executorId, threadId, turnId, requestId, decision });
|
|
572
646
|
return { executorId, threadId, turnId, requestId, decision, status: 'submitted' };
|
|
573
647
|
}
|