@logicseek/hello-cc 1.0.1 → 1.0.2

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 (97) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +4 -0
  3. package/README.zh-CN.md +4 -0
  4. package/docs/README.md +9 -0
  5. package/docs/README.zh-CN.md +9 -0
  6. package/docs/commands.md +53 -0
  7. package/docs/commands.zh-CN.md +48 -0
  8. package/docs/dsh.md +137 -0
  9. package/docs/dsh.zh-CN.md +139 -0
  10. package/docs/mcp.md +9 -0
  11. package/docs/mcp.zh-CN.md +23 -0
  12. package/docs/native.md +295 -0
  13. package/docs/native.zh-CN.md +253 -0
  14. package/docs/web-handoff.zh-CN.md +53 -0
  15. package/lib/cli/app.mjs +25 -6
  16. package/lib/cli/commands/doctor.mjs +17 -2
  17. package/lib/cli/commands/dsh.mjs +79 -0
  18. package/lib/cli/commands/hook.mjs +241 -90
  19. package/lib/cli/commands/lock.mjs +34 -19
  20. package/lib/cli/commands/mcp.mjs +28 -0
  21. package/lib/cli/commands/native.mjs +130 -0
  22. package/lib/cli/commands/run.mjs +66 -21
  23. package/lib/cli/commands/task.mjs +6 -3
  24. package/lib/cli-args.mjs +4 -0
  25. package/lib/coordination-state.mjs +15 -1
  26. package/lib/core/coordination/event-retention.mjs +14 -2
  27. package/lib/core/coordination/gc-plan.mjs +3 -13
  28. package/lib/core/coordination/locks.mjs +45 -0
  29. package/lib/core/coordination/results.mjs +79 -0
  30. package/lib/core/peers/bindings.mjs +9 -3
  31. package/lib/diagnostics/codex.mjs +121 -0
  32. package/lib/integrations/codex-interactions.mjs +114 -0
  33. package/lib/integrations/dsh-collaboration.mjs +170 -0
  34. package/lib/integrations/dsh-cordis.d.ts +19 -0
  35. package/lib/integrations/dsh-cordis.mjs +177 -0
  36. package/lib/integrations/dsh.bundle.yml +7 -0
  37. package/lib/integrations/dsh.mjs +287 -0
  38. package/lib/integrations/native/claude.mjs +451 -0
  39. package/lib/integrations/native/codex.mjs +242 -0
  40. package/lib/integrations/native/dsh-acp.mjs +343 -0
  41. package/lib/integrations/native/index.mjs +19 -0
  42. package/lib/integrations/native/interactions.mjs +53 -0
  43. package/lib/integrations/native/jsonrpc.mjs +185 -0
  44. package/lib/integrations/peers/identity.mjs +36 -10
  45. package/lib/integrations/peers/processes.mjs +22 -8
  46. package/lib/integrations/providers.mjs +6 -2
  47. package/lib/mcp/scope.mjs +104 -0
  48. package/lib/mcp/stdio.mjs +80 -0
  49. package/lib/mcp/tools.mjs +197 -0
  50. package/lib/process/identity.mjs +3 -1
  51. package/lib/release/release-notes.mjs +9 -0
  52. package/lib/runtime/native/client.mjs +38 -0
  53. package/lib/runtime/native/service.mjs +511 -0
  54. package/lib/runtime/native/store.mjs +121 -0
  55. package/lib/ui/help.mjs +5 -2
  56. package/lib/web/adoption-state.mjs +55 -0
  57. package/lib/web/auto-attach.mjs +18 -2
  58. package/lib/web/codex-app-server.mjs +557 -0
  59. package/lib/web/codex-sessions.mjs +287 -0
  60. package/lib/web/control-lease.mjs +204 -0
  61. package/lib/web/http-routes.mjs +197 -15
  62. package/lib/web/http.mjs +5 -4
  63. package/lib/web/native-sessions.mjs +286 -0
  64. package/lib/web/project-contexts.mjs +2 -1
  65. package/lib/web/pty-sessions.mjs +60 -33
  66. package/lib/web/runtime-main.mjs +98 -6
  67. package/lib/web/session-serialize.mjs +0 -0
  68. package/lib/web/task-results.mjs +76 -0
  69. package/lib/web/tmux-sessions.mjs +92 -11
  70. package/lib/web/tmux-stream.mjs +2 -2
  71. package/lib/web/ui-codex.mjs +550 -0
  72. package/lib/web/ui-command-palette.mjs +119 -0
  73. package/lib/web/ui-handoff.mjs +102 -0
  74. package/lib/web/ui-history.mjs +118 -0
  75. package/lib/web/ui-i18n.mjs +841 -0
  76. package/lib/web/ui-interactions.mjs +78 -0
  77. package/lib/web/ui-login.mjs +101 -0
  78. package/lib/web/ui-native.mjs +165 -0
  79. package/lib/web/ui-preferences.mjs +278 -0
  80. package/lib/web/ui-review.mjs +132 -0
  81. package/lib/web/ui-template.mjs +1106 -576
  82. package/lib/web/ui-terminal-find.mjs +98 -0
  83. package/lib/web/ui-workbench.mjs +94 -0
  84. package/lib/web/ui-workspace.mjs +222 -0
  85. package/package.json +11 -3
  86. package/scripts/check-free-identifiers.mjs +67 -8
  87. package/scripts/codex-diagnostics-installed-acceptance.mjs +72 -0
  88. package/scripts/dsh-cordis-acceptance.mjs +200 -0
  89. package/scripts/dsh-installed-acceptance.mjs +247 -0
  90. package/scripts/dsh-official-acceptance.mjs +180 -0
  91. package/scripts/dsh-web-startup-acceptance.mjs +87 -0
  92. package/scripts/github-release.mjs +3 -1
  93. package/scripts/native-live-acceptance.mjs +369 -0
  94. package/scripts/regression.mjs +243 -89
  95. package/scripts/web-handoff-installed-acceptance.mjs +98 -0
  96. package/scripts/web-native-interaction-acceptance.mjs +204 -0
  97. package/scripts/web-native-model-acceptance.mjs +138 -0
@@ -0,0 +1,511 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import http from 'node:http';
4
+ import net from 'node:net';
5
+ import { randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
6
+ import { CliError } from '../../shared/errors.mjs';
7
+ import { redactSecrets } from '../../shared/redact.mjs';
8
+ import { fileLockEndpoint } from '../../shared/file-lock.mjs';
9
+ import { shellQuoteArg } from '../../format.mjs';
10
+ import { commandPath } from '../../cli-runtime.mjs';
11
+ import { tx } from '../../db/schema.mjs';
12
+ import { createScopedMcpConfig } from '../../mcp/scope.mjs';
13
+ import { createNativeAdapter, nativeWorkerEnv, NATIVE_PROVIDERS } from '../../integrations/native/index.mjs';
14
+ import { createNativeStore, nativePaths, readNativePointer, writeNativePointer } from './store.mjs';
15
+
16
+ function publicError(error) {
17
+ return redactSecrets({ code: error.code || 'NATIVE_REQUEST_FAILED', message: error.message || 'Native operation failed', extra: error.extra || {} });
18
+ }
19
+ function reply(response, status, result) {
20
+ response.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
21
+ response.end(JSON.stringify(result));
22
+ }
23
+ async function jsonBody(request) {
24
+ request.setEncoding('utf8');
25
+ let data = '';
26
+ for await (const chunk of request) {
27
+ data += chunk;
28
+ if (Buffer.byteLength(data) > 1024 * 1024) throw new CliError('BAD_ARGS', 'Native request body is too large');
29
+ }
30
+ try {
31
+ const value = JSON.parse(data || '{}');
32
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('not an object');
33
+ return value;
34
+ } catch { throw new CliError('BAD_ARGS', 'Native request body must be a JSON object'); }
35
+ }
36
+
37
+ // Business state stays in mesh.db; owned provider lifecycle and receipts have
38
+ // their own store. No desktop-private servers or foreign processes are used.
39
+ export async function startNativeService(ctx, deps, options = {}) {
40
+ const paths = nativePaths(ctx, { create: true });
41
+ const ownership = net.createServer((socket) => socket.end());
42
+ const endpoint = fileLockEndpoint(path.join(paths.dir, 'service-owner'));
43
+ await new Promise((resolve, reject) => {
44
+ ownership.once('error', () => reject(new CliError('NATIVE_RUNTIME_IN_USE', 'A native runtime already owns this project, or its ownership port is unavailable.')));
45
+ ownership.listen(endpoint.port, '127.0.0.1', resolve);
46
+ });
47
+ let store;
48
+ let mesh;
49
+ try {
50
+ store = createNativeStore(ctx);
51
+ mesh = deps.connect(ctx, { migrateRegistered: false });
52
+ store.disconnected();
53
+ } catch (error) { ownership.close(); store?.close(); mesh?.close(); throw error; }
54
+ const workers = new Map();
55
+ const creating = new Set();
56
+ const operations = new Set();
57
+ const generation = randomUUID();
58
+ const token = randomBytes(32).toString('hex');
59
+ let stopping = false;
60
+ let storeClosed = false;
61
+ let shutdownPromise;
62
+ let shutdownError = null;
63
+ let pollTimer;
64
+ const adapterFactory = options.adapterFactory || createNativeAdapter;
65
+ const identity = deps.liveProcessIdentity(process.pid);
66
+
67
+ function track(promise) {
68
+ operations.add(promise);
69
+ void promise.finally(() => operations.delete(promise)).catch(() => {});
70
+ return promise;
71
+ }
72
+ function requireRunning() {
73
+ if (stopping) throw new CliError('NATIVE_RUNTIME_STOPPING', 'Native runtime is stopping');
74
+ }
75
+ function assertOwner(worker) {
76
+ const binding = mesh.prepare('SELECT * FROM peer_bindings WHERE peer=?').get(worker.peer);
77
+ if (!binding || binding.transport !== 'native' || binding.runtime_target !== worker.owner || binding.provider !== worker.provider) {
78
+ throw new CliError('NATIVE_PEER_IN_USE', 'Peer ownership changed; its current binding has been preserved');
79
+ }
80
+ }
81
+ function assertRequestedOwner(worker, input = {}) {
82
+ assertOwner(worker);
83
+ const snapshot = worker.adapter?.snapshot();
84
+ if ((input.generation !== undefined && input.generation !== generation) ||
85
+ (input.owner !== undefined && input.owner !== worker.owner) ||
86
+ (input.provider !== undefined && input.provider !== worker.provider) ||
87
+ (input.sessionId !== undefined && input.sessionId !== (snapshot?.sessionId || null))) {
88
+ throw new CliError('NATIVE_OWNER_CHANGED', 'Native worker identity changed; refresh its current state before controlling it');
89
+ }
90
+ }
91
+ function save(worker) {
92
+ if (!worker.adapter || storeClosed) return;
93
+ const snapshot = worker.adapter.snapshot();
94
+ tx(mesh, () => {
95
+ assertOwner(worker);
96
+ if (snapshot.sessionId && mesh.prepare('SELECT peer FROM peer_bindings WHERE provider=? AND (provider_session_id=? OR provider_session_name=?) AND peer<>?')
97
+ .get(worker.provider, snapshot.sessionId, snapshot.sessionId, worker.peer)) {
98
+ throw new CliError('PROVIDER_SESSION_IN_USE', 'Native session became bound to another peer; that binding has been preserved');
99
+ }
100
+ const exited = ['closed', 'disconnected', 'exited', 'stopped'].includes(snapshot.status);
101
+ const status = exited ? 'exited' : worker.quarantined || ['error', 'uncertain', 'failed'].includes(snapshot.status) ? 'blocked'
102
+ : worker.active || ['busy', 'running', 'queued', 'opening', 'closing', 'interrupting'].includes(snapshot.status) ? 'working' : 'idle';
103
+ deps.upsertCanonicalPeerBinding(mesh, { peer: worker.peer, provider: worker.provider,
104
+ provider_session_id: snapshot.sessionId || null, provider_session_name: null,
105
+ resume_mode: snapshot.sessionId ? 'resume' : 'new', resume_arg: snapshot.sessionId || null,
106
+ command: null, transport: 'native', runtime_session_id: worker.peer, runtime_target: worker.owner });
107
+ deps.upsertPeer(mesh, { id: worker.peer, kind: worker.provider, role: 'native-worker', worktree: worker.cwd,
108
+ branch: worker.branch, pid: exited ? null : process.pid, processIdentity: exited ? null : identity,
109
+ status, capabilities: JSON.stringify(snapshot.capabilities || worker.adapter.capabilities) });
110
+ });
111
+ store.saveWorker({ peer: worker.peer, provider: worker.provider, cwd: worker.cwd, ...snapshot });
112
+ }
113
+ function belongs(event, active) {
114
+ if (!active) return false;
115
+ if (event.submissionId && event.submissionId !== active.delivery.submission_id) return false;
116
+ if (event.turnId && active.turnId && event.turnId !== active.turnId) return false;
117
+ return event.submissionId === active.delivery.submission_id || Boolean(event.turnId && event.turnId === active.turnId);
118
+ }
119
+ function record(worker, event) {
120
+ if (storeClosed || worker.closing) return;
121
+ try {
122
+ assertOwner(worker);
123
+ const safe = redactSecrets(event);
124
+ if (event.type !== 'delta' && event.type !== 'output') store.event(worker.peer, safe);
125
+ const active = worker.active;
126
+ const associated = belongs(event, active);
127
+ if (associated && event.turnId) active.turnId = event.turnId;
128
+ if (event.type === 'message' && associated && typeof event.text === 'string') worker.output.push(event.text);
129
+ if (associated && ['message', 'delta', 'output'].includes(event.type)) {
130
+ store.updateDelivery(active.delivery.id, 'accepted', active.turnId, { evidence: 'provider_output_observed' });
131
+ }
132
+ if (event.type === 'completed' && associated) {
133
+ const completed = event.status === 'completed';
134
+ // Reply and ACK commit together. A persistence failure cannot claim
135
+ // completion or put a potentially consumed prompt back in the queue.
136
+ tx(mesh, () => {
137
+ assertOwner(worker);
138
+ if (completed) {
139
+ const text = worker.output.join('\n\n') || event.text;
140
+ // A reply is delivered for context, but must not create another
141
+ // automatic reply and an unbounded loop between native workers.
142
+ if (text && active.message.kind !== 'reply') deps.sendMessage(mesh, worker.peer, active.message.sender, active.message.task_id, 'reply', text,
143
+ { reply_to: active.message.id, thread_id: active.message.thread_id || active.message.id });
144
+ deps.ackMessage(mesh, worker.peer, active.message);
145
+ }
146
+ deps.addEvent(mesh, `native.delivery.${completed ? 'completed' : 'failed'}`, worker.peer, active.message.task_id,
147
+ { message_id: active.message.id, submission_id: active.delivery.submission_id, turn_id: event.turnId || active.turnId });
148
+ });
149
+ store.updateDelivery(active.delivery.id, completed ? 'completed' : 'failed', event.turnId || active.turnId,
150
+ { status: event.status, error: event.error || null });
151
+ worker.active = null;
152
+ worker.output = [];
153
+ }
154
+ if (event.type === 'error' && active && (associated || (!event.turnId && !event.submissionId))) {
155
+ store.updateDelivery(active.delivery.id, 'uncertain', active.turnId, safe);
156
+ worker.active = null;
157
+ worker.output = [];
158
+ worker.quarantined = true;
159
+ }
160
+ save(worker);
161
+ } catch (error) {
162
+ worker.quarantined = true;
163
+ console.error(JSON.stringify(publicError(error)));
164
+ void closeWorker(worker.peer).catch((closeError) => console.error(JSON.stringify(publicError(closeError))));
165
+ }
166
+ }
167
+ function ingest(worker, initial = false) {
168
+ const messages = initial ? deps.queryInbox(mesh, worker.peer, false, 1_000_000)
169
+ : mesh.prepare(`SELECT * FROM messages WHERE id>? AND sender<>? AND
170
+ (recipient IS NULL OR recipient='' OR recipient='all' OR recipient=?) ORDER BY id LIMIT 1000`)
171
+ .all(worker.lastMessageId, worker.peer, worker.peer);
172
+ for (const message of messages) {
173
+ if (message.sender !== worker.peer) store.queue(worker.peer, message.id, randomUUID());
174
+ if (!initial) worker.lastMessageId = message.id;
175
+ }
176
+ }
177
+ function createWorker(input) {
178
+ const operation = createWorkerNow(input);
179
+ const worker = workers.get(input.peer);
180
+ if (worker) worker.creation = operation;
181
+ return track(operation);
182
+ }
183
+ async function createWorkerNow(input) {
184
+ requireRunning();
185
+ const peer = typeof input.peer === 'string' ? input.peer : '';
186
+ const provider = input.provider;
187
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(peer) || peer === 'all' || !NATIVE_PROVIDERS.includes(provider)) {
188
+ throw new CliError('BAD_ARGS', 'Use a valid peer name and provider codex, claude, or dsh');
189
+ }
190
+ for (const key of ['binary', 'model', 'resume']) {
191
+ if (input[key] !== undefined && (typeof input[key] !== 'string' || !input[key].trim())) {
192
+ throw new CliError('BAD_ARGS', `Worker ${key} must be a nonempty string`);
193
+ }
194
+ }
195
+ if (provider === 'claude' && input.binary !== undefined) throw new CliError('BAD_ARGS', 'Claude workers use the Agent SDK; --binary is available for Codex and dsh');
196
+ if (workers.has(peer) || creating.has(peer)) throw new CliError('NATIVE_WORKER_EXISTS', `Native worker is already open: ${peer}`);
197
+ const previous = store.worker(peer);
198
+ let sessionId;
199
+ if (input.resume) {
200
+ if (!previous || previous.provider !== provider || !previous.session_id ||
201
+ (input.resume !== 'last' && input.resume !== previous.session_id)) {
202
+ throw new CliError('NATIVE_SESSION_NOT_OWNED', 'Only a session previously created by this native runtime can be resumed');
203
+ }
204
+ sessionId = previous.session_id;
205
+ } else if (previous?.session_id) throw new CliError('NATIVE_RESUME_REQUIRED', 'This peer has a saved session. Use --resume last or choose a new peer name.');
206
+ if (previous && previous.provider !== provider) throw new CliError('NATIVE_SESSION_NOT_OWNED', 'Peer belongs to a different native provider');
207
+ if (input.cwd !== undefined && typeof input.cwd !== 'string') throw new CliError('BAD_ARGS', 'Worker cwd must be a directory');
208
+ const cwd = fs.realpathSync(input.cwd || ctx.root);
209
+ if (!fs.statSync(cwd).isDirectory()) throw new CliError('BAD_ARGS', 'Worker cwd must be a directory');
210
+ const worker = { peer, provider, cwd, branch: deps.detectBranch(cwd), owner: `native:${generation}:${peer}:${randomUUID()}`,
211
+ active: null, output: [], quarantined: false, closing: false, dispatch: null, closePromise: null };
212
+ // Reserve identity before starting the child so installed hooks can only
213
+ // heartbeat this exact worker. Every subsequent write checks this owner.
214
+ tx(mesh, () => {
215
+ const binding = mesh.prepare('SELECT * FROM peer_bindings WHERE peer=?').get(peer);
216
+ const existing = mesh.prepare('SELECT * FROM peers WHERE id=?').get(peer);
217
+ if ((binding && (binding.transport !== 'native' || binding.provider !== provider || !previous)) || (existing && !binding)) {
218
+ throw new CliError('NATIVE_PEER_IN_USE', 'Peer is already owned by another transport');
219
+ }
220
+ if (sessionId && mesh.prepare('SELECT peer FROM peer_bindings WHERE provider=? AND (provider_session_id=? OR provider_session_name=?) AND peer<>?').get(provider, sessionId, sessionId, peer)) {
221
+ throw new CliError('PROVIDER_SESSION_IN_USE', 'The saved native session is bound to another peer');
222
+ }
223
+ deps.upsertPeer(mesh, { id: peer, kind: provider, role: 'native-worker', worktree: cwd, branch: worker.branch,
224
+ pid: process.pid, processIdentity: identity, status: 'working', capabilities: '' });
225
+ deps.upsertCanonicalPeerBinding(mesh, { peer, provider, provider_session_id: sessionId || null,
226
+ provider_session_name: null, resume_mode: sessionId ? 'resume' : 'new', resume_arg: sessionId || null,
227
+ command: null, transport: 'native', runtime_session_id: peer, runtime_target: worker.owner });
228
+ assertOwner(worker);
229
+ worker.lastMessageId = mesh.prepare('SELECT COALESCE(MAX(id),0) AS id FROM messages').get().id;
230
+ ingest(worker, true);
231
+ });
232
+ store.saveWorker({ peer, provider, cwd, sessionId, status: 'opening' });
233
+ creating.add(peer);
234
+ workers.set(peer, worker);
235
+ try {
236
+ worker.mcpCapability = (options.mcpConfigFactory || createScopedMcpConfig)({ root: ctx.root, dbPath: ctx.dbPath,
237
+ peer, executorId: worker.owner, ownerIdentity: identity,
238
+ binding: { transport: 'native', runtimeSessionId: peer, runtimeTarget: worker.owner } });
239
+ worker.adapter = await adapterFactory(provider, { binary: input.binary || undefined, cwd,
240
+ executorId: worker.owner, interactive: true, mcpServers: { hello_cc_scoped: worker.mcpCapability.config },
241
+ env: nativeWorkerEnv(process.env, ctx, peer, worker.owner), onEvent: (event) => {
242
+ if (worker.adapter) record(worker, event);
243
+ } });
244
+ requireRunning();
245
+ if (worker.closing) throw new CliError('NATIVE_WORKER_CLOSING', 'Worker was closed while opening');
246
+ assertOwner(worker);
247
+ await worker.adapter.open({ sessionId, model: input.model || undefined });
248
+ requireRunning();
249
+ save(worker);
250
+ deps.addEvent(mesh, 'native.worker.opened', peer, null, { provider, session_id: worker.adapter.snapshot().sessionId || null, resumed: Boolean(sessionId) });
251
+ return { peer, ...worker.adapter.snapshot() };
252
+ } catch (error) {
253
+ worker.closing = true;
254
+ worker.mcpCapability?.dispose();
255
+ let cleanupError;
256
+ try { await worker.adapter?.close(); } catch (closeError) {
257
+ cleanupError = closeError;
258
+ worker.quarantined = true;
259
+ console.error(JSON.stringify(publicError(closeError)));
260
+ }
261
+ if (!cleanupError) workers.delete(peer);
262
+ try {
263
+ if (worker.adapter) save(worker);
264
+ else tx(mesh, () => { assertOwner(worker); mesh.prepare("UPDATE peers SET status='blocked' WHERE id=?").run(peer); });
265
+ } catch {}
266
+ store.saveWorker({ peer, provider, cwd, ...(worker.adapter?.snapshot() || {}), status: 'error' });
267
+ if (cleanupError) throw new CliError('NATIVE_CLOSE_FAILED', 'Worker initialization failed and its owned provider exit was not confirmed',
268
+ { uncertain: true, initialization: publicError(error), cleanup: publicError(cleanupError) });
269
+ throw error;
270
+ } finally { creating.delete(peer); }
271
+ }
272
+ function context(worker, message) {
273
+ const command = ['env', `HCC_PEER=${worker.peer}`, `HCC_ROOT=${ctx.root}`, `HCC_DB=${ctx.dbPath}`,
274
+ process.execPath, commandPath()].map(shellQuoteArg).join(' ');
275
+ const tasks = mesh.prepare("SELECT id,status,owner,title FROM tasks WHERE status NOT IN ('done','abandoned') ORDER BY id DESC LIMIT 8").all();
276
+ const locks = mesh.prepare('SELECT resource,owner,task_id FROM locks WHERE expires_at>? ORDER BY resource LIMIT 8').all(Math.floor(Date.now() / 1000));
277
+ return ['[hello-cc native coordination]', `peer: ${worker.peer}`, `project: ${ctx.root}`,
278
+ 'Use the scoped hello_cc_scoped MCP tools for HCC state, tasks, locks, results and handoffs. Continue your owned task until done, blocked or handed off.',
279
+ `Use this exact prefix for HCC commands: ${command}`,
280
+ 'Only the message below is being delivered in this turn. Do not independently consume or acknowledge other inbox messages.',
281
+ message.kind === 'reply'
282
+ ? 'This message is a reply. Use it to continue your work; do not send another acknowledgement reply. Final output remains in provider events.'
283
+ : 'Your final response will be saved automatically as a reply to this message; no separate reply command is needed.',
284
+ '[open tasks]', ...tasks.map((row) => JSON.stringify(row)), '[active locks]', ...locks.map((row) => JSON.stringify(row))].join('\n');
285
+ }
286
+ async function dispatch(worker, delivery, message) {
287
+ try {
288
+ const receipt = await worker.adapter.send({ text: `${context(worker, message)}\n\nMessage #${message.id} (${message.kind}) from ${message.sender}:\n${message.body}`,
289
+ submissionId: delivery.submission_id });
290
+ if (worker.active?.delivery.id === delivery.id) {
291
+ worker.active.turnId = receipt.turnId || worker.active.turnId;
292
+ const current = store.delivery(worker.peer, message.id);
293
+ const state = receipt.status === 'accepted' || current.state === 'accepted' ? 'accepted' : 'submitted';
294
+ store.updateDelivery(delivery.id, state, worker.active.turnId, { receipt_status: receipt.status });
295
+ }
296
+ } catch (error) {
297
+ if (!storeClosed) {
298
+ const current = store.delivery(worker.peer, message.id);
299
+ if (current && !['completed', 'failed'].includes(current.state)) {
300
+ store.updateDelivery(delivery.id, error.extra?.uncertain || current.state === 'uncertain' ? 'uncertain' : 'failed', null, publicError(error));
301
+ }
302
+ }
303
+ if (worker.active?.delivery.id === delivery.id) { worker.active = null; worker.output = []; }
304
+ worker.quarantined = true;
305
+ }
306
+ if (!worker.closing) { try { save(worker); } catch (error) { record(worker, { type: 'error', ...publicError(error) }); } }
307
+ }
308
+ async function poll() {
309
+ if (stopping) return;
310
+ for (const worker of workers.values()) {
311
+ try {
312
+ if (worker.closing || creating.has(worker.peer)) continue;
313
+ save(worker);
314
+ ingest(worker);
315
+ if (worker.active || worker.dispatch || worker.quarantined) continue;
316
+ if (!['idle', 'ready', 'completed'].includes(worker.adapter.snapshot().status)) continue;
317
+ const delivery = store.pending(worker.peer);
318
+ if (!delivery) continue;
319
+ const message = mesh.prepare('SELECT * FROM messages WHERE id=?').get(delivery.message_id);
320
+ if (!message) { store.updateDelivery(delivery.id, 'failed', null, { reason: 'source_message_missing' }); continue; }
321
+ store.updateDelivery(delivery.id, 'dispatching');
322
+ worker.active = { message, delivery, turnId: null };
323
+ worker.output = [];
324
+ const operation = dispatch(worker, delivery, message);
325
+ worker.dispatch = operation;
326
+ void track(operation).finally(() => { worker.dispatch = null; }).catch(() => {});
327
+ } catch (error) { record(worker, { type: 'error', ...publicError(error) }); }
328
+ }
329
+ }
330
+ function closeWorker(peer) {
331
+ const worker = workers.get(peer);
332
+ if (!worker) return Promise.reject(new CliError('NATIVE_WORKER_NOT_FOUND', `Native worker is not open: ${peer}`));
333
+ if (worker.closePromise) return worker.closePromise;
334
+ worker.closing = true;
335
+ worker.mcpCapability?.dispose();
336
+ if (worker.active && !storeClosed) {
337
+ store.updateDelivery(worker.active.delivery.id, 'uncertain', worker.active.turnId, { reason: 'worker_closed' });
338
+ worker.active = null;
339
+ }
340
+ const closing = (async () => {
341
+ // A pending factory detects closing and disposes its late child itself.
342
+ const adapterAtClose = worker.adapter;
343
+ await adapterAtClose?.close();
344
+ if (worker.creation) await worker.creation.catch(() => {});
345
+ // Factories can resolve after close begins. Confirm their cleanup even
346
+ // when initialization itself rejected; its cleanup may also have failed.
347
+ if (worker.adapter && worker.adapter !== adapterAtClose) await worker.adapter.close();
348
+ try { save(worker); } catch (error) { if (error.code !== 'NATIVE_PEER_IN_USE') throw error; }
349
+ if (!creating.has(peer)) workers.delete(peer);
350
+ return { peer, status: 'closed' };
351
+ })();
352
+ worker.closePromise = track(closing);
353
+ void closing.catch((error) => {
354
+ worker.quarantined = true;
355
+ worker.closePromise = null;
356
+ if (!storeClosed) store.saveWorker({ peer: worker.peer, provider: worker.provider, cwd: worker.cwd,
357
+ ...worker.adapter?.snapshot(), status: 'uncertain' });
358
+ console.error(JSON.stringify(publicError(error)));
359
+ });
360
+ return worker.closePromise;
361
+ }
362
+ async function route(request, response) {
363
+ if (request.headers.origin) return reply(response, 403, { ok: false, error: { code: 'NATIVE_ORIGIN_REFUSED', message: 'Native control is a local authenticated API' } });
364
+ const provided = Buffer.from(String(request.headers.authorization || ''));
365
+ const expected = Buffer.from(`Bearer ${token}`);
366
+ if (provided.length !== expected.length || !timingSafeEqual(provided, expected)) {
367
+ return reply(response, 401, { ok: false, error: { code: 'NATIVE_UNAUTHORIZED', message: 'Invalid native runtime credential' } });
368
+ }
369
+ try {
370
+ const url = new URL(request.url, 'http://localhost');
371
+ let result;
372
+ if (request.method === 'POST' && !['/down', '/close'].includes(url.pathname)) requireRunning();
373
+ if (request.method === 'GET' && url.pathname === '/status') {
374
+ result = { generation, root: fs.realpathSync(ctx.root), meshDb: path.resolve(ctx.dbPath), pid: process.pid, stopping, shutdown_error: shutdownError,
375
+ workers: store.workers().map((row) => ({ ...row, owned: workers.has(row.peer),
376
+ owner: workers.get(row.peer)?.owner || null,
377
+ ...(workers.has(row.peer) && workers.get(row.peer).adapter ? { capabilities: workers.get(row.peer).adapter.snapshot().capabilities,
378
+ quarantined: workers.get(row.peer).quarantined } : {}) })) };
379
+ } else if (request.method === 'GET' && /^\/workers\/[^/]+\/state$/.test(url.pathname)) {
380
+ const peer = decodeURIComponent(url.pathname.split('/')[2]);
381
+ const after = Number(url.searchParams.get('after') || 0);
382
+ if (!Number.isSafeInteger(after) || after < 0) throw new CliError('BAD_ARGS', 'Event cursor must be a nonnegative integer');
383
+ const worker = workers.get(peer);
384
+ if (!worker?.adapter) throw new CliError('NATIVE_WORKER_NOT_FOUND', 'Target native worker is not open');
385
+ assertRequestedOwner(worker, {
386
+ ...(url.searchParams.has('generation') ? { generation: url.searchParams.get('generation') } : {}),
387
+ ...(url.searchParams.has('owner') ? { owner: url.searchParams.get('owner') } : {})
388
+ });
389
+ const snapshot = worker.adapter.snapshot();
390
+ result = { root: fs.realpathSync(ctx.root), meshDb: path.resolve(ctx.dbPath), generation,
391
+ peer, provider: worker.provider, owner: worker.owner, cwd: worker.cwd, snapshot,
392
+ quarantined: worker.quarantined, closing: worker.closing,
393
+ active_delivery: worker.active ? { message_id: worker.active.message.id,
394
+ submission_id: worker.active.delivery.submission_id, turn_id: worker.active.turnId } : null,
395
+ events: store.events(peer, after), deliveries: store.deliveries(peer) };
396
+ } else if (request.method === 'GET' && url.pathname === '/deliveries') result = store.deliveries(url.searchParams.get('peer'));
397
+ else if (request.method === 'GET' && url.pathname === '/events') {
398
+ const after = Number(url.searchParams.get('after') || 0);
399
+ if (!Number.isSafeInteger(after) || after < 0) throw new CliError('BAD_ARGS', 'Event cursor must be a nonnegative integer');
400
+ result = store.events(url.searchParams.get('peer') || '', after);
401
+ } else if (request.method === 'POST' && url.pathname === '/workers') {
402
+ const input = await jsonBody(request);
403
+ requireRunning();
404
+ result = await createWorker(input);
405
+ } else if (request.method === 'POST' && url.pathname === '/send') {
406
+ const input = await jsonBody(request);
407
+ requireRunning();
408
+ const worker = workers.get(input.peer);
409
+ if (!worker || worker.closing || creating.has(input.peer)) throw new CliError('NATIVE_WORKER_NOT_FOUND', 'Target native worker is not open');
410
+ if (worker.quarantined) throw new CliError('NATIVE_WORKER_UNCERTAIN', 'Worker needs inspection and an explicit close/resume before more submissions');
411
+ if (typeof input.body !== 'string' || !input.body.trim() || typeof input.from !== 'string' || !input.from.trim() ||
412
+ (input.taskId != null && (!Number.isSafeInteger(input.taskId) || input.taskId < 1))) throw new CliError('BAD_ARGS', 'Message needs a sender, nonempty body, and valid task ID');
413
+ assertRequestedOwner(worker, input);
414
+ if (input.submissionId !== undefined && (typeof input.submissionId !== 'string' ||
415
+ !/^[A-Za-z0-9_-]{8,100}$/.test(input.submissionId))) {
416
+ throw new CliError('BAD_ARGS', 'Submission ID must contain 8 to 100 letters, digits, underscores, or hyphens');
417
+ }
418
+ if (input.submissionId) {
419
+ const previous = store.db.prepare('SELECT * FROM deliveries WHERE submission_id=?').get(input.submissionId);
420
+ if (previous) {
421
+ const message = mesh.prepare('SELECT * FROM messages WHERE id=?').get(previous.message_id);
422
+ if (previous.peer !== input.peer || !message || message.sender !== input.from ||
423
+ message.recipient !== input.peer || message.body !== input.body ||
424
+ (message.task_id || null) !== (input.taskId || null)) {
425
+ throw new CliError('NATIVE_SUBMISSION_MISMATCH', 'Submission ID is already used by a different message');
426
+ }
427
+ reply(response, 200, { ok: true, data: { message_id: previous.message_id,
428
+ submission_id: previous.submission_id, state: previous.state } });
429
+ return;
430
+ }
431
+ }
432
+ const messageId = deps.sendMessage(mesh, input.from, input.peer, input.taskId || null, 'ask', input.body);
433
+ const delivery = store.queue(input.peer, messageId, input.submissionId || randomUUID());
434
+ result = { message_id: messageId, submission_id: delivery.submission_id, state: delivery.state };
435
+ } else if (request.method === 'POST' && url.pathname === '/interrupt') {
436
+ const input = await jsonBody(request);
437
+ requireRunning();
438
+ const worker = workers.get(input.peer);
439
+ if (!worker?.adapter || worker.closing) throw new CliError('NATIVE_WORKER_NOT_FOUND', 'Target native worker is not open');
440
+ assertRequestedOwner(worker, input);
441
+ if (!worker.adapter.capabilities.interrupt) throw new CliError('NATIVE_CAPABILITY_UNSUPPORTED', 'Provider does not support interrupt');
442
+ result = await track(worker.adapter.interrupt({ turnId: input.turnId || undefined }));
443
+ } else if (request.method === 'POST' && url.pathname === '/respond') {
444
+ const input = await jsonBody(request);
445
+ requireRunning();
446
+ const worker = workers.get(input.peer);
447
+ if (!worker?.adapter || worker.closing || creating.has(input.peer)) throw new CliError('NATIVE_WORKER_NOT_FOUND', 'Target native worker is not open');
448
+ assertRequestedOwner(worker, input);
449
+ if (input.executorId !== worker.owner || !worker.adapter.capabilities.approvals || !worker.adapter.respond) {
450
+ throw new CliError('NATIVE_APPROVAL_MISMATCH', 'Interaction belongs to another executor or provider has no responder');
451
+ }
452
+ result = await track(Promise.resolve(worker.adapter.respond(input)));
453
+ save(worker);
454
+ } else if (request.method === 'POST' && url.pathname === '/close') {
455
+ const input = await jsonBody(request);
456
+ if (input.owner !== undefined || input.generation !== undefined || input.provider !== undefined || input.sessionId !== undefined) {
457
+ const worker = workers.get(input.peer);
458
+ if (!worker) throw new CliError('NATIVE_WORKER_NOT_FOUND', 'Target native worker is not open');
459
+ assertRequestedOwner(worker, input);
460
+ }
461
+ result = await closeWorker(input.peer);
462
+ } else if (request.method === 'POST' && url.pathname === '/down') {
463
+ reply(response, 200, { ok: true, data: { stopping: true } });
464
+ void shutdown().catch((error) => console.error(JSON.stringify(publicError(error))));
465
+ return;
466
+ } else throw new CliError('NOT_FOUND', 'Unknown native control route');
467
+ reply(response, 200, { ok: true, data: redactSecrets(result) });
468
+ } catch (error) { reply(response, error.code === 'BAD_ARGS' ? 400 : 409, { ok: false, error: publicError(error) }); }
469
+ }
470
+ const server = http.createServer((request, response) => { void route(request, response); });
471
+ server.requestTimeout = 40000;
472
+ server.headersTimeout = 10000;
473
+ try {
474
+ await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); });
475
+ writeNativePointer(ctx, { root: fs.realpathSync(ctx.root), meshDb: path.resolve(ctx.dbPath), pid: process.pid,
476
+ port: server.address().port, token, generation });
477
+ } catch (error) { server.close(); ownership.close(); mesh.close(); store.close(); throw error; }
478
+ pollTimer = setInterval(() => void poll(), options.pollMs || 300);
479
+ function shutdown() {
480
+ if (shutdownPromise) return shutdownPromise;
481
+ stopping = true;
482
+ clearInterval(pollTimer);
483
+ shutdownPromise = (async () => {
484
+ const closing = await Promise.allSettled([...workers.keys()].map(closeWorker));
485
+ // Settle creates and admissions before closing persistence.
486
+ while (operations.size) await Promise.allSettled([...operations]);
487
+ const remaining = await Promise.allSettled([...workers.keys()].map(closeWorker));
488
+ const failures = [...closing, ...remaining].filter((result) => result.status === 'rejected');
489
+ if (failures.length) {
490
+ shutdownError = publicError(failures[0].reason);
491
+ // Keep the control endpoint and ownership lock while a child may still
492
+ // be alive. A new daemon must not silently take over these sessions.
493
+ throw new CliError('NATIVE_SHUTDOWN_INCOMPLETE', 'Owned provider shutdown was not confirmed; runtime ownership is retained', shutdownError);
494
+ }
495
+ server.closeAllConnections();
496
+ await new Promise((resolve) => server.close(resolve));
497
+ await new Promise((resolve) => ownership.close(resolve));
498
+ if (readNativePointer(ctx)?.generation === generation) fs.rmSync(paths.pointer, { force: true });
499
+ storeClosed = true;
500
+ mesh.close();
501
+ store.close();
502
+ options.onShutdown?.();
503
+ })();
504
+ const pendingShutdown = shutdownPromise;
505
+ void pendingShutdown.catch(() => {
506
+ if (shutdownPromise === pendingShutdown) shutdownPromise = null;
507
+ });
508
+ return shutdownPromise;
509
+ }
510
+ return { shutdown, port: server.address().port, generation, poll };
511
+ }
@@ -0,0 +1,121 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { DatabaseSync } from 'node:sqlite';
4
+ import { randomBytes } from 'node:crypto';
5
+ import { CliError } from '../../shared/errors.mjs';
6
+
7
+ function statOrMissing(file) {
8
+ try { return fs.lstatSync(file); }
9
+ catch (error) { if (error.code === 'ENOENT') return null; throw error; }
10
+ }
11
+
12
+ function directory(file, create) {
13
+ let stat = statOrMissing(file);
14
+ if (!stat && create) { fs.mkdirSync(file, { mode: 0o700 }); stat = fs.lstatSync(file); }
15
+ if (!stat) return false;
16
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new CliError('NATIVE_STATE_UNSAFE', `Native state must be a real directory: ${file}`);
17
+ return true;
18
+ }
19
+
20
+ export function nativePaths(ctx, { create = false } = {}) {
21
+ const root = fs.realpathSync(ctx.root);
22
+ const parent = path.join(root, '.hello-cc');
23
+ if (!directory(parent, create)) return null;
24
+ const dir = path.join(parent, 'native');
25
+ if (!directory(dir, create)) return null;
26
+ for (const name of ['runtime.json', 'state.db', 'state.db-wal', 'state.db-shm', 'runtime.log']) {
27
+ const file = path.join(dir, name);
28
+ const stat = statOrMissing(file);
29
+ if (stat && (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== 1)) {
30
+ throw new CliError('NATIVE_STATE_UNSAFE', `Native state file is not a regular file: ${file}`);
31
+ }
32
+ }
33
+ return { dir, pointer: path.join(dir, 'runtime.json'), db: path.join(dir, 'state.db'), log: path.join(dir, 'runtime.log') };
34
+ }
35
+
36
+ export function writeNativePointer(ctx, value) {
37
+ const paths = nativePaths(ctx, { create: true });
38
+ const temporary = path.join(paths.dir, `.runtime-${randomBytes(12).toString('hex')}.tmp`);
39
+ fs.writeFileSync(temporary, JSON.stringify(value), { flag: 'wx', mode: 0o600 });
40
+ try { fs.renameSync(temporary, paths.pointer); }
41
+ finally { fs.rmSync(temporary, { force: true }); }
42
+ }
43
+
44
+ export function readNativePointer(ctx) {
45
+ const paths = nativePaths(ctx);
46
+ if (!paths || !fs.existsSync(paths.pointer)) return null;
47
+ if (fs.statSync(paths.pointer).size > 16384) throw new CliError('NATIVE_STATE_UNSAFE', 'Native runtime pointer is too large');
48
+ let value;
49
+ try { value = JSON.parse(fs.readFileSync(paths.pointer, 'utf8')); }
50
+ catch { throw new CliError('NATIVE_STATE_INVALID', 'Native runtime pointer is invalid'); }
51
+ if (!value || typeof value !== 'object' || Array.isArray(value) ||
52
+ value.root !== fs.realpathSync(ctx.root) || value.meshDb !== path.resolve(ctx.dbPath) ||
53
+ !Number.isInteger(value.port) || value.port < 1 || value.port > 65535 ||
54
+ typeof value.token !== 'string' || value.token.length < 32 || value.token.length > 256 ||
55
+ typeof value.generation !== 'string' || !value.generation || value.generation.length > 128) {
56
+ throw new CliError('NATIVE_STATE_INVALID', 'Native runtime pointer does not match this project');
57
+ }
58
+ return value;
59
+ }
60
+
61
+ export function createNativeStore(ctx) {
62
+ const paths = nativePaths(ctx, { create: true });
63
+ const db = new DatabaseSync(paths.db, { timeout: 5000 });
64
+ fs.chmodSync(paths.db, 0o600);
65
+ db.exec(`PRAGMA journal_mode = WAL;
66
+ PRAGMA busy_timeout = 5000;
67
+ PRAGMA foreign_keys = ON;
68
+ CREATE TABLE IF NOT EXISTS workers (
69
+ peer TEXT PRIMARY KEY, provider TEXT NOT NULL, session_id TEXT,
70
+ cwd TEXT NOT NULL, status TEXT NOT NULL, updated_at INTEGER NOT NULL
71
+ );
72
+ CREATE TABLE IF NOT EXISTS deliveries (
73
+ id INTEGER PRIMARY KEY, peer TEXT NOT NULL, message_id INTEGER NOT NULL,
74
+ submission_id TEXT NOT NULL UNIQUE, state TEXT NOT NULL, turn_id TEXT,
75
+ detail TEXT, updated_at INTEGER NOT NULL, UNIQUE(peer, message_id)
76
+ );
77
+ CREATE TABLE IF NOT EXISTS provider_events (
78
+ id INTEGER PRIMARY KEY, peer TEXT NOT NULL, payload TEXT NOT NULL, created_at INTEGER NOT NULL
79
+ );`);
80
+ const now = () => Date.now();
81
+ return {
82
+ db,
83
+ worker(peer) { return db.prepare('SELECT * FROM workers WHERE peer = ?').get(peer) || null; },
84
+ workers() { return db.prepare('SELECT * FROM workers ORDER BY peer').all(); },
85
+ saveWorker(worker) {
86
+ db.prepare(`INSERT INTO workers(peer, provider, session_id, cwd, status, updated_at) VALUES (?,?,?,?,?,?)
87
+ ON CONFLICT(peer) DO UPDATE SET provider=excluded.provider, session_id=COALESCE(excluded.session_id,workers.session_id),
88
+ cwd=excluded.cwd, status=excluded.status, updated_at=excluded.updated_at`)
89
+ .run(worker.peer, worker.provider, worker.sessionId || null, worker.cwd, worker.status, now());
90
+ },
91
+ queue(peer, messageId, submissionId) {
92
+ db.prepare(`INSERT OR IGNORE INTO deliveries(peer,message_id,submission_id,state,updated_at) VALUES (?,?,?,'queued',?)`)
93
+ .run(peer, messageId, submissionId, now());
94
+ return this.delivery(peer, messageId);
95
+ },
96
+ delivery(peer, messageId) { return db.prepare('SELECT * FROM deliveries WHERE peer=? AND message_id=?').get(peer, messageId) || null; },
97
+ pending(peer) { return db.prepare("SELECT * FROM deliveries WHERE peer=? AND state='queued' ORDER BY id LIMIT 1").get(peer) || null; },
98
+ updateDelivery(id, state, turnId = null, detail = null) {
99
+ db.prepare('UPDATE deliveries SET state=?,turn_id=COALESCE(?,turn_id),detail=?,updated_at=? WHERE id=?')
100
+ .run(state, turnId, detail ? JSON.stringify(detail) : null, now(), id);
101
+ },
102
+ deliveries(peer = null) {
103
+ return peer ? db.prepare('SELECT * FROM deliveries WHERE peer=? ORDER BY id DESC LIMIT 100').all(peer)
104
+ : db.prepare('SELECT * FROM deliveries ORDER BY id DESC LIMIT 100').all();
105
+ },
106
+ event(peer, payload) {
107
+ db.prepare('INSERT INTO provider_events(peer,payload,created_at) VALUES (?,?,?)').run(peer, JSON.stringify(payload), now());
108
+ // Event inspection is bounded; authoritative messages remain in mesh.db.
109
+ db.prepare('DELETE FROM provider_events WHERE id < (SELECT MAX(id)-2000 FROM provider_events)').run();
110
+ },
111
+ events(peer, after = 0) {
112
+ return db.prepare('SELECT * FROM provider_events WHERE peer=? AND id>? ORDER BY id LIMIT 100').all(peer, after)
113
+ .map((row) => ({ ...row, payload: JSON.parse(row.payload) }));
114
+ },
115
+ disconnected() {
116
+ db.prepare("UPDATE workers SET status='disconnected',updated_at=? WHERE status!='closed'").run(now());
117
+ db.prepare("UPDATE deliveries SET state='uncertain',updated_at=? WHERE state IN ('dispatching','submitted','accepted')").run(now());
118
+ },
119
+ close() { db.close(); }
120
+ };
121
+ }