@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
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { JsonRpcProcess } from './jsonrpc.mjs';
|
|
2
|
+
import { createCodexAccountState } from '../codex-account.mjs';
|
|
2
3
|
import { createNativeInteractions } from './interactions.mjs';
|
|
4
|
+
import { codexUsage } from './telemetry.mjs';
|
|
5
|
+
import { captureSelectedCwdIdentity } from '../../process/selected-cwd-identity.mjs';
|
|
3
6
|
import { CODEX_INTERACTION_METHODS, codexInteractionKind, codexInteractionResponse, cancelCodexInteraction, codexInteractiveConfig } from '../codex-interactions.mjs';
|
|
4
7
|
|
|
5
8
|
const CAPABILITIES = Object.freeze({ create: true, resume: true, send: true, observe: true,
|
|
6
|
-
steer: true, interrupt: true, close: true, fork: false });
|
|
9
|
+
steer: true, interrupt: true, close: true, fork: false, accountRead: true });
|
|
7
10
|
const TERMINAL_TURN_STATUSES = new Set(['completed', 'interrupted', 'failed']);
|
|
8
11
|
|
|
9
12
|
function adapterError(code, message) {
|
|
@@ -14,6 +17,7 @@ function adapterError(code, message) {
|
|
|
14
17
|
export function createCodexAdapter(options = {}) {
|
|
15
18
|
const { binary = 'codex', cwd, env, timeoutMs = 30_000, onEvent = () => {},
|
|
16
19
|
rpcFactory = (config) => new JsonRpcProcess(config) } = options;
|
|
20
|
+
const cwdBinding = cwd ? captureSelectedCwdIdentity(cwd, options.expectedCwdIdentity) : null;
|
|
17
21
|
const state = { provider: 'codex', sessionId: null, turnId: null, status: 'new', owned: true };
|
|
18
22
|
const terminalTurns = new Set();
|
|
19
23
|
const interruptedTurns = new Set();
|
|
@@ -24,6 +28,8 @@ export function createCodexAdapter(options = {}) {
|
|
|
24
28
|
let admission = Promise.resolve();
|
|
25
29
|
let pendingSubmissionId;
|
|
26
30
|
let closePromise;
|
|
31
|
+
const account = createCodexAccountState({ request: (method, params) => rpc.request(method, params),
|
|
32
|
+
onChange: value => emit({ type: 'account', account: value }) });
|
|
27
33
|
const capabilities = { ...CAPABILITIES, approvals: Boolean(options.interactive), userInput: Boolean(options.interactive), mcp: Boolean(options.mcpServers) };
|
|
28
34
|
const interactions = createNativeInteractions({ executorId: options.executorId,
|
|
29
35
|
isActive: (sessionId, turnId) => !closed && sessionId === state.sessionId && turnId === state.turnId && !terminalTurns.has(turnId) && !interruptedTurns.has(turnId) && !['error', 'uncertain', 'closed'].includes(state.status),
|
|
@@ -55,10 +61,13 @@ export function createCodexAdapter(options = {}) {
|
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
function notification(method, params = {}) {
|
|
58
|
-
if (closed || !state.sessionId || params.threadId !== state.sessionId) return;
|
|
64
|
+
if (closed || account.notification(method, params) || !state.sessionId || params.threadId !== state.sessionId) return;
|
|
59
65
|
const turnId = params.turn?.id ?? params.turnId;
|
|
60
66
|
const submissionId = turnSubmissions.get(turnId) ?? pendingSubmissionId;
|
|
61
|
-
if (method === '
|
|
67
|
+
if (method === 'thread/tokenUsage/updated') {
|
|
68
|
+
const metrics = codexUsage(params);
|
|
69
|
+
if (metrics) { state.metrics = metrics; emit({ type: 'usage', metrics, turnId }); }
|
|
70
|
+
} else if (method === 'turn/started' && turnId && !terminalTurns.has(turnId)) {
|
|
62
71
|
if (submissionId) turnSubmissions.set(turnId, submissionId);
|
|
63
72
|
state.turnId = turnId;
|
|
64
73
|
status('busy');
|
|
@@ -119,6 +128,8 @@ export function createCodexAdapter(options = {}) {
|
|
|
119
128
|
|
|
120
129
|
function exited(details = {}) {
|
|
121
130
|
if (closed) return;
|
|
131
|
+
cwdBinding?.release();
|
|
132
|
+
account.close();
|
|
122
133
|
interactions.expire();
|
|
123
134
|
state.turnId = null;
|
|
124
135
|
status('error');
|
|
@@ -128,6 +139,7 @@ export function createCodexAdapter(options = {}) {
|
|
|
128
139
|
|
|
129
140
|
function transportError(error) {
|
|
130
141
|
if (closed) return;
|
|
142
|
+
account.close();
|
|
131
143
|
interactions.expire();
|
|
132
144
|
status('error');
|
|
133
145
|
emit({ type: 'error', code: error.code ?? 'NATIVE_TRANSPORT_ERROR', text: error.message,
|
|
@@ -143,6 +155,7 @@ export function createCodexAdapter(options = {}) {
|
|
|
143
155
|
}
|
|
144
156
|
status('opening');
|
|
145
157
|
try {
|
|
158
|
+
cwdBinding?.assertUnchanged();
|
|
146
159
|
rpc = rpcFactory({ binary, args: ['app-server', '--stdio'], cwd, env, timeoutMs,
|
|
147
160
|
onNotification: notification, onRequest: serverRequest, onExit: exited, onError: transportError });
|
|
148
161
|
await rpc.start();
|
|
@@ -156,12 +169,16 @@ export function createCodexAdapter(options = {}) {
|
|
|
156
169
|
const params = { sandbox: 'workspace-write', approvalPolicy: 'on-request', approvalsReviewer: 'user',
|
|
157
170
|
...(Object.keys(config).length ? { config } : {}),
|
|
158
171
|
...(cwd ? { cwd } : {}), ...(model ? { model } : {}), ...(sessionId ? { threadId: sessionId } : {}) };
|
|
172
|
+
cwdBinding?.assertUnchanged();
|
|
159
173
|
const response = await rpc.request(sessionId ? 'thread/resume' : 'thread/start', params);
|
|
160
174
|
if (!response?.thread?.id || (sessionId && response.thread.id !== sessionId)) {
|
|
161
175
|
throw adapterError('NATIVE_PROTOCOL_ERROR', 'Codex returned an invalid native thread identity.');
|
|
162
176
|
}
|
|
163
177
|
if (closed) throw adapterError('NATIVE_SESSION_CLOSED', 'The Codex session was closed while opening.');
|
|
164
178
|
state.sessionId = response.thread.id;
|
|
179
|
+
state.runtimeMetadata = { ...(typeof response.model === 'string' ? { model: response.model } : {}),
|
|
180
|
+
...(typeof response.approvalPolicy === 'string' ? { permissionMode: response.approvalPolicy } : {}) };
|
|
181
|
+
emit({ type: 'metadata', runtimeMetadata: state.runtimeMetadata });
|
|
165
182
|
const activeTurn = response.thread.turns?.findLast((turn) => turn.status === 'inProgress');
|
|
166
183
|
state.turnId = activeTurn?.id ?? null;
|
|
167
184
|
status(state.turnId ? 'busy' : 'idle');
|
|
@@ -229,14 +246,28 @@ export function createCodexAdapter(options = {}) {
|
|
|
229
246
|
function close() {
|
|
230
247
|
if (closePromise) return closePromise;
|
|
231
248
|
closed = true;
|
|
249
|
+
account.close();
|
|
232
250
|
interactions.expire();
|
|
233
251
|
state.turnId = null;
|
|
234
|
-
status('
|
|
235
|
-
closePromise = Promise.resolve().then(() => rpc?.close()).then(() =>
|
|
252
|
+
status('closing');
|
|
253
|
+
closePromise = Promise.resolve().then(() => rpc?.close()).then(() => {
|
|
254
|
+
cwdBinding?.release();
|
|
255
|
+
status('closed');
|
|
256
|
+
return snapshot();
|
|
257
|
+
}).catch((error) => {
|
|
258
|
+
cwdBinding?.release();
|
|
259
|
+
// Keep admissions revoked while allowing owned-process cleanup to retry.
|
|
260
|
+
status('uncertain');
|
|
261
|
+
closePromise = null;
|
|
262
|
+
throw error;
|
|
263
|
+
});
|
|
236
264
|
return closePromise;
|
|
237
265
|
}
|
|
238
266
|
|
|
239
|
-
function snapshot() { return { ...state, executorId: interactions.executorId, capabilities: { ...capabilities },
|
|
267
|
+
function snapshot() { return { ...state, executorId: interactions.executorId, capabilities: { ...capabilities },
|
|
268
|
+
account: account.snapshot(), pendingApprovals: interactions.snapshot() }; }
|
|
269
|
+
|
|
270
|
+
async function readAccount() { requireOpen(); await account.refresh(); requireOpen(); return account.snapshot(); }
|
|
240
271
|
|
|
241
|
-
return { capabilities, open, send, interrupt, close, snapshot, respond: interactions.respond };
|
|
272
|
+
return { capabilities, open, send, interrupt, close, snapshot, readAccount, respond: interactions.respond };
|
|
242
273
|
}
|
|
@@ -2,6 +2,8 @@ import path from 'node:path';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { createNativeInteractions } from './interactions.mjs';
|
|
4
4
|
import { JsonRpcProcess } from './jsonrpc.mjs';
|
|
5
|
+
import { acpUsage } from './telemetry.mjs';
|
|
6
|
+
import { captureSelectedCwdIdentity } from '../../process/selected-cwd-identity.mjs';
|
|
5
7
|
|
|
6
8
|
export const DSH_ACP_BASELINE_VERSION = '0.2.0-rc.2';
|
|
7
9
|
|
|
@@ -16,6 +18,7 @@ export const DSH_ACP_BASELINE_VERSION = '0.2.0-rc.2';
|
|
|
16
18
|
export function createDshAcpAdapter(options = {}) {
|
|
17
19
|
const cwd = options.cwd || process.cwd();
|
|
18
20
|
if (!path.isAbsolute(cwd)) throw failure('NATIVE_BAD_CWD', 'ACP requires an absolute workspace directory');
|
|
21
|
+
const cwdBinding = captureSelectedCwdIdentity(cwd, options.expectedCwdIdentity);
|
|
19
22
|
const capabilities = {
|
|
20
23
|
create: true, resume: false, send: true, observe: true,
|
|
21
24
|
steer: false, interrupt: true, close: false, fork: false, approvals: Boolean(options.interactive), mcp: Boolean(options.mcpServers)
|
|
@@ -26,9 +29,13 @@ export function createDshAcpAdapter(options = {}) {
|
|
|
26
29
|
let active = null;
|
|
27
30
|
let opening = null;
|
|
28
31
|
let openingIdentity = null;
|
|
29
|
-
let closing =
|
|
32
|
+
let closing = false;
|
|
33
|
+
let closePromise = null;
|
|
34
|
+
let runtimeDisposalStarted = false;
|
|
35
|
+
let runtimeExited = false;
|
|
30
36
|
let runtimeDisposed = false;
|
|
31
37
|
let lastError = null;
|
|
38
|
+
let metrics = null, runtimeMetadata = {};
|
|
32
39
|
const receipts = new Map();
|
|
33
40
|
const toolCalls = new Map(), toolContextWaiters = new Set();
|
|
34
41
|
const interactions = createNativeInteractions({ executorId: options.executorId,
|
|
@@ -50,6 +57,7 @@ export function createDshAcpAdapter(options = {}) {
|
|
|
50
57
|
cwd, sessionId, status,
|
|
51
58
|
turnId: active?.turnId || null,
|
|
52
59
|
submissionId: active?.submissionId || null,
|
|
60
|
+
metrics, runtimeMetadata: { ...runtimeMetadata },
|
|
53
61
|
capabilities: { ...capabilities }, executorId: interactions.executorId, pendingApprovals: interactions.snapshot(),
|
|
54
62
|
error: lastError ? { ...lastError } : null
|
|
55
63
|
};
|
|
@@ -89,6 +97,12 @@ export function createDshAcpAdapter(options = {}) {
|
|
|
89
97
|
if (closing || ['closed', 'failed', 'exited'].includes(status) || method !== 'session/update' || params?.sessionId !== sessionId) return;
|
|
90
98
|
const update = params.update;
|
|
91
99
|
if (!update || typeof update !== 'object') return;
|
|
100
|
+
const reported = acpUsage(update);
|
|
101
|
+
if (reported) { metrics = reported; emit({ type: 'usage', metrics, turnId: active?.turnId || null }); }
|
|
102
|
+
if (update.sessionUpdate === 'available_commands_update' && Array.isArray(update.availableCommands)) {
|
|
103
|
+
runtimeMetadata = { ...runtimeMetadata, commands: update.availableCommands.filter(command => typeof command?.name === 'string').map(command => ({ name: command.name, description: command.description })) };
|
|
104
|
+
emit({ type: 'metadata', runtimeMetadata: { ...runtimeMetadata } });
|
|
105
|
+
}
|
|
92
106
|
rememberToolCall(update);
|
|
93
107
|
if (update.sessionUpdate === 'agent_message_chunk' && update.content?.type === 'text' && typeof update.content.text === 'string') {
|
|
94
108
|
if (active) active.text += update.content.text;
|
|
@@ -135,6 +149,8 @@ export function createDshAcpAdapter(options = {}) {
|
|
|
135
149
|
}
|
|
136
150
|
|
|
137
151
|
function onExit(info) {
|
|
152
|
+
runtimeExited = true;
|
|
153
|
+
cwdBinding.release();
|
|
138
154
|
if (closing || runtimeDisposed || status === 'closed') return;
|
|
139
155
|
const turn = active;
|
|
140
156
|
interactions.expire();
|
|
@@ -176,6 +192,7 @@ export function createDshAcpAdapter(options = {}) {
|
|
|
176
192
|
openingIdentity = input.sessionId || null;
|
|
177
193
|
opening = (async () => {
|
|
178
194
|
try {
|
|
195
|
+
cwdBinding.assertUnchanged();
|
|
179
196
|
const config = {
|
|
180
197
|
binary: options.binary || 'dsh', args: ['--profile', 'acp'],
|
|
181
198
|
cwd, env: options.env, timeoutMs: options.timeoutMs,
|
|
@@ -193,6 +210,7 @@ export function createDshAcpAdapter(options = {}) {
|
|
|
193
210
|
capabilities.resume = extensions.resume !== undefined && extensions.resume !== null && extensions.resume !== false;
|
|
194
211
|
capabilities.close = extensions.close !== undefined && extensions.close !== null && extensions.close !== false;
|
|
195
212
|
if (input.sessionId && !capabilities.resume) throw failure('NATIVE_UNSUPPORTED', 'This ACP runtime does not advertise session resume');
|
|
213
|
+
cwdBinding.assertUnchanged();
|
|
196
214
|
const result = await rpc.request(input.sessionId ? 'session/resume' : 'session/new', {
|
|
197
215
|
...(input.sessionId ? { sessionId: input.sessionId } : {}),
|
|
198
216
|
cwd, mcpServers: Object.entries(options.mcpServers || {}).map(([name, config]) => ({ name, command: config.command, args: config.args,
|
|
@@ -203,13 +221,18 @@ export function createDshAcpAdapter(options = {}) {
|
|
|
203
221
|
throw failure('NATIVE_PROTOCOL_ERROR', 'ACP returned an invalid session identity');
|
|
204
222
|
}
|
|
205
223
|
sessionId = resolvedId;
|
|
224
|
+
const reportedModel = result?.configOptions?.find(option => option?.id === 'model')?.currentValue || result?.models?.currentModelId;
|
|
225
|
+
runtimeMetadata = typeof reportedModel === 'string' ? { model: reportedModel } : {};
|
|
206
226
|
if (input.model !== undefined) {
|
|
207
227
|
const model = result?.configOptions?.find((option) => option?.id === 'model');
|
|
208
228
|
if (!model || !advertisesValue(model.options, input.model)) throw failure('NATIVE_UNSUPPORTED_MODEL', 'Model must match an advertised ACP model option value');
|
|
209
|
-
await rpc.request('session/set_config_option', { sessionId, configId: 'model', value: input.model });
|
|
229
|
+
const configured = await rpc.request('session/set_config_option', { sessionId, configId: 'model', value: input.model });
|
|
230
|
+
const confirmed = configured?.configOptions?.find(option => option?.id === 'model')?.currentValue;
|
|
231
|
+
runtimeMetadata = typeof confirmed === 'string' ? { model: confirmed } : {};
|
|
210
232
|
}
|
|
211
233
|
if (closing || status === 'exited') throw failure('NATIVE_ADAPTER_CLOSED', 'The dsh ACP runtime closed during session initialization');
|
|
212
234
|
status = 'idle';
|
|
235
|
+
emit({ type: 'metadata', runtimeMetadata: { ...runtimeMetadata } });
|
|
213
236
|
emit({ type: 'opened', resumed: Boolean(input.sessionId), status });
|
|
214
237
|
return snapshot();
|
|
215
238
|
} catch (error) {
|
|
@@ -239,6 +262,7 @@ export function createDshAcpAdapter(options = {}) {
|
|
|
239
262
|
}
|
|
240
263
|
if (input.expectedTurnId && input.expectedTurnId !== active?.turnId) throw failure('NATIVE_STALE_TURN', 'The expected ACP submission is no longer active');
|
|
241
264
|
if (active) throw failure('NATIVE_BUSY', 'ACP permits one in-flight prompt per session and does not support steering');
|
|
265
|
+
cwdBinding.assertUnchanged();
|
|
242
266
|
clearToolContext();
|
|
243
267
|
const turn = { turnId: randomUUID(), submissionId, text: '' };
|
|
244
268
|
active = turn;
|
|
@@ -252,11 +276,12 @@ export function createDshAcpAdapter(options = {}) {
|
|
|
252
276
|
// only the real RPC response constitutes a completed submission.
|
|
253
277
|
void Promise.resolve().then(() => {
|
|
254
278
|
if (closing) throw failure('NATIVE_ADAPTER_CLOSED', 'The adapter closed before prompt submission');
|
|
279
|
+
cwdBinding.assertUnchanged();
|
|
255
280
|
return rpc.request('session/prompt', {
|
|
256
281
|
sessionId, prompt: [{ type: 'text', text: input.text }]
|
|
257
282
|
}, { timeoutMs: 0 });
|
|
258
283
|
}).then((result) => {
|
|
259
|
-
if (active !== turn) return;
|
|
284
|
+
if (active !== turn || closing) return;
|
|
260
285
|
if (!['end_turn', 'max_tokens', 'max_turn_requests', 'refusal', 'cancelled'].includes(result?.stopReason)) {
|
|
261
286
|
throw failure('NATIVE_PROTOCOL_ERROR', 'ACP prompt returned an invalid stop reason');
|
|
262
287
|
}
|
|
@@ -267,7 +292,7 @@ export function createDshAcpAdapter(options = {}) {
|
|
|
267
292
|
emit({ type: 'completed', status: result.stopReason === 'cancelled' ? 'interrupted' : 'completed',
|
|
268
293
|
sessionId, turnId: turn.turnId, submissionId, text: turn.text, stopReason: result.stopReason });
|
|
269
294
|
}).catch((error) => {
|
|
270
|
-
if (active !== turn) return;
|
|
295
|
+
if (active !== turn || closing) return;
|
|
271
296
|
interactions.expire(turn.turnId);
|
|
272
297
|
clearToolContext();
|
|
273
298
|
active = null;
|
|
@@ -300,30 +325,48 @@ export function createDshAcpAdapter(options = {}) {
|
|
|
300
325
|
}
|
|
301
326
|
|
|
302
327
|
function close() {
|
|
303
|
-
if (
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
328
|
+
if (closePromise) return closePromise;
|
|
329
|
+
if (runtimeDisposed && status === 'closed') return Promise.resolve(snapshot());
|
|
330
|
+
// Once closure is requested, failed cleanup must never reopen this worker.
|
|
331
|
+
// Only the in-flight cleanup promise is retryable.
|
|
332
|
+
closing = true;
|
|
333
|
+
const operation = (async () => {
|
|
334
|
+
try {
|
|
335
|
+
status = 'closing';
|
|
336
|
+
interactions.expire();
|
|
337
|
+
clearToolContext();
|
|
338
|
+
const errors = [];
|
|
339
|
+
if (opening) { try { await opening; } catch {} }
|
|
340
|
+
if (rpc && !runtimeDisposed && !runtimeExited && !runtimeDisposalStarted && sessionId && capabilities.close) {
|
|
341
|
+
try { await rpc.request('session/close', { sessionId }); } catch (error) { errors.push(error); }
|
|
342
|
+
}
|
|
343
|
+
try { await disposeRuntime(); } catch (error) { errors.push(error); }
|
|
344
|
+
if (errors.length) lastError = { ...describe(errors.at(-1)), ...(!runtimeDisposed ? { uncertain: true } : {}) };
|
|
345
|
+
status = runtimeDisposed ? 'closed' : 'failed';
|
|
346
|
+
if (runtimeDisposed) {
|
|
347
|
+
active = null;
|
|
348
|
+
emit({ type: 'closed', status });
|
|
349
|
+
}
|
|
350
|
+
if (errors.length) throw new AggregateError(errors, 'ACP session or owned runtime cleanup failed');
|
|
351
|
+
return snapshot();
|
|
352
|
+
} finally { cwdBinding.release(); }
|
|
319
353
|
})();
|
|
320
|
-
|
|
354
|
+
closePromise = operation;
|
|
355
|
+
void operation.catch(() => { if (closePromise === operation) closePromise = null; });
|
|
356
|
+
return operation;
|
|
321
357
|
}
|
|
322
358
|
|
|
323
359
|
async function disposeRuntime() {
|
|
324
|
-
if (runtimeDisposed) return;
|
|
325
|
-
|
|
326
|
-
|
|
360
|
+
if (runtimeDisposed || runtimeExited) { runtimeDisposed = true; return; }
|
|
361
|
+
runtimeDisposalStarted = true;
|
|
362
|
+
try {
|
|
363
|
+
await rpc?.close();
|
|
364
|
+
runtimeDisposed = true;
|
|
365
|
+
} finally {
|
|
366
|
+
// The transport may report its own cleanup failure after observing exit.
|
|
367
|
+
// Preserve that error while retaining the independent exit receipt.
|
|
368
|
+
if (runtimeExited) runtimeDisposed = true;
|
|
369
|
+
}
|
|
327
370
|
}
|
|
328
371
|
|
|
329
372
|
return { capabilities, open, send, interrupt, close, snapshot, respond: interactions.respond };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CliError } from '../../shared/errors.mjs';
|
|
2
|
+
import { HOOK_ROOT_IDENTITY_ENV, hookRootIdentityValue } from '../../core/sessions/hook-root-identity.mjs';
|
|
2
3
|
|
|
3
4
|
export const NATIVE_PROVIDERS = Object.freeze(['codex', 'claude', 'dsh']);
|
|
4
5
|
|
|
@@ -15,5 +16,7 @@ export function nativeWorkerEnv(env, ctx, peer, owner) {
|
|
|
15
16
|
!['CODEX_THREAD_ID', 'CODEX_SESSION_ID', 'CLAUDE_SESSION_ID', 'CLAUDECODE'].includes(key)));
|
|
16
17
|
// Explicit, worker-specific coordination identity; provider credentials and
|
|
17
18
|
// the real HOME are preserved. No global configuration is rewritten.
|
|
18
|
-
return { ...clean, HCC_ROOT: ctx.root, HCC_DB: ctx.dbPath, HCC_PEER: peer,
|
|
19
|
+
return { ...clean, HCC_ROOT: ctx.root, HCC_DB: ctx.dbPath, HCC_PEER: peer,
|
|
20
|
+
[HOOK_ROOT_IDENTITY_ENV]: hookRootIdentityValue(ctx.root, ctx.initialRootIdentity || ctx.rootIdentity),
|
|
21
|
+
...(owner ? { HCC_NATIVE_OWNER: owner } : {}) };
|
|
19
22
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { redactMcpUrlInteraction } from '../mcp-url-elicitation.mjs';
|
|
2
3
|
|
|
3
4
|
// Only the owning runtime can answer. Answers, including secrets, are never
|
|
4
5
|
// placed in snapshots, events or persistent delivery receipts.
|
|
@@ -33,7 +34,7 @@ export function createNativeInteractions({ executorId = randomUUID(), isActive,
|
|
|
33
34
|
signal.addEventListener('abort', aborted, { once: true });
|
|
34
35
|
entry.cleanup = () => signal.removeEventListener('abort', aborted);
|
|
35
36
|
}
|
|
36
|
-
onChange?.({ type: 'approval', ...snapshot().find(value => value.requestId === id) });
|
|
37
|
+
onChange?.(redactMcpUrlInteraction({ type: 'approval', ...snapshot().find(value => value.requestId === id) }));
|
|
37
38
|
});
|
|
38
39
|
return promise;
|
|
39
40
|
}
|
|
@@ -1,5 +1,28 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { CliError } from '../../shared/errors.mjs';
|
|
4
|
+
import { spawnPinnedCwdProcess } from '../../process/pinned-cwd.mjs';
|
|
5
|
+
|
|
6
|
+
function assertExecutableAvailable(binary, cwd, env) {
|
|
7
|
+
const searchPath = env === undefined ? process.env.PATH : env.PATH;
|
|
8
|
+
const candidates = binary.includes('/')
|
|
9
|
+
? [path.resolve(cwd, binary)]
|
|
10
|
+
: String(searchPath === undefined ? '/usr/bin:/bin' : searchPath)
|
|
11
|
+
.split(path.delimiter).map(entry => path.resolve(cwd, entry || '.', binary));
|
|
12
|
+
let inaccessible = false;
|
|
13
|
+
for (const candidate of candidates) {
|
|
14
|
+
try {
|
|
15
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
16
|
+
if (fs.statSync(candidate).isFile()) return;
|
|
17
|
+
} catch (error) {
|
|
18
|
+
if (error?.code === 'EACCES') inaccessible = true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const code = inaccessible ? 'EACCES' : 'ENOENT';
|
|
22
|
+
throw Object.assign(new Error('Cannot start ' + binary + ': ' + code), {
|
|
23
|
+
code, path: binary, syscall: 'spawn'
|
|
24
|
+
});
|
|
25
|
+
}
|
|
3
26
|
|
|
4
27
|
// Each transport owns the child it starts. It never connects to a desktop
|
|
5
28
|
// application's private server or signals a process supplied by the caller.
|
|
@@ -18,7 +41,10 @@ export class JsonRpcProcess {
|
|
|
18
41
|
async start() {
|
|
19
42
|
if (this.child || this.closed) throw new CliError('NATIVE_TRANSPORT_STATE', 'Transport cannot be started twice');
|
|
20
43
|
const { binary, args = [], cwd, env } = this.options;
|
|
21
|
-
|
|
44
|
+
assertExecutableAvailable(binary, cwd || process.cwd(), env);
|
|
45
|
+
this.child = spawnPinnedCwdProcess(binary, args, {
|
|
46
|
+
cwd: cwd || process.cwd(), env, shell: false, stdio: ['pipe', 'pipe', 'pipe']
|
|
47
|
+
});
|
|
22
48
|
this.child.stdout.setEncoding('utf8');
|
|
23
49
|
this.child.stdout.on('data', (chunk) => this.read(chunk));
|
|
24
50
|
// Drain stderr without putting credentials or unbounded provider logs into
|
|
@@ -86,8 +112,8 @@ export class JsonRpcProcess {
|
|
|
86
112
|
const error = new CliError('NATIVE_PROTOCOL_ERROR', message, { uncertain: true });
|
|
87
113
|
this.fail(error);
|
|
88
114
|
this.reportError(error);
|
|
89
|
-
//
|
|
90
|
-
//
|
|
115
|
+
// Report failed cleanup without an unhandled rejection. Explicit callers
|
|
116
|
+
// may retry termination; the original attempt keeps its failure receipt.
|
|
91
117
|
void this.close().catch((closeError) => {
|
|
92
118
|
this.lastCloseError = closeError;
|
|
93
119
|
this.reportError(closeError);
|
|
@@ -180,6 +206,12 @@ export class JsonRpcProcess {
|
|
|
180
206
|
try { child.stdin.end(); } catch {}
|
|
181
207
|
signal('SIGTERM');
|
|
182
208
|
});
|
|
183
|
-
|
|
209
|
+
const closing = this.closePromise;
|
|
210
|
+
void closing.catch(() => {
|
|
211
|
+
// Share a pending attempt, but do not pin a transient signal/exit failure
|
|
212
|
+
// forever. A retry must still confirm this owned child's actual exit.
|
|
213
|
+
if (this.closePromise === closing) this.closePromise = null;
|
|
214
|
+
});
|
|
215
|
+
return closing;
|
|
184
216
|
}
|
|
185
217
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Preserve only counters actually reported by the upstream protocol. No token
|
|
2
|
+
// estimates, local duration substitutes, or cumulative-usage/context inference.
|
|
3
|
+
function fields(source, mapping) {
|
|
4
|
+
const result = {};
|
|
5
|
+
for (const [key, field] of Object.entries(mapping)) {
|
|
6
|
+
const value = source?.[field];
|
|
7
|
+
if (typeof value === 'number' && Number.isFinite(value) && value >= 0) result[key] = value;
|
|
8
|
+
}
|
|
9
|
+
return result;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function codexUsage(params, observedAt = Date.now()) {
|
|
13
|
+
const usage = params?.tokenUsage;
|
|
14
|
+
if (!usage || typeof usage !== 'object') return null;
|
|
15
|
+
const counters = fields(usage.total, { inputTokens: 'inputTokens', outputTokens: 'outputTokens', cachedInputTokens: 'cachedInputTokens', totalTokens: 'totalTokens' });
|
|
16
|
+
Object.assign(counters, fields(usage, { contextWindow: 'modelContextWindow' }));
|
|
17
|
+
return Object.keys(counters).length ? { source: 'codex thread/tokenUsage/updated', scope: 'session', ...(params.turnId ? { turnId: params.turnId } : {}), observedAt, ...counters } : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function claudeUsage(message, turnId, observedAt = Date.now()) {
|
|
21
|
+
const counters = { ...fields(message?.usage, { inputTokens: 'input_tokens', outputTokens: 'output_tokens', cachedInputTokens: 'cache_read_input_tokens', totalTokens: 'total_tokens' }),
|
|
22
|
+
...fields(message, { durationMs: 'duration_ms' }) };
|
|
23
|
+
return Object.keys(counters).length ? { source: 'claude SDK result', scope: 'turn', ...(turnId ? { turnId } : {}), observedAt, ...counters } : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function acpUsage(update, observedAt = Date.now()) {
|
|
27
|
+
if (update?.sessionUpdate !== 'usage_update') return null;
|
|
28
|
+
const counters = fields(update, { contextTokens: 'used', contextWindow: 'size' });
|
|
29
|
+
return Object.keys(counters).length ? { source: 'ACP session/update usage_update', scope: 'session', observedAt, ...counters } : null;
|
|
30
|
+
}
|
package/lib/process/identity.mjs
CHANGED
|
@@ -97,6 +97,10 @@ function macTokenFormat(token) {
|
|
|
97
97
|
if (legacy && BigInt(legacy[1]) > 0n && Number(legacy[2]) >= 0 && Number(legacy[2]) <= 999999) return 'legacy-wall-clock';
|
|
98
98
|
const current = /^darwin:([^:]+):(.+)$/.exec(token);
|
|
99
99
|
if (current && MAC_BOOT_UUID.test(current[1]) && new RegExp(`^${start}$`).test(current[2])) return 'boot-session';
|
|
100
|
+
// Earlier workbench candidates used a different prefix for the same OS
|
|
101
|
+
// evidence. A prefix change alone must not prove that their owner exited.
|
|
102
|
+
const preview = /^mac:([^:]+):(.+)$/.exec(token);
|
|
103
|
+
if (preview && MAC_BOOT_UUID.test(preview[1]) && new RegExp(`^${start}$`).test(preview[2])) return 'preview-boot-session';
|
|
100
104
|
return null;
|
|
101
105
|
}
|
|
102
106
|
|