borgmcp 4.2.3 → 4.4.0
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/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +5 -2
- package/dist/cli-help.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -9
- package/dist/index.js.map +1 -1
- package/dist/opencode-drone.d.ts +17 -0
- package/dist/opencode-drone.d.ts.map +1 -1
- package/dist/opencode-drone.js +306 -66
- package/dist/opencode-drone.js.map +1 -1
- package/dist/opencode-seat-identity.d.ts +1 -1
- package/dist/opencode-seat-identity.d.ts.map +1 -1
- package/dist/opencode-seat-identity.js.map +1 -1
- package/dist/remote-client.d.ts +23 -19
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +70 -6
- package/dist/remote-client.js.map +1 -1
- package/dist/roster-render.d.ts.map +1 -1
- package/dist/roster-render.js +2 -3
- package/dist/roster-render.js.map +1 -1
- package/dist/server-errors.d.ts +22 -0
- package/dist/server-errors.d.ts.map +1 -1
- package/dist/server-errors.js +32 -0
- package/dist/server-errors.js.map +1 -1
- package/dist/stream-status.d.ts.map +1 -1
- package/dist/stream-status.js +7 -2
- package/dist/stream-status.js.map +1 -1
- package/dist/tool-manifest.d.ts.map +1 -1
- package/dist/tool-manifest.js +17 -4
- package/dist/tool-manifest.js.map +1 -1
- package/dist/update-cmd.d.ts +2 -1
- package/dist/update-cmd.d.ts.map +1 -1
- package/dist/update-cmd.js +110 -41
- package/dist/update-cmd.js.map +1 -1
- package/docs/LOCAL_SERVER.md +14 -5
- package/package.json +1 -1
- package/src/cli-help.ts +5 -2
- package/src/index.ts +49 -9
- package/src/opencode-drone.ts +363 -72
- package/src/opencode-seat-identity.ts +1 -0
- package/src/remote-client.ts +99 -10
- package/src/roster-render.ts +2 -3
- package/src/server-errors.ts +50 -0
- package/src/stream-status.ts +7 -2
- package/src/tool-manifest.ts +17 -4
- package/src/update-cmd.ts +118 -41
package/dist/opencode-drone.js
CHANGED
|
@@ -1,17 +1,48 @@
|
|
|
1
|
-
import { appendFileSync, existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'fs';
|
|
1
|
+
import { appendFileSync, chmodSync, existsSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'fs';
|
|
2
2
|
import { createHash } from 'crypto';
|
|
3
3
|
import { createServer } from 'node:net';
|
|
4
4
|
import { join } from 'path';
|
|
5
5
|
import { tmpdir } from 'os';
|
|
6
6
|
import { OPENCODE_INJECTED_ENTRY_METADATA_KEY, OPENCODE_WAKE_IDENTITY_METADATA_KEY, OPENCODE_LAUNCH_CORRELATION_METADATA_KEY, } from './opencode-plugin.js';
|
|
7
7
|
import { createOpenCodeLaunchTrust, isOpenCode256BitIdentity, OPENCODE_SERVER_USERNAME, } from './opencode-launch-trust.js';
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
import { OpenCodeAuthenticationError, OpenCodeHttpError, OpenCodeResponseError, OpenCodeUnreachableError, } from './server-errors.js';
|
|
9
|
+
const OPEN_CODE_DIAGNOSTIC_LOG_MAX_BYTES = 64 * 1024;
|
|
10
|
+
const diagnosticLogPathsForTests = new Set();
|
|
11
|
+
function stateIdentityDigest(current) {
|
|
12
|
+
const key = [current.serverUrl, current.directory, current.cubeName, current.droneLabel].join('\0');
|
|
13
|
+
return createHash('sha256').update(key).digest('hex').slice(0, 24);
|
|
14
|
+
}
|
|
15
|
+
function diagnosticLogPath(owner) {
|
|
16
|
+
const path = join(tmpdir(), `borg-opencode-drone-${stateIdentityDigest(owner)}.log`);
|
|
17
|
+
diagnosticLogPathsForTests.add(path);
|
|
18
|
+
return path;
|
|
19
|
+
}
|
|
20
|
+
function log(msg, owner = state) {
|
|
10
21
|
const line = `[${new Date().toISOString()}] ${msg}\n`;
|
|
22
|
+
if (!owner) {
|
|
23
|
+
process.stderr.write(line);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
11
26
|
try {
|
|
12
|
-
|
|
27
|
+
const path = diagnosticLogPath(owner);
|
|
28
|
+
if (existsSync(path))
|
|
29
|
+
chmodSync(path, 0o600);
|
|
30
|
+
appendFileSync(path, line, { encoding: 'utf8', mode: 0o600 });
|
|
31
|
+
chmodSync(path, 0o600);
|
|
32
|
+
if (statSync(path).size <= OPEN_CODE_DIAGNOSTIC_LOG_MAX_BYTES)
|
|
33
|
+
return;
|
|
34
|
+
const contents = readFileSync(path);
|
|
35
|
+
const tail = contents.subarray(contents.length - OPEN_CODE_DIAGNOSTIC_LOG_MAX_BYTES);
|
|
36
|
+
const firstNewline = tail.indexOf(0x0a);
|
|
37
|
+
const bounded = firstNewline >= 0 ? tail.subarray(firstNewline + 1) : tail;
|
|
38
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
39
|
+
writeFileSync(temporary, bounded, { mode: 0o600 });
|
|
40
|
+
renameSync(temporary, path);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
const code = error?.code ?? 'unknown';
|
|
44
|
+
process.stderr.write(`OpenCode diagnostic log write failed (${code})\n`);
|
|
13
45
|
}
|
|
14
|
-
catch { }
|
|
15
46
|
}
|
|
16
47
|
let state = null;
|
|
17
48
|
const OPEN_CODE_DELIVERY_RETRY_DELAYS_MS = [0, 250, 1_000, 3_000];
|
|
@@ -46,7 +77,7 @@ function abandonOpenCodeDeliveries(current) {
|
|
|
46
77
|
}
|
|
47
78
|
export async function connectOpenCodeDrone(deps) {
|
|
48
79
|
if (!isOpenCode256BitIdentity(deps.apiPassword)) {
|
|
49
|
-
throw new
|
|
80
|
+
throw new OpenCodeAuthenticationError('OpenCode API password is missing or unverifiable');
|
|
50
81
|
}
|
|
51
82
|
abandonOpenCodeDeliveries(state);
|
|
52
83
|
state = {
|
|
@@ -69,8 +100,18 @@ export async function connectOpenCodeDrone(deps) {
|
|
|
69
100
|
pendingSubmissions: new Map(),
|
|
70
101
|
reconcilingEntryIds: new Set(),
|
|
71
102
|
processingDeliveries: false,
|
|
103
|
+
nextObservationSequence: 0,
|
|
104
|
+
lastObservation: {
|
|
105
|
+
injectionSequence: 0,
|
|
106
|
+
acceptedSequence: 0,
|
|
107
|
+
failureSequence: 0,
|
|
108
|
+
lastInjectionAt: null,
|
|
109
|
+
lastInjectionResult: null,
|
|
110
|
+
lastAcceptedEntryId: null,
|
|
111
|
+
lastFailureCode: null,
|
|
112
|
+
},
|
|
72
113
|
};
|
|
73
|
-
log(`connected url=${deps.serverUrl} dir=${deps.directory}
|
|
114
|
+
log(`connected url=${deps.serverUrl} dir=${deps.directory}`, state);
|
|
74
115
|
}
|
|
75
116
|
// ---------------------------------------------------------------------------
|
|
76
117
|
// Raw fetch wrappers
|
|
@@ -82,7 +123,7 @@ function apiUrl(path) {
|
|
|
82
123
|
function authenticatedHeaders(headers = {}) {
|
|
83
124
|
const password = state?.apiPassword;
|
|
84
125
|
if (!isOpenCode256BitIdentity(password)) {
|
|
85
|
-
throw new
|
|
126
|
+
throw new OpenCodeAuthenticationError('OpenCode API password is missing or unverifiable');
|
|
86
127
|
}
|
|
87
128
|
return {
|
|
88
129
|
...headers,
|
|
@@ -99,6 +140,11 @@ async function rawGet(path) {
|
|
|
99
140
|
const body = await res.text();
|
|
100
141
|
return { status: res.status, body };
|
|
101
142
|
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (error instanceof OpenCodeAuthenticationError)
|
|
145
|
+
throw error;
|
|
146
|
+
throw new OpenCodeUnreachableError(controller.signal.aborted ? 'timeout' : 'transient', controller.signal.aborted ? 'OpenCode request timed out' : 'OpenCode request failed', { cause: error });
|
|
147
|
+
}
|
|
102
148
|
finally {
|
|
103
149
|
clearTimeout(timer);
|
|
104
150
|
}
|
|
@@ -117,29 +163,100 @@ async function rawPost(path, bodyObj) {
|
|
|
117
163
|
const body = await res.text();
|
|
118
164
|
return { status: res.status, body };
|
|
119
165
|
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
if (error instanceof OpenCodeAuthenticationError)
|
|
168
|
+
throw error;
|
|
169
|
+
throw new OpenCodeUnreachableError(controller.signal.aborted ? 'timeout' : 'transient', controller.signal.aborted ? 'OpenCode request timed out' : 'OpenCode request failed', { cause: error });
|
|
170
|
+
}
|
|
120
171
|
finally {
|
|
121
172
|
clearTimeout(timer);
|
|
122
173
|
}
|
|
123
174
|
}
|
|
175
|
+
function openCodeHttpError(status, operation) {
|
|
176
|
+
const code = status === 401
|
|
177
|
+
? 'unauthorized'
|
|
178
|
+
: status === 404
|
|
179
|
+
? 'not-found'
|
|
180
|
+
: status >= 500 || status === 429
|
|
181
|
+
? 'transient'
|
|
182
|
+
: 'incompatible-api';
|
|
183
|
+
return new OpenCodeHttpError(status, code, `OpenCode ${operation} request failed (${status})`);
|
|
184
|
+
}
|
|
185
|
+
function parseOpenCodeJson(body) {
|
|
186
|
+
try {
|
|
187
|
+
return JSON.parse(body);
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
throw new OpenCodeResponseError('OpenCode returned malformed JSON', { cause: error });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function isRecord(value) {
|
|
194
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
195
|
+
}
|
|
196
|
+
function decodeSession(value) {
|
|
197
|
+
if (!isRecord(value)
|
|
198
|
+
|| typeof value.id !== 'string'
|
|
199
|
+
|| value.id.length === 0
|
|
200
|
+
|| typeof value.directory !== 'string'
|
|
201
|
+
|| !isRecord(value.time)
|
|
202
|
+
|| typeof value.time.created !== 'number'
|
|
203
|
+
|| !Number.isFinite(value.time.created)
|
|
204
|
+
|| (value.parentID !== undefined && typeof value.parentID !== 'string')
|
|
205
|
+
|| (value.agent !== undefined && typeof value.agent !== 'string')
|
|
206
|
+
|| (value.model !== undefined && (!isRecord(value.model)
|
|
207
|
+
|| typeof value.model.providerID !== 'string'
|
|
208
|
+
|| typeof value.model.modelID !== 'string'))) {
|
|
209
|
+
throw new OpenCodeResponseError();
|
|
210
|
+
}
|
|
211
|
+
return value;
|
|
212
|
+
}
|
|
213
|
+
function decodeSessions(body) {
|
|
214
|
+
const value = parseOpenCodeJson(body);
|
|
215
|
+
if (!Array.isArray(value))
|
|
216
|
+
throw new OpenCodeResponseError();
|
|
217
|
+
return value.map(decodeSession);
|
|
218
|
+
}
|
|
219
|
+
function decodeMessages(body) {
|
|
220
|
+
const value = parseOpenCodeJson(body);
|
|
221
|
+
if (!Array.isArray(value))
|
|
222
|
+
throw new OpenCodeResponseError();
|
|
223
|
+
return value.map((message) => {
|
|
224
|
+
if (!isRecord(message))
|
|
225
|
+
throw new OpenCodeResponseError();
|
|
226
|
+
if (!isRecord(message.info)
|
|
227
|
+
|| typeof message.info.role !== 'string'
|
|
228
|
+
|| (message.info.id !== undefined && typeof message.info.id !== 'string')
|
|
229
|
+
|| (message.info.time !== undefined && (!isRecord(message.info.time)
|
|
230
|
+
|| (message.info.time.created !== undefined && typeof message.info.time.created !== 'number'))))
|
|
231
|
+
throw new OpenCodeResponseError();
|
|
232
|
+
if (!Array.isArray(message.parts)
|
|
233
|
+
|| message.parts.some((part) => !isRecord(part)
|
|
234
|
+
|| (part.type !== undefined && typeof part.type !== 'string')
|
|
235
|
+
|| (part.text !== undefined && typeof part.text !== 'string')
|
|
236
|
+
|| (part.metadata !== undefined && !isRecord(part.metadata))))
|
|
237
|
+
throw new OpenCodeResponseError();
|
|
238
|
+
return message;
|
|
239
|
+
});
|
|
240
|
+
}
|
|
124
241
|
async function listSessions() {
|
|
125
242
|
const { status, body } = await rawGet('/session');
|
|
126
243
|
if (status !== 200)
|
|
127
|
-
throw
|
|
128
|
-
return
|
|
244
|
+
throw openCodeHttpError(status, 'sessions');
|
|
245
|
+
return decodeSessions(body);
|
|
129
246
|
}
|
|
130
247
|
async function getSession(id) {
|
|
131
248
|
const { status, body } = await rawGet(`/session/${id}`);
|
|
132
249
|
if (status === 404)
|
|
133
250
|
return null;
|
|
134
251
|
if (status !== 200)
|
|
135
|
-
throw
|
|
136
|
-
return
|
|
252
|
+
throw openCodeHttpError(status, 'session');
|
|
253
|
+
return decodeSession(parseOpenCodeJson(body));
|
|
137
254
|
}
|
|
138
255
|
async function listSessionMessages(id) {
|
|
139
256
|
const { status, body } = await rawGet(`/session/${id}/message`);
|
|
140
257
|
if (status !== 200)
|
|
141
|
-
throw
|
|
142
|
-
return
|
|
258
|
+
throw openCodeHttpError(status, 'session messages');
|
|
259
|
+
return decodeMessages(body);
|
|
143
260
|
}
|
|
144
261
|
async function findInjectedMessage(sessionId, expectedWakeIdentity) {
|
|
145
262
|
const messages = await listSessionMessages(sessionId);
|
|
@@ -165,9 +282,7 @@ async function promptSession(id, bodyObj) {
|
|
|
165
282
|
// ---------------------------------------------------------------------------
|
|
166
283
|
function bindingPath() {
|
|
167
284
|
const current = state;
|
|
168
|
-
const
|
|
169
|
-
const digest = createHash('sha256').update(key).digest('hex').slice(0, 24);
|
|
170
|
-
const path = join(tmpdir(), `borg-opencode-session-${digest}.json`);
|
|
285
|
+
const path = join(tmpdir(), `borg-opencode-session-${stateIdentityDigest(current)}.json`);
|
|
171
286
|
bindingPathsForTests.add(path);
|
|
172
287
|
return path;
|
|
173
288
|
}
|
|
@@ -310,25 +425,20 @@ function restoreBinding() {
|
|
|
310
425
|
return binding;
|
|
311
426
|
}
|
|
312
427
|
function isBoundSession(session, binding) {
|
|
313
|
-
return session.id === binding.sessionId && session.directory ===
|
|
428
|
+
return session.id === binding.sessionId && session.directory === binding.directory;
|
|
314
429
|
}
|
|
315
430
|
function isTopLevelSession(session) {
|
|
316
431
|
return !session.parentID;
|
|
317
432
|
}
|
|
318
|
-
async function findUnseenTopLevelSession(knownRootSessionIds) {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (matched.length === 0)
|
|
325
|
-
return null;
|
|
326
|
-
const best = matched.reduce((a, b) => a.time.created > b.time.created ? a : b);
|
|
327
|
-
return { session: best, knownRootSessionIds: roots.map((session) => session.id) };
|
|
328
|
-
}
|
|
329
|
-
catch {
|
|
433
|
+
async function findUnseenTopLevelSession(knownRootSessionIds, directory) {
|
|
434
|
+
const sessions = await listSessions();
|
|
435
|
+
const roots = sessions.filter((session) => session.directory === directory
|
|
436
|
+
&& isTopLevelSession(session));
|
|
437
|
+
const matched = roots.filter((session) => !knownRootSessionIds.includes(session.id));
|
|
438
|
+
if (matched.length === 0)
|
|
330
439
|
return null;
|
|
331
|
-
|
|
440
|
+
const best = matched.reduce((a, b) => a.time.created > b.time.created ? a : b);
|
|
441
|
+
return { session: best, knownRootSessionIds: roots.map((session) => session.id) };
|
|
332
442
|
}
|
|
333
443
|
function launchCorrelationMatchCount(messages, correlationIdentity) {
|
|
334
444
|
let count = 0;
|
|
@@ -351,8 +461,13 @@ function launchCorrelationMatchCount(messages, correlationIdentity) {
|
|
|
351
461
|
* therefore allowed only when it received this launch's correlation metadata.
|
|
352
462
|
*/
|
|
353
463
|
async function findLaunchSession(correlationIdentity) {
|
|
464
|
+
const owner = state;
|
|
465
|
+
const observationSequence = ++owner.nextObservationSequence;
|
|
354
466
|
try {
|
|
355
|
-
const
|
|
467
|
+
const listedSessions = await listSessions();
|
|
468
|
+
if (state !== owner)
|
|
469
|
+
return null;
|
|
470
|
+
const sessions = listedSessions.filter((session) => session.directory === owner.directory);
|
|
356
471
|
const knownRootSessionIds = sessions
|
|
357
472
|
.filter(isTopLevelSession)
|
|
358
473
|
.map((session) => session.id);
|
|
@@ -360,24 +475,34 @@ async function findLaunchSession(correlationIdentity) {
|
|
|
360
475
|
session,
|
|
361
476
|
matchCount: launchCorrelationMatchCount(await listSessionMessages(session.id), correlationIdentity),
|
|
362
477
|
})));
|
|
478
|
+
if (state !== owner)
|
|
479
|
+
return null;
|
|
363
480
|
const totalMatches = candidates.reduce((total, candidate) => total + candidate.matchCount, 0);
|
|
364
481
|
if (totalMatches !== 1)
|
|
365
482
|
return null;
|
|
366
483
|
const matched = candidates.find((candidate) => candidate.matchCount === 1);
|
|
367
484
|
return matched ? { session: matched.session, knownRootSessionIds } : null;
|
|
368
485
|
}
|
|
369
|
-
catch {
|
|
486
|
+
catch (error) {
|
|
487
|
+
if (state === owner)
|
|
488
|
+
recordOpenCodeFailure(owner, error, observationSequence);
|
|
370
489
|
return null;
|
|
371
490
|
}
|
|
372
491
|
}
|
|
373
|
-
async function resolveInjectionSession() {
|
|
492
|
+
async function resolveInjectionSession(owner, observationSequence) {
|
|
493
|
+
if (state !== owner)
|
|
494
|
+
return null;
|
|
374
495
|
const binding = restoreBinding();
|
|
375
496
|
if (!binding)
|
|
376
497
|
return null;
|
|
377
498
|
const bound = await getSession(binding.sessionId);
|
|
499
|
+
if (state !== owner)
|
|
500
|
+
return null;
|
|
378
501
|
if (!bound || !isBoundSession(bound, binding)) {
|
|
379
502
|
clearBinding();
|
|
380
|
-
const replacement = await findUnseenTopLevelSession(binding.knownRootSessionIds);
|
|
503
|
+
const replacement = await findUnseenTopLevelSession(binding.knownRootSessionIds, owner.directory);
|
|
504
|
+
if (state !== owner)
|
|
505
|
+
return null;
|
|
381
506
|
if (!replacement)
|
|
382
507
|
return null;
|
|
383
508
|
saveBinding(replacement.session, replacement.knownRootSessionIds);
|
|
@@ -386,7 +511,15 @@ async function resolveInjectionSession() {
|
|
|
386
511
|
// `/new` creates an unseen top-level session. Keep the launch-time root
|
|
387
512
|
// snapshot so an old, unrelated root is never mistaken for a user switch.
|
|
388
513
|
// Children never supersede the bound root.
|
|
389
|
-
|
|
514
|
+
let switched = null;
|
|
515
|
+
try {
|
|
516
|
+
switched = await findUnseenTopLevelSession(binding.knownRootSessionIds, owner.directory);
|
|
517
|
+
}
|
|
518
|
+
catch (error) {
|
|
519
|
+
recordOpenCodeFailure(owner, error, observationSequence);
|
|
520
|
+
}
|
|
521
|
+
if (state !== owner)
|
|
522
|
+
return null;
|
|
390
523
|
if (switched) {
|
|
391
524
|
saveBinding(switched.session, switched.knownRootSessionIds);
|
|
392
525
|
return switched.session;
|
|
@@ -403,6 +536,50 @@ function rememberBounded(entries, entryId, text, sourceEntryId) {
|
|
|
403
536
|
entries.delete(oldest);
|
|
404
537
|
}
|
|
405
538
|
}
|
|
539
|
+
function openCodeFailureCode(error) {
|
|
540
|
+
const code = error?.code;
|
|
541
|
+
if (typeof code === 'string' && code.length > 0)
|
|
542
|
+
return code;
|
|
543
|
+
return error instanceof Error && error.name ? error.name : 'unknown';
|
|
544
|
+
}
|
|
545
|
+
function updateLastOpenCodeObservation(owner, sequence, update) {
|
|
546
|
+
// Attempts, acceptances, and failures resolve independently; an observation
|
|
547
|
+
// may be stale for one field without being stale for the others.
|
|
548
|
+
const current = owner.lastObservation;
|
|
549
|
+
const updatesInjection = 'lastInjectionAt' in update || 'lastInjectionResult' in update;
|
|
550
|
+
const updatesAccepted = 'lastAcceptedEntryId' in update;
|
|
551
|
+
const updatesFailure = 'lastFailureCode' in update;
|
|
552
|
+
owner.lastObservation = {
|
|
553
|
+
...current,
|
|
554
|
+
...(updatesInjection && sequence >= current.injectionSequence
|
|
555
|
+
? {
|
|
556
|
+
injectionSequence: sequence,
|
|
557
|
+
...('lastInjectionAt' in update
|
|
558
|
+
? { lastInjectionAt: update.lastInjectionAt }
|
|
559
|
+
: {}),
|
|
560
|
+
...('lastInjectionResult' in update
|
|
561
|
+
? { lastInjectionResult: update.lastInjectionResult }
|
|
562
|
+
: {}),
|
|
563
|
+
}
|
|
564
|
+
: {}),
|
|
565
|
+
...(updatesAccepted && sequence >= current.acceptedSequence
|
|
566
|
+
? { acceptedSequence: sequence, lastAcceptedEntryId: update.lastAcceptedEntryId }
|
|
567
|
+
: {}),
|
|
568
|
+
...(updatesFailure && sequence >= current.failureSequence
|
|
569
|
+
? { failureSequence: sequence, lastFailureCode: update.lastFailureCode }
|
|
570
|
+
: {}),
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
function recordOpenCodeFailure(owner, error, observationSequence) {
|
|
574
|
+
updateLastOpenCodeObservation(owner, observationSequence, {
|
|
575
|
+
lastFailureCode: openCodeFailureCode(error),
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
function recordOpenCodeAcceptance(owner, delivery) {
|
|
579
|
+
updateLastOpenCodeObservation(owner, delivery.sequence, {
|
|
580
|
+
lastAcceptedEntryId: delivery.entryId,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
406
583
|
function clearPendingSubmission(owner, entryId) {
|
|
407
584
|
if (!owner.pendingSubmissions.delete(entryId))
|
|
408
585
|
return;
|
|
@@ -418,6 +595,11 @@ function confirmOpenCodeDelivery(owner, delivery) {
|
|
|
418
595
|
clearPendingSubmission(owner, delivery.entryId);
|
|
419
596
|
rememberBounded(owner.deliveredEntries, delivery.entryId, delivery.text, delivery.sourceEntryId);
|
|
420
597
|
owner.totalEntriesInjected++;
|
|
598
|
+
updateLastOpenCodeObservation(owner, delivery.sequence, {
|
|
599
|
+
lastAcceptedEntryId: delivery.entryId,
|
|
600
|
+
lastInjectionResult: 'delivered',
|
|
601
|
+
lastFailureCode: null,
|
|
602
|
+
});
|
|
421
603
|
}
|
|
422
604
|
function scheduleOpenCodeReconciliation(owner, delivery) {
|
|
423
605
|
if (!delivery.sessionId || owner.reconcilingEntryIds.has(delivery.entryId))
|
|
@@ -443,7 +625,8 @@ function scheduleOpenCodeReconciliation(owner, delivery) {
|
|
|
443
625
|
}
|
|
444
626
|
}
|
|
445
627
|
catch (err) {
|
|
446
|
-
|
|
628
|
+
recordOpenCodeFailure(owner, err, delivery.sequence);
|
|
629
|
+
log(`entry ${delivery.entryId} reconciliation unavailable: ${err}`, owner);
|
|
447
630
|
}
|
|
448
631
|
}
|
|
449
632
|
}
|
|
@@ -492,14 +675,16 @@ async function deliverOpenCodeEntry(owner, delivery) {
|
|
|
492
675
|
}
|
|
493
676
|
if (!target) {
|
|
494
677
|
try {
|
|
495
|
-
target = await resolveInjectionSession();
|
|
678
|
+
target = await resolveInjectionSession(owner, delivery.sequence);
|
|
496
679
|
}
|
|
497
680
|
catch (err) {
|
|
498
|
-
|
|
681
|
+
recordOpenCodeFailure(owner, err, delivery.sequence);
|
|
682
|
+
log(`entry ${delivery.entryId} target unavailable: ${err}`, owner);
|
|
499
683
|
continue;
|
|
500
684
|
}
|
|
501
685
|
if (!target) {
|
|
502
|
-
|
|
686
|
+
recordOpenCodeFailure(owner, openCodeHttpError(404, 'session'), delivery.sequence);
|
|
687
|
+
log(`entry ${delivery.entryId} target unavailable: no bound session`, owner);
|
|
503
688
|
return 'failed';
|
|
504
689
|
}
|
|
505
690
|
delivery.sessionId = target.id;
|
|
@@ -512,13 +697,14 @@ async function deliverOpenCodeEntry(owner, delivery) {
|
|
|
512
697
|
? null
|
|
513
698
|
: await findInjectedMessage(confirmationSessionId, delivery.sourceEntryId));
|
|
514
699
|
if (deliveredIdentity) {
|
|
515
|
-
log(`entry ${delivery.entryId} already present in session ${confirmationSessionId}
|
|
700
|
+
log(`entry ${delivery.entryId} already present in session ${confirmationSessionId}`, owner);
|
|
516
701
|
clearPendingSubmission(owner, delivery.entryId);
|
|
517
702
|
return 'delivered';
|
|
518
703
|
}
|
|
519
704
|
}
|
|
520
705
|
catch (err) {
|
|
521
|
-
|
|
706
|
+
recordOpenCodeFailure(owner, err, delivery.sequence);
|
|
707
|
+
log(`entry ${delivery.entryId} confirmation unavailable: ${err}`, owner);
|
|
522
708
|
continue;
|
|
523
709
|
}
|
|
524
710
|
const submittedBefore = pendingSubmission !== undefined;
|
|
@@ -540,7 +726,7 @@ async function deliverOpenCodeEntry(owner, delivery) {
|
|
|
540
726
|
});
|
|
541
727
|
if (!persistCurrentBinding()) {
|
|
542
728
|
owner.pendingSubmissions.delete(delivery.entryId);
|
|
543
|
-
log(`entry ${delivery.entryId} submission skipped: pending intent was not durable
|
|
729
|
+
log(`entry ${delivery.entryId} submission skipped: pending intent was not durable`, owner);
|
|
544
730
|
return 'failed';
|
|
545
731
|
}
|
|
546
732
|
// prompt_async is not idempotent. Persist the intent before the one POST;
|
|
@@ -559,16 +745,20 @@ async function deliverOpenCodeEntry(owner, delivery) {
|
|
|
559
745
|
});
|
|
560
746
|
}
|
|
561
747
|
catch (err) {
|
|
562
|
-
|
|
748
|
+
recordOpenCodeFailure(owner, err, delivery.sequence);
|
|
749
|
+
log(`entry ${delivery.entryId} submission outcome unavailable: ${err}`, owner);
|
|
563
750
|
}
|
|
564
751
|
delivery.state = 'delivered-unconfirmed';
|
|
565
752
|
if (status !== null && status !== 200 && status !== 204) {
|
|
753
|
+
recordOpenCodeFailure(owner, openCodeHttpError(status, 'prompt'), delivery.sequence);
|
|
566
754
|
clearPendingSubmission(owner, delivery.entryId);
|
|
567
755
|
if (status === 404)
|
|
568
756
|
clearBinding();
|
|
569
757
|
return 'failed';
|
|
570
758
|
}
|
|
571
759
|
delivery.acceptedSubmission = true;
|
|
760
|
+
if (status === 200 || status === 204)
|
|
761
|
+
recordOpenCodeAcceptance(owner, delivery);
|
|
572
762
|
}
|
|
573
763
|
for (let confirmationAttempt = 0; confirmationAttempt < OPEN_CODE_DELIVERY_RETRY_DELAYS_MS.length; confirmationAttempt++) {
|
|
574
764
|
if (confirmationAttempt > 0) {
|
|
@@ -588,7 +778,8 @@ async function deliverOpenCodeEntry(owner, delivery) {
|
|
|
588
778
|
}
|
|
589
779
|
}
|
|
590
780
|
catch (err) {
|
|
591
|
-
|
|
781
|
+
recordOpenCodeFailure(owner, err, delivery.sequence);
|
|
782
|
+
log(`entry ${delivery.entryId} post-acceptance confirmation unavailable: ${err}`, owner);
|
|
592
783
|
}
|
|
593
784
|
}
|
|
594
785
|
return 'delivered-unconfirmed';
|
|
@@ -605,13 +796,27 @@ async function processOpenCodeDeliveries(owner) {
|
|
|
605
796
|
try {
|
|
606
797
|
while (state === owner && owner.deliveryQueue.length > 0) {
|
|
607
798
|
const delivery = owner.deliveryQueue.shift();
|
|
799
|
+
updateLastOpenCodeObservation(owner, delivery.sequence, {
|
|
800
|
+
lastInjectionAt: Date.now(),
|
|
801
|
+
lastInjectionResult: null,
|
|
802
|
+
lastFailureCode: null,
|
|
803
|
+
});
|
|
608
804
|
let outcome = 'failed';
|
|
609
805
|
try {
|
|
610
806
|
outcome = await deliverOpenCodeEntry(owner, delivery);
|
|
611
807
|
}
|
|
612
808
|
catch (err) {
|
|
613
|
-
|
|
809
|
+
recordOpenCodeFailure(owner, err, delivery.sequence);
|
|
810
|
+
log(`entry ${delivery.entryId} delivery error: ${err}`, owner);
|
|
614
811
|
}
|
|
812
|
+
updateLastOpenCodeObservation(owner, delivery.sequence, {
|
|
813
|
+
lastInjectionResult: outcome,
|
|
814
|
+
lastFailureCode: outcome === 'delivered'
|
|
815
|
+
? null
|
|
816
|
+
: outcome === 'failed'
|
|
817
|
+
? (owner.lastObservation.lastFailureCode ?? 'unknown')
|
|
818
|
+
: owner.lastObservation.lastFailureCode,
|
|
819
|
+
});
|
|
615
820
|
owner.activeDeliveries.delete(delivery.entryId);
|
|
616
821
|
if (delivery.settled) {
|
|
617
822
|
delivery.resolve(true);
|
|
@@ -647,12 +852,13 @@ async function processOpenCodeDeliveries(owner) {
|
|
|
647
852
|
* the separate MCP-child process, which must never fall back to a newest-session heuristic.
|
|
648
853
|
*/
|
|
649
854
|
export async function injectInitialKickoff(launch) {
|
|
650
|
-
|
|
651
|
-
|
|
855
|
+
const owner = state;
|
|
856
|
+
if (!owner?.connected) {
|
|
857
|
+
log('kickoff: not connected', owner);
|
|
652
858
|
return false;
|
|
653
859
|
}
|
|
654
860
|
if (!isOpenCode256BitIdentity(launch.correlationIdentity)) {
|
|
655
|
-
log('kickoff: correlation identity missing or unverifiable');
|
|
861
|
+
log('kickoff: correlation identity missing or unverifiable', owner);
|
|
656
862
|
return false;
|
|
657
863
|
}
|
|
658
864
|
try {
|
|
@@ -660,7 +866,7 @@ export async function injectInitialKickoff(launch) {
|
|
|
660
866
|
for (let i = 0; i < 30; i++) {
|
|
661
867
|
try {
|
|
662
868
|
await listSessions();
|
|
663
|
-
log(`kickoff: server ready (attempt ${i + 1})
|
|
869
|
+
log(`kickoff: server ready (attempt ${i + 1})`, owner);
|
|
664
870
|
break;
|
|
665
871
|
}
|
|
666
872
|
catch {
|
|
@@ -673,17 +879,19 @@ export async function injectInitialKickoff(launch) {
|
|
|
673
879
|
for (let i = 0; i < 30; i++) {
|
|
674
880
|
const binding = await findLaunchSession(launch.correlationIdentity);
|
|
675
881
|
if (binding) {
|
|
882
|
+
if (state !== owner)
|
|
883
|
+
return false;
|
|
676
884
|
saveBinding(binding.session, binding.knownRootSessionIds);
|
|
677
|
-
log(`kickoff: bound session ${binding.session.id.slice(0, 8)}
|
|
885
|
+
log(`kickoff: bound session ${binding.session.id.slice(0, 8)}…`, owner);
|
|
678
886
|
return true;
|
|
679
887
|
}
|
|
680
888
|
await new Promise((r) => setTimeout(r, 1000));
|
|
681
889
|
}
|
|
682
|
-
log('kickoff: no session found');
|
|
890
|
+
log('kickoff: no session found', owner);
|
|
683
891
|
return false;
|
|
684
892
|
}
|
|
685
893
|
catch (err) {
|
|
686
|
-
log(`kickoff error: ${err}
|
|
894
|
+
log(`kickoff error: ${err}`, owner);
|
|
687
895
|
return false;
|
|
688
896
|
}
|
|
689
897
|
}
|
|
@@ -698,7 +906,7 @@ export async function injectInitialKickoff(launch) {
|
|
|
698
906
|
export function injectOpenCodeEntry(text, entryId = createHash('sha256').update(text).digest('hex'), allowSubmit = true, sourceEntryId = entryId, isSourcePending) {
|
|
699
907
|
const owner = state;
|
|
700
908
|
if (!owner?.connected) {
|
|
701
|
-
log(`entry ${entryId} rejected: OpenCode is not connected
|
|
909
|
+
log(`entry ${entryId} rejected: OpenCode is not connected`, owner);
|
|
702
910
|
return Promise.resolve(false);
|
|
703
911
|
}
|
|
704
912
|
// Rehydrate durable source markers before source-level deduplication. A
|
|
@@ -708,14 +916,14 @@ export function injectOpenCodeEntry(text, entryId = createHash('sha256').update(
|
|
|
708
916
|
restoreBinding();
|
|
709
917
|
const pendingSource = [...owner.pendingSubmissions].find(([pendingEntryId, pending]) => pendingEntryId !== entryId && pending.sourceEntryId === sourceEntryId);
|
|
710
918
|
if (pendingSource) {
|
|
711
|
-
log(`entry ${entryId} reconciles pending source ${sourceEntryId}
|
|
919
|
+
log(`entry ${entryId} reconciles pending source ${sourceEntryId}`, owner);
|
|
712
920
|
return injectOpenCodeEntry(text, pendingSource[0], false, sourceEntryId, isSourcePending);
|
|
713
921
|
}
|
|
714
922
|
for (const [deliveredEntryId, record] of owner.deliveredEntries) {
|
|
715
923
|
if (deliveredEntryId !== entryId && record.sourceEntryId === sourceEntryId) {
|
|
716
924
|
if (record.text !== text)
|
|
717
925
|
return Promise.resolve(false);
|
|
718
|
-
log(`entry ${entryId} source ${sourceEntryId} already delivered
|
|
926
|
+
log(`entry ${entryId} source ${sourceEntryId} already delivered`, owner);
|
|
719
927
|
return Promise.resolve(true);
|
|
720
928
|
}
|
|
721
929
|
}
|
|
@@ -723,7 +931,7 @@ export function injectOpenCodeEntry(text, entryId = createHash('sha256').update(
|
|
|
723
931
|
if (unconfirmedEntryId !== entryId && record.sourceEntryId === sourceEntryId) {
|
|
724
932
|
if (record.text !== text)
|
|
725
933
|
return Promise.resolve(false);
|
|
726
|
-
log(`entry ${entryId} source ${sourceEntryId} remains unconfirmed
|
|
934
|
+
log(`entry ${entryId} source ${sourceEntryId} remains unconfirmed`, owner);
|
|
727
935
|
return Promise.resolve(true);
|
|
728
936
|
}
|
|
729
937
|
}
|
|
@@ -731,32 +939,33 @@ export function injectOpenCodeEntry(text, entryId = createHash('sha256').update(
|
|
|
731
939
|
if (active.entryId !== entryId && active.sourceEntryId === sourceEntryId) {
|
|
732
940
|
if (active.text !== text)
|
|
733
941
|
return Promise.resolve(false);
|
|
734
|
-
log(`entry ${entryId} joined active source ${sourceEntryId}
|
|
942
|
+
log(`entry ${entryId} joined active source ${sourceEntryId}`, owner);
|
|
735
943
|
return active.promise;
|
|
736
944
|
}
|
|
737
945
|
}
|
|
738
946
|
const delivered = owner.deliveredEntries.get(entryId);
|
|
739
947
|
if (delivered !== undefined) {
|
|
740
948
|
if (delivered.text !== text || delivered.sourceEntryId !== sourceEntryId) {
|
|
741
|
-
log(`entry ${entryId} replay text mismatch
|
|
949
|
+
log(`entry ${entryId} replay text mismatch`, owner);
|
|
742
950
|
rememberBounded(owner.failedEntries, entryId, text, sourceEntryId);
|
|
743
951
|
return Promise.resolve(false);
|
|
744
952
|
}
|
|
745
|
-
log(`entry ${entryId} replay already delivered
|
|
953
|
+
log(`entry ${entryId} replay already delivered`, owner);
|
|
746
954
|
return Promise.resolve(true);
|
|
747
955
|
}
|
|
748
956
|
const unconfirmed = owner.unconfirmedEntries.get(entryId);
|
|
749
957
|
if (unconfirmed !== undefined) {
|
|
750
958
|
if (unconfirmed.text !== text || unconfirmed.sourceEntryId !== sourceEntryId) {
|
|
751
|
-
log(`entry ${entryId} unconfirmed replay text mismatch
|
|
959
|
+
log(`entry ${entryId} unconfirmed replay text mismatch`, owner);
|
|
752
960
|
rememberBounded(owner.failedEntries, entryId, text, sourceEntryId);
|
|
753
961
|
return Promise.resolve(false);
|
|
754
962
|
}
|
|
755
|
-
log(`entry ${entryId} replay remains unconfirmed
|
|
963
|
+
log(`entry ${entryId} replay remains unconfirmed`, owner);
|
|
756
964
|
const pending = owner.pendingSubmissions.get(entryId);
|
|
757
965
|
const accepted = pending !== undefined;
|
|
758
966
|
if (pending) {
|
|
759
967
|
scheduleOpenCodeReconciliation(owner, {
|
|
968
|
+
sequence: ++owner.nextObservationSequence,
|
|
760
969
|
entryId,
|
|
761
970
|
sourceEntryId,
|
|
762
971
|
text,
|
|
@@ -774,11 +983,11 @@ export function injectOpenCodeEntry(text, entryId = createHash('sha256').update(
|
|
|
774
983
|
const active = owner.activeDeliveries.get(entryId);
|
|
775
984
|
if (active) {
|
|
776
985
|
if (active.text !== text || active.sourceEntryId !== sourceEntryId) {
|
|
777
|
-
log(`entry ${entryId} active text mismatch
|
|
986
|
+
log(`entry ${entryId} active text mismatch`, owner);
|
|
778
987
|
rememberBounded(owner.failedEntries, entryId, text, sourceEntryId);
|
|
779
988
|
return Promise.resolve(false);
|
|
780
989
|
}
|
|
781
|
-
log(`entry ${entryId} replay joined active delivery
|
|
990
|
+
log(`entry ${entryId} replay joined active delivery`, owner);
|
|
782
991
|
return active.promise;
|
|
783
992
|
}
|
|
784
993
|
let resolveDelivery;
|
|
@@ -786,6 +995,7 @@ export function injectOpenCodeEntry(text, entryId = createHash('sha256').update(
|
|
|
786
995
|
resolveDelivery = resolve;
|
|
787
996
|
});
|
|
788
997
|
const delivery = {
|
|
998
|
+
sequence: ++owner.nextObservationSequence,
|
|
789
999
|
entryId,
|
|
790
1000
|
sourceEntryId,
|
|
791
1001
|
text,
|
|
@@ -833,19 +1043,26 @@ export function settleOpenCodeEntry(sourceEntryId) {
|
|
|
833
1043
|
persistCurrentBinding();
|
|
834
1044
|
}
|
|
835
1045
|
export async function probeOpenCodeDroneArmed() {
|
|
836
|
-
|
|
1046
|
+
const owner = state;
|
|
1047
|
+
if (!owner?.connected)
|
|
837
1048
|
return null;
|
|
1049
|
+
const observationSequence = ++owner.nextObservationSequence;
|
|
838
1050
|
const binding = restoreBinding();
|
|
839
1051
|
if (!binding)
|
|
840
1052
|
return false;
|
|
841
1053
|
try {
|
|
842
1054
|
const session = await getSession(binding.sessionId);
|
|
1055
|
+
if (state !== owner)
|
|
1056
|
+
return null;
|
|
843
1057
|
if (session && isBoundSession(session, binding))
|
|
844
1058
|
return true;
|
|
1059
|
+
recordOpenCodeFailure(owner, openCodeHttpError(404, 'session'), observationSequence);
|
|
845
1060
|
clearBinding();
|
|
846
1061
|
return false;
|
|
847
1062
|
}
|
|
848
|
-
catch {
|
|
1063
|
+
catch (error) {
|
|
1064
|
+
if (state === owner)
|
|
1065
|
+
recordOpenCodeFailure(owner, error, observationSequence);
|
|
849
1066
|
return false;
|
|
850
1067
|
}
|
|
851
1068
|
}
|
|
@@ -868,9 +1085,23 @@ export function getOpenCodeConnectionState() {
|
|
|
868
1085
|
sessionId: state?.sessionId ?? null,
|
|
869
1086
|
totalEntriesInjected: state?.totalEntriesInjected ?? 0,
|
|
870
1087
|
totalEntriesRetried: state?.totalEntriesRetried ?? 0,
|
|
1088
|
+
lastInjectionAt: state?.lastObservation.lastInjectionAt ?? null,
|
|
1089
|
+
lastInjectionResult: state?.lastObservation.lastInjectionResult ?? null,
|
|
1090
|
+
lastAcceptedEntryId: state?.lastObservation.lastAcceptedEntryId ?? null,
|
|
1091
|
+
lastFailureCode: state?.lastObservation.lastFailureCode ?? null,
|
|
871
1092
|
deliveryStates,
|
|
872
1093
|
};
|
|
873
1094
|
}
|
|
1095
|
+
export function __getOpenCodeDiagnosticLogPathForTests() {
|
|
1096
|
+
if (!state)
|
|
1097
|
+
throw new Error('OpenCode drone is not connected');
|
|
1098
|
+
return diagnosticLogPath(state);
|
|
1099
|
+
}
|
|
1100
|
+
export function __getOpenCodeLastObservationForTests() {
|
|
1101
|
+
if (!state)
|
|
1102
|
+
throw new Error('OpenCode drone is not connected');
|
|
1103
|
+
return { ...state.lastObservation };
|
|
1104
|
+
}
|
|
874
1105
|
export function computeOpenCodePort(droneId, base = 14096) {
|
|
875
1106
|
let hash = 0;
|
|
876
1107
|
for (let i = 0; i < droneId.length; i++) {
|
|
@@ -938,5 +1169,14 @@ export function __resetOpenCodeDroneForTests() {
|
|
|
938
1169
|
}
|
|
939
1170
|
}
|
|
940
1171
|
bindingPathsForTests.clear();
|
|
1172
|
+
for (const path of diagnosticLogPathsForTests) {
|
|
1173
|
+
try {
|
|
1174
|
+
unlinkSync(path);
|
|
1175
|
+
}
|
|
1176
|
+
catch {
|
|
1177
|
+
// Already removed.
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
diagnosticLogPathsForTests.clear();
|
|
941
1181
|
}
|
|
942
1182
|
//# sourceMappingURL=opencode-drone.js.map
|