borgmcp 4.3.0 → 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 +19 -3
- 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/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/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 +23 -3
- package/src/opencode-drone.ts +363 -72
- package/src/opencode-seat-identity.ts +1 -0
- package/src/roster-render.ts +2 -3
- package/src/server-errors.ts +50 -0
- package/src/stream-status.ts +7 -2
- package/src/update-cmd.ts +118 -41
package/src/opencode-drone.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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';
|
|
@@ -14,11 +14,51 @@ import {
|
|
|
14
14
|
OPENCODE_SERVER_USERNAME,
|
|
15
15
|
type OpenCodeLaunchTrust,
|
|
16
16
|
} from './opencode-launch-trust.js';
|
|
17
|
+
import {
|
|
18
|
+
OpenCodeAuthenticationError,
|
|
19
|
+
OpenCodeHttpError,
|
|
20
|
+
OpenCodeResponseError,
|
|
21
|
+
OpenCodeUnreachableError,
|
|
22
|
+
type OpenCodeFailureCode,
|
|
23
|
+
} from './server-errors.js';
|
|
24
|
+
|
|
25
|
+
const OPEN_CODE_DIAGNOSTIC_LOG_MAX_BYTES = 64 * 1024;
|
|
26
|
+
const diagnosticLogPathsForTests = new Set<string>();
|
|
27
|
+
|
|
28
|
+
function stateIdentityDigest(current: OpenCodeDroneState): string {
|
|
29
|
+
const key = [current.serverUrl, current.directory, current.cubeName, current.droneLabel].join('\0');
|
|
30
|
+
return createHash('sha256').update(key).digest('hex').slice(0, 24);
|
|
31
|
+
}
|
|
17
32
|
|
|
18
|
-
|
|
19
|
-
|
|
33
|
+
function diagnosticLogPath(owner: OpenCodeDroneState): string {
|
|
34
|
+
const path = join(tmpdir(), `borg-opencode-drone-${stateIdentityDigest(owner)}.log`);
|
|
35
|
+
diagnosticLogPathsForTests.add(path);
|
|
36
|
+
return path;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function log(msg: string, owner: OpenCodeDroneState | null = state) {
|
|
20
40
|
const line = `[${new Date().toISOString()}] ${msg}\n`;
|
|
21
|
-
|
|
41
|
+
if (!owner) {
|
|
42
|
+
process.stderr.write(line);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const path = diagnosticLogPath(owner);
|
|
47
|
+
if (existsSync(path)) chmodSync(path, 0o600);
|
|
48
|
+
appendFileSync(path, line, { encoding: 'utf8', mode: 0o600 });
|
|
49
|
+
chmodSync(path, 0o600);
|
|
50
|
+
if (statSync(path).size <= OPEN_CODE_DIAGNOSTIC_LOG_MAX_BYTES) return;
|
|
51
|
+
const contents = readFileSync(path);
|
|
52
|
+
const tail = contents.subarray(contents.length - OPEN_CODE_DIAGNOSTIC_LOG_MAX_BYTES);
|
|
53
|
+
const firstNewline = tail.indexOf(0x0a);
|
|
54
|
+
const bounded = firstNewline >= 0 ? tail.subarray(firstNewline + 1) : tail;
|
|
55
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
56
|
+
writeFileSync(temporary, bounded, { mode: 0o600 });
|
|
57
|
+
renameSync(temporary, path);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const code = (error as NodeJS.ErrnoException | null)?.code ?? 'unknown';
|
|
60
|
+
process.stderr.write(`OpenCode diagnostic log write failed (${code})\n`);
|
|
61
|
+
}
|
|
22
62
|
}
|
|
23
63
|
|
|
24
64
|
interface OpenCodeDroneState {
|
|
@@ -41,8 +81,25 @@ interface OpenCodeDroneState {
|
|
|
41
81
|
pendingSubmissions: Map<string, PendingOpenCodeSubmission>;
|
|
42
82
|
reconcilingEntryIds: Set<string>;
|
|
43
83
|
processingDeliveries: boolean;
|
|
84
|
+
nextObservationSequence: number;
|
|
85
|
+
lastObservation: OpenCodeLastObservation;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface OpenCodeLastObservation {
|
|
89
|
+
injectionSequence: number;
|
|
90
|
+
acceptedSequence: number;
|
|
91
|
+
failureSequence: number;
|
|
92
|
+
lastInjectionAt: number | null;
|
|
93
|
+
lastInjectionResult: OpenCodeInjectionResult | null;
|
|
94
|
+
lastAcceptedEntryId: string | null;
|
|
95
|
+
lastFailureCode: string | null;
|
|
44
96
|
}
|
|
45
97
|
|
|
98
|
+
type OpenCodeLastFields = Pick<
|
|
99
|
+
OpenCodeLastObservation,
|
|
100
|
+
'lastInjectionAt' | 'lastInjectionResult' | 'lastAcceptedEntryId' | 'lastFailureCode'
|
|
101
|
+
>;
|
|
102
|
+
|
|
46
103
|
let state: OpenCodeDroneState | null = null;
|
|
47
104
|
|
|
48
105
|
interface ConnectDeps {
|
|
@@ -63,12 +120,12 @@ interface OCSession {
|
|
|
63
120
|
}
|
|
64
121
|
|
|
65
122
|
interface OCMessage {
|
|
66
|
-
info
|
|
123
|
+
info: {
|
|
67
124
|
id?: string;
|
|
68
|
-
role
|
|
125
|
+
role: string;
|
|
69
126
|
time?: { created?: number };
|
|
70
127
|
};
|
|
71
|
-
parts
|
|
128
|
+
parts: Array<{
|
|
72
129
|
type?: string;
|
|
73
130
|
text?: string;
|
|
74
131
|
metadata?: Record<string, unknown>;
|
|
@@ -82,6 +139,7 @@ export type OpenCodeDeliveryState =
|
|
|
82
139
|
| 'failed';
|
|
83
140
|
|
|
84
141
|
interface OpenCodeDelivery {
|
|
142
|
+
sequence: number;
|
|
85
143
|
entryId: string;
|
|
86
144
|
sourceEntryId: string;
|
|
87
145
|
text: string;
|
|
@@ -107,6 +165,7 @@ interface PendingOpenCodeSubmission {
|
|
|
107
165
|
}
|
|
108
166
|
|
|
109
167
|
type OpenCodeDeliveryOutcome = 'delivered' | 'delivered-unconfirmed' | 'failed';
|
|
168
|
+
type OpenCodeInjectionResult = OpenCodeDeliveryOutcome;
|
|
110
169
|
|
|
111
170
|
const OPEN_CODE_DELIVERY_RETRY_DELAYS_MS = [0, 250, 1_000, 3_000] as const;
|
|
112
171
|
const OPEN_CODE_RECONCILIATION_DELAY_MS = 3_000;
|
|
@@ -168,7 +227,7 @@ function abandonOpenCodeDeliveries(current: OpenCodeDroneState | null): void {
|
|
|
168
227
|
|
|
169
228
|
export async function connectOpenCodeDrone(deps: ConnectDeps): Promise<void> {
|
|
170
229
|
if (!isOpenCode256BitIdentity(deps.apiPassword)) {
|
|
171
|
-
throw new
|
|
230
|
+
throw new OpenCodeAuthenticationError('OpenCode API password is missing or unverifiable');
|
|
172
231
|
}
|
|
173
232
|
abandonOpenCodeDeliveries(state);
|
|
174
233
|
state = {
|
|
@@ -191,8 +250,18 @@ export async function connectOpenCodeDrone(deps: ConnectDeps): Promise<void> {
|
|
|
191
250
|
pendingSubmissions: new Map(),
|
|
192
251
|
reconcilingEntryIds: new Set(),
|
|
193
252
|
processingDeliveries: false,
|
|
253
|
+
nextObservationSequence: 0,
|
|
254
|
+
lastObservation: {
|
|
255
|
+
injectionSequence: 0,
|
|
256
|
+
acceptedSequence: 0,
|
|
257
|
+
failureSequence: 0,
|
|
258
|
+
lastInjectionAt: null,
|
|
259
|
+
lastInjectionResult: null,
|
|
260
|
+
lastAcceptedEntryId: null,
|
|
261
|
+
lastFailureCode: null,
|
|
262
|
+
},
|
|
194
263
|
};
|
|
195
|
-
log(`connected url=${deps.serverUrl} dir=${deps.directory}
|
|
264
|
+
log(`connected url=${deps.serverUrl} dir=${deps.directory}`, state);
|
|
196
265
|
}
|
|
197
266
|
|
|
198
267
|
// ---------------------------------------------------------------------------
|
|
@@ -207,7 +276,7 @@ function apiUrl(path: string): string {
|
|
|
207
276
|
function authenticatedHeaders(headers: Record<string, string> = {}): Record<string, string> {
|
|
208
277
|
const password = state?.apiPassword;
|
|
209
278
|
if (!isOpenCode256BitIdentity(password)) {
|
|
210
|
-
throw new
|
|
279
|
+
throw new OpenCodeAuthenticationError('OpenCode API password is missing or unverifiable');
|
|
211
280
|
}
|
|
212
281
|
return {
|
|
213
282
|
...headers,
|
|
@@ -225,6 +294,13 @@ async function rawGet(path: string): Promise<{ status: number; body: string }> {
|
|
|
225
294
|
const res = await fetch(url, { headers: authenticatedHeaders(), signal: controller.signal });
|
|
226
295
|
const body = await res.text();
|
|
227
296
|
return { status: res.status, body };
|
|
297
|
+
} catch (error) {
|
|
298
|
+
if (error instanceof OpenCodeAuthenticationError) throw error;
|
|
299
|
+
throw new OpenCodeUnreachableError(
|
|
300
|
+
controller.signal.aborted ? 'timeout' : 'transient',
|
|
301
|
+
controller.signal.aborted ? 'OpenCode request timed out' : 'OpenCode request failed',
|
|
302
|
+
{ cause: error },
|
|
303
|
+
);
|
|
228
304
|
} finally {
|
|
229
305
|
clearTimeout(timer);
|
|
230
306
|
}
|
|
@@ -243,28 +319,111 @@ async function rawPost(path: string, bodyObj: unknown): Promise<{ status: number
|
|
|
243
319
|
});
|
|
244
320
|
const body = await res.text();
|
|
245
321
|
return { status: res.status, body };
|
|
322
|
+
} catch (error) {
|
|
323
|
+
if (error instanceof OpenCodeAuthenticationError) throw error;
|
|
324
|
+
throw new OpenCodeUnreachableError(
|
|
325
|
+
controller.signal.aborted ? 'timeout' : 'transient',
|
|
326
|
+
controller.signal.aborted ? 'OpenCode request timed out' : 'OpenCode request failed',
|
|
327
|
+
{ cause: error },
|
|
328
|
+
);
|
|
246
329
|
} finally {
|
|
247
330
|
clearTimeout(timer);
|
|
248
331
|
}
|
|
249
332
|
}
|
|
250
333
|
|
|
334
|
+
function openCodeHttpError(status: number, operation: string): OpenCodeHttpError {
|
|
335
|
+
const code: OpenCodeFailureCode = status === 401
|
|
336
|
+
? 'unauthorized'
|
|
337
|
+
: status === 404
|
|
338
|
+
? 'not-found'
|
|
339
|
+
: status >= 500 || status === 429
|
|
340
|
+
? 'transient'
|
|
341
|
+
: 'incompatible-api';
|
|
342
|
+
return new OpenCodeHttpError(status, code, `OpenCode ${operation} request failed (${status})`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function parseOpenCodeJson(body: string): unknown {
|
|
346
|
+
try {
|
|
347
|
+
return JSON.parse(body);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
throw new OpenCodeResponseError('OpenCode returned malformed JSON', { cause: error });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
354
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function decodeSession(value: unknown): OCSession {
|
|
358
|
+
if (
|
|
359
|
+
!isRecord(value)
|
|
360
|
+
|| typeof value.id !== 'string'
|
|
361
|
+
|| value.id.length === 0
|
|
362
|
+
|| typeof value.directory !== 'string'
|
|
363
|
+
|| !isRecord(value.time)
|
|
364
|
+
|| typeof value.time.created !== 'number'
|
|
365
|
+
|| !Number.isFinite(value.time.created)
|
|
366
|
+
|| (value.parentID !== undefined && typeof value.parentID !== 'string')
|
|
367
|
+
|| (value.agent !== undefined && typeof value.agent !== 'string')
|
|
368
|
+
|| (value.model !== undefined && (
|
|
369
|
+
!isRecord(value.model)
|
|
370
|
+
|| typeof value.model.providerID !== 'string'
|
|
371
|
+
|| typeof value.model.modelID !== 'string'
|
|
372
|
+
))
|
|
373
|
+
) {
|
|
374
|
+
throw new OpenCodeResponseError();
|
|
375
|
+
}
|
|
376
|
+
return value as unknown as OCSession;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function decodeSessions(body: string): OCSession[] {
|
|
380
|
+
const value = parseOpenCodeJson(body);
|
|
381
|
+
if (!Array.isArray(value)) throw new OpenCodeResponseError();
|
|
382
|
+
return value.map(decodeSession);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function decodeMessages(body: string): OCMessage[] {
|
|
386
|
+
const value = parseOpenCodeJson(body);
|
|
387
|
+
if (!Array.isArray(value)) throw new OpenCodeResponseError();
|
|
388
|
+
return value.map((message) => {
|
|
389
|
+
if (!isRecord(message)) throw new OpenCodeResponseError();
|
|
390
|
+
if (
|
|
391
|
+
!isRecord(message.info)
|
|
392
|
+
|| typeof message.info.role !== 'string'
|
|
393
|
+
|| (message.info.id !== undefined && typeof message.info.id !== 'string')
|
|
394
|
+
|| (message.info.time !== undefined && (
|
|
395
|
+
!isRecord(message.info.time)
|
|
396
|
+
|| (message.info.time.created !== undefined && typeof message.info.time.created !== 'number')
|
|
397
|
+
))
|
|
398
|
+
) throw new OpenCodeResponseError();
|
|
399
|
+
if (
|
|
400
|
+
!Array.isArray(message.parts)
|
|
401
|
+
|| message.parts.some((part) => !isRecord(part)
|
|
402
|
+
|| (part.type !== undefined && typeof part.type !== 'string')
|
|
403
|
+
|| (part.text !== undefined && typeof part.text !== 'string')
|
|
404
|
+
|| (part.metadata !== undefined && !isRecord(part.metadata)))
|
|
405
|
+
) throw new OpenCodeResponseError();
|
|
406
|
+
return message as unknown as OCMessage;
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
251
410
|
async function listSessions(): Promise<OCSession[]> {
|
|
252
411
|
const { status, body } = await rawGet('/session');
|
|
253
|
-
if (status !== 200) throw
|
|
254
|
-
return
|
|
412
|
+
if (status !== 200) throw openCodeHttpError(status, 'sessions');
|
|
413
|
+
return decodeSessions(body);
|
|
255
414
|
}
|
|
256
415
|
|
|
257
416
|
async function getSession(id: string): Promise<OCSession | null> {
|
|
258
417
|
const { status, body } = await rawGet(`/session/${id}`);
|
|
259
418
|
if (status === 404) return null;
|
|
260
|
-
if (status !== 200) throw
|
|
261
|
-
return
|
|
419
|
+
if (status !== 200) throw openCodeHttpError(status, 'session');
|
|
420
|
+
return decodeSession(parseOpenCodeJson(body));
|
|
262
421
|
}
|
|
263
422
|
|
|
264
423
|
async function listSessionMessages(id: string): Promise<OCMessage[]> {
|
|
265
424
|
const { status, body } = await rawGet(`/session/${id}/message`);
|
|
266
|
-
if (status !== 200) throw
|
|
267
|
-
return
|
|
425
|
+
if (status !== 200) throw openCodeHttpError(status, 'session messages');
|
|
426
|
+
return decodeMessages(body);
|
|
268
427
|
}
|
|
269
428
|
|
|
270
429
|
async function findInjectedMessage(
|
|
@@ -298,9 +457,7 @@ async function promptSession(id: string, bodyObj: Record<string, unknown>): Prom
|
|
|
298
457
|
|
|
299
458
|
function bindingPath(): string {
|
|
300
459
|
const current = state!;
|
|
301
|
-
const
|
|
302
|
-
const digest = createHash('sha256').update(key).digest('hex').slice(0, 24);
|
|
303
|
-
const path = join(tmpdir(), `borg-opencode-session-${digest}.json`);
|
|
460
|
+
const path = join(tmpdir(), `borg-opencode-session-${stateIdentityDigest(current)}.json`);
|
|
304
461
|
bindingPathsForTests.add(path);
|
|
305
462
|
return path;
|
|
306
463
|
}
|
|
@@ -446,32 +603,28 @@ function restoreBinding(): SessionBinding | null {
|
|
|
446
603
|
}
|
|
447
604
|
|
|
448
605
|
function isBoundSession(session: OCSession, binding: SessionBinding): boolean {
|
|
449
|
-
return session.id === binding.sessionId && session.directory ===
|
|
606
|
+
return session.id === binding.sessionId && session.directory === binding.directory;
|
|
450
607
|
}
|
|
451
608
|
|
|
452
609
|
function isTopLevelSession(session: OCSession): boolean {
|
|
453
610
|
return !session.parentID;
|
|
454
611
|
}
|
|
455
612
|
|
|
456
|
-
async function findUnseenTopLevelSession(knownRootSessionIds: string[]): Promise<{
|
|
613
|
+
async function findUnseenTopLevelSession(knownRootSessionIds: string[], directory: string): Promise<{
|
|
457
614
|
session: OCSession;
|
|
458
615
|
knownRootSessionIds: string[];
|
|
459
616
|
} | null> {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
(session)
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
return { session: best, knownRootSessionIds: roots.map((session) => session.id) };
|
|
472
|
-
} catch {
|
|
473
|
-
return null;
|
|
474
|
-
}
|
|
617
|
+
const sessions = await listSessions();
|
|
618
|
+
const roots = sessions.filter(
|
|
619
|
+
(session) => session.directory === directory
|
|
620
|
+
&& isTopLevelSession(session),
|
|
621
|
+
);
|
|
622
|
+
const matched = roots.filter((session) => !knownRootSessionIds.includes(session.id));
|
|
623
|
+
if (matched.length === 0) return null;
|
|
624
|
+
const best = matched.reduce((a, b) =>
|
|
625
|
+
a.time.created > b.time.created ? a : b,
|
|
626
|
+
);
|
|
627
|
+
return { session: best, knownRootSessionIds: roots.map((session) => session.id) };
|
|
475
628
|
}
|
|
476
629
|
|
|
477
630
|
function launchCorrelationMatchCount(messages: OCMessage[], correlationIdentity: string): number {
|
|
@@ -500,9 +653,13 @@ async function findLaunchSession(correlationIdentity: string): Promise<{
|
|
|
500
653
|
session: OCSession;
|
|
501
654
|
knownRootSessionIds: string[];
|
|
502
655
|
} | null> {
|
|
656
|
+
const owner = state!;
|
|
657
|
+
const observationSequence = ++owner.nextObservationSequence;
|
|
503
658
|
try {
|
|
504
|
-
const
|
|
505
|
-
|
|
659
|
+
const listedSessions = await listSessions();
|
|
660
|
+
if (state !== owner) return null;
|
|
661
|
+
const sessions = listedSessions.filter(
|
|
662
|
+
(session) => session.directory === owner.directory,
|
|
506
663
|
);
|
|
507
664
|
const knownRootSessionIds = sessions
|
|
508
665
|
.filter(isTopLevelSession)
|
|
@@ -514,23 +671,31 @@ async function findLaunchSession(correlationIdentity: string): Promise<{
|
|
|
514
671
|
correlationIdentity,
|
|
515
672
|
),
|
|
516
673
|
})));
|
|
674
|
+
if (state !== owner) return null;
|
|
517
675
|
const totalMatches = candidates.reduce((total, candidate) => total + candidate.matchCount, 0);
|
|
518
676
|
if (totalMatches !== 1) return null;
|
|
519
677
|
const matched = candidates.find((candidate) => candidate.matchCount === 1);
|
|
520
678
|
return matched ? { session: matched.session, knownRootSessionIds } : null;
|
|
521
|
-
} catch {
|
|
679
|
+
} catch (error) {
|
|
680
|
+
if (state === owner) recordOpenCodeFailure(owner, error, observationSequence);
|
|
522
681
|
return null;
|
|
523
682
|
}
|
|
524
683
|
}
|
|
525
684
|
|
|
526
|
-
async function resolveInjectionSession(
|
|
685
|
+
async function resolveInjectionSession(
|
|
686
|
+
owner: OpenCodeDroneState,
|
|
687
|
+
observationSequence: number,
|
|
688
|
+
): Promise<OCSession | null> {
|
|
689
|
+
if (state !== owner) return null;
|
|
527
690
|
const binding = restoreBinding();
|
|
528
691
|
if (!binding) return null;
|
|
529
692
|
|
|
530
693
|
const bound = await getSession(binding.sessionId);
|
|
694
|
+
if (state !== owner) return null;
|
|
531
695
|
if (!bound || !isBoundSession(bound, binding)) {
|
|
532
696
|
clearBinding();
|
|
533
|
-
const replacement = await findUnseenTopLevelSession(binding.knownRootSessionIds);
|
|
697
|
+
const replacement = await findUnseenTopLevelSession(binding.knownRootSessionIds, owner.directory);
|
|
698
|
+
if (state !== owner) return null;
|
|
534
699
|
if (!replacement) return null;
|
|
535
700
|
saveBinding(replacement.session, replacement.knownRootSessionIds);
|
|
536
701
|
return replacement.session;
|
|
@@ -539,7 +704,13 @@ async function resolveInjectionSession(): Promise<OCSession | null> {
|
|
|
539
704
|
// `/new` creates an unseen top-level session. Keep the launch-time root
|
|
540
705
|
// snapshot so an old, unrelated root is never mistaken for a user switch.
|
|
541
706
|
// Children never supersede the bound root.
|
|
542
|
-
|
|
707
|
+
let switched: Awaited<ReturnType<typeof findUnseenTopLevelSession>> = null;
|
|
708
|
+
try {
|
|
709
|
+
switched = await findUnseenTopLevelSession(binding.knownRootSessionIds, owner.directory);
|
|
710
|
+
} catch (error) {
|
|
711
|
+
recordOpenCodeFailure(owner, error, observationSequence);
|
|
712
|
+
}
|
|
713
|
+
if (state !== owner) return null;
|
|
543
714
|
if (switched) {
|
|
544
715
|
saveBinding(switched.session, switched.knownRootSessionIds);
|
|
545
716
|
return switched.session;
|
|
@@ -562,6 +733,64 @@ function rememberBounded(
|
|
|
562
733
|
}
|
|
563
734
|
}
|
|
564
735
|
|
|
736
|
+
function openCodeFailureCode(error: unknown): string {
|
|
737
|
+
const code = (error as { code?: unknown } | null)?.code;
|
|
738
|
+
if (typeof code === 'string' && code.length > 0) return code;
|
|
739
|
+
return error instanceof Error && error.name ? error.name : 'unknown';
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function updateLastOpenCodeObservation(
|
|
743
|
+
owner: OpenCodeDroneState,
|
|
744
|
+
sequence: number,
|
|
745
|
+
update: Partial<OpenCodeLastFields>,
|
|
746
|
+
): void {
|
|
747
|
+
// Attempts, acceptances, and failures resolve independently; an observation
|
|
748
|
+
// may be stale for one field without being stale for the others.
|
|
749
|
+
const current = owner.lastObservation;
|
|
750
|
+
const updatesInjection = 'lastInjectionAt' in update || 'lastInjectionResult' in update;
|
|
751
|
+
const updatesAccepted = 'lastAcceptedEntryId' in update;
|
|
752
|
+
const updatesFailure = 'lastFailureCode' in update;
|
|
753
|
+
owner.lastObservation = {
|
|
754
|
+
...current,
|
|
755
|
+
...(updatesInjection && sequence >= current.injectionSequence
|
|
756
|
+
? {
|
|
757
|
+
injectionSequence: sequence,
|
|
758
|
+
...('lastInjectionAt' in update
|
|
759
|
+
? { lastInjectionAt: update.lastInjectionAt as number | null }
|
|
760
|
+
: {}),
|
|
761
|
+
...('lastInjectionResult' in update
|
|
762
|
+
? { lastInjectionResult: update.lastInjectionResult as OpenCodeInjectionResult | null }
|
|
763
|
+
: {}),
|
|
764
|
+
}
|
|
765
|
+
: {}),
|
|
766
|
+
...(updatesAccepted && sequence >= current.acceptedSequence
|
|
767
|
+
? { acceptedSequence: sequence, lastAcceptedEntryId: update.lastAcceptedEntryId as string | null }
|
|
768
|
+
: {}),
|
|
769
|
+
...(updatesFailure && sequence >= current.failureSequence
|
|
770
|
+
? { failureSequence: sequence, lastFailureCode: update.lastFailureCode as string | null }
|
|
771
|
+
: {}),
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function recordOpenCodeFailure(
|
|
776
|
+
owner: OpenCodeDroneState,
|
|
777
|
+
error: unknown,
|
|
778
|
+
observationSequence: number,
|
|
779
|
+
): void {
|
|
780
|
+
updateLastOpenCodeObservation(owner, observationSequence, {
|
|
781
|
+
lastFailureCode: openCodeFailureCode(error),
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function recordOpenCodeAcceptance(
|
|
786
|
+
owner: OpenCodeDroneState,
|
|
787
|
+
delivery: Pick<OpenCodeDelivery, 'sequence' | 'entryId'>,
|
|
788
|
+
): void {
|
|
789
|
+
updateLastOpenCodeObservation(owner, delivery.sequence, {
|
|
790
|
+
lastAcceptedEntryId: delivery.entryId,
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
|
|
565
794
|
function clearPendingSubmission(owner: OpenCodeDroneState, entryId: string): void {
|
|
566
795
|
if (!owner.pendingSubmissions.delete(entryId)) return;
|
|
567
796
|
if (state === owner) persistCurrentBinding();
|
|
@@ -569,7 +798,7 @@ function clearPendingSubmission(owner: OpenCodeDroneState, entryId: string): voi
|
|
|
569
798
|
|
|
570
799
|
function confirmOpenCodeDelivery(
|
|
571
800
|
owner: OpenCodeDroneState,
|
|
572
|
-
delivery: Pick<OpenCodeDelivery, 'entryId' | 'sourceEntryId' | 'text'>,
|
|
801
|
+
delivery: Pick<OpenCodeDelivery, 'sequence' | 'entryId' | 'sourceEntryId' | 'text'>,
|
|
573
802
|
): void {
|
|
574
803
|
const unconfirmed = owner.unconfirmedEntries.get(delivery.entryId);
|
|
575
804
|
if (unconfirmed && unconfirmed.text !== delivery.text) return;
|
|
@@ -583,6 +812,11 @@ function confirmOpenCodeDelivery(
|
|
|
583
812
|
delivery.sourceEntryId,
|
|
584
813
|
);
|
|
585
814
|
owner.totalEntriesInjected++;
|
|
815
|
+
updateLastOpenCodeObservation(owner, delivery.sequence, {
|
|
816
|
+
lastAcceptedEntryId: delivery.entryId,
|
|
817
|
+
lastInjectionResult: 'delivered',
|
|
818
|
+
lastFailureCode: null,
|
|
819
|
+
});
|
|
586
820
|
}
|
|
587
821
|
|
|
588
822
|
function scheduleOpenCodeReconciliation(
|
|
@@ -607,7 +841,8 @@ function scheduleOpenCodeReconciliation(
|
|
|
607
841
|
return;
|
|
608
842
|
}
|
|
609
843
|
} catch (err) {
|
|
610
|
-
|
|
844
|
+
recordOpenCodeFailure(owner, err, delivery.sequence);
|
|
845
|
+
log(`entry ${delivery.entryId} reconciliation unavailable: ${err}`, owner);
|
|
611
846
|
}
|
|
612
847
|
}
|
|
613
848
|
} finally {
|
|
@@ -656,13 +891,15 @@ async function deliverOpenCodeEntry(
|
|
|
656
891
|
|
|
657
892
|
if (!target) {
|
|
658
893
|
try {
|
|
659
|
-
target = await resolveInjectionSession();
|
|
894
|
+
target = await resolveInjectionSession(owner, delivery.sequence);
|
|
660
895
|
} catch (err) {
|
|
661
|
-
|
|
896
|
+
recordOpenCodeFailure(owner, err, delivery.sequence);
|
|
897
|
+
log(`entry ${delivery.entryId} target unavailable: ${err}`, owner);
|
|
662
898
|
continue;
|
|
663
899
|
}
|
|
664
900
|
if (!target) {
|
|
665
|
-
|
|
901
|
+
recordOpenCodeFailure(owner, openCodeHttpError(404, 'session'), delivery.sequence);
|
|
902
|
+
log(`entry ${delivery.entryId} target unavailable: no bound session`, owner);
|
|
666
903
|
return 'failed';
|
|
667
904
|
}
|
|
668
905
|
delivery.sessionId = target.id;
|
|
@@ -679,12 +916,13 @@ async function deliverOpenCodeEntry(
|
|
|
679
916
|
: await findInjectedMessage(confirmationSessionId, delivery.sourceEntryId)
|
|
680
917
|
);
|
|
681
918
|
if (deliveredIdentity) {
|
|
682
|
-
log(`entry ${delivery.entryId} already present in session ${confirmationSessionId}
|
|
919
|
+
log(`entry ${delivery.entryId} already present in session ${confirmationSessionId}`, owner);
|
|
683
920
|
clearPendingSubmission(owner, delivery.entryId);
|
|
684
921
|
return 'delivered';
|
|
685
922
|
}
|
|
686
923
|
} catch (err) {
|
|
687
|
-
|
|
924
|
+
recordOpenCodeFailure(owner, err, delivery.sequence);
|
|
925
|
+
log(`entry ${delivery.entryId} confirmation unavailable: ${err}`, owner);
|
|
688
926
|
continue;
|
|
689
927
|
}
|
|
690
928
|
|
|
@@ -705,7 +943,7 @@ async function deliverOpenCodeEntry(
|
|
|
705
943
|
});
|
|
706
944
|
if (!persistCurrentBinding()) {
|
|
707
945
|
owner.pendingSubmissions.delete(delivery.entryId);
|
|
708
|
-
log(`entry ${delivery.entryId} submission skipped: pending intent was not durable
|
|
946
|
+
log(`entry ${delivery.entryId} submission skipped: pending intent was not durable`, owner);
|
|
709
947
|
return 'failed';
|
|
710
948
|
}
|
|
711
949
|
|
|
@@ -724,16 +962,19 @@ async function deliverOpenCodeEntry(
|
|
|
724
962
|
}],
|
|
725
963
|
});
|
|
726
964
|
} catch (err) {
|
|
727
|
-
|
|
965
|
+
recordOpenCodeFailure(owner, err, delivery.sequence);
|
|
966
|
+
log(`entry ${delivery.entryId} submission outcome unavailable: ${err}`, owner);
|
|
728
967
|
}
|
|
729
968
|
|
|
730
969
|
delivery.state = 'delivered-unconfirmed';
|
|
731
970
|
if (status !== null && status !== 200 && status !== 204) {
|
|
971
|
+
recordOpenCodeFailure(owner, openCodeHttpError(status, 'prompt'), delivery.sequence);
|
|
732
972
|
clearPendingSubmission(owner, delivery.entryId);
|
|
733
973
|
if (status === 404) clearBinding();
|
|
734
974
|
return 'failed';
|
|
735
975
|
}
|
|
736
976
|
delivery.acceptedSubmission = true;
|
|
977
|
+
if (status === 200 || status === 204) recordOpenCodeAcceptance(owner, delivery);
|
|
737
978
|
}
|
|
738
979
|
|
|
739
980
|
for (
|
|
@@ -755,7 +996,8 @@ async function deliverOpenCodeEntry(
|
|
|
755
996
|
return 'delivered';
|
|
756
997
|
}
|
|
757
998
|
} catch (err) {
|
|
758
|
-
|
|
999
|
+
recordOpenCodeFailure(owner, err, delivery.sequence);
|
|
1000
|
+
log(`entry ${delivery.entryId} post-acceptance confirmation unavailable: ${err}`, owner);
|
|
759
1001
|
}
|
|
760
1002
|
}
|
|
761
1003
|
|
|
@@ -774,12 +1016,26 @@ async function processOpenCodeDeliveries(owner: OpenCodeDroneState): Promise<voi
|
|
|
774
1016
|
try {
|
|
775
1017
|
while (state === owner && owner.deliveryQueue.length > 0) {
|
|
776
1018
|
const delivery = owner.deliveryQueue.shift()!;
|
|
1019
|
+
updateLastOpenCodeObservation(owner, delivery.sequence, {
|
|
1020
|
+
lastInjectionAt: Date.now(),
|
|
1021
|
+
lastInjectionResult: null,
|
|
1022
|
+
lastFailureCode: null,
|
|
1023
|
+
});
|
|
777
1024
|
let outcome: OpenCodeDeliveryOutcome = 'failed';
|
|
778
1025
|
try {
|
|
779
1026
|
outcome = await deliverOpenCodeEntry(owner, delivery);
|
|
780
1027
|
} catch (err) {
|
|
781
|
-
|
|
1028
|
+
recordOpenCodeFailure(owner, err, delivery.sequence);
|
|
1029
|
+
log(`entry ${delivery.entryId} delivery error: ${err}`, owner);
|
|
782
1030
|
}
|
|
1031
|
+
updateLastOpenCodeObservation(owner, delivery.sequence, {
|
|
1032
|
+
lastInjectionResult: outcome,
|
|
1033
|
+
lastFailureCode: outcome === 'delivered'
|
|
1034
|
+
? null
|
|
1035
|
+
: outcome === 'failed'
|
|
1036
|
+
? (owner.lastObservation.lastFailureCode ?? 'unknown')
|
|
1037
|
+
: owner.lastObservation.lastFailureCode,
|
|
1038
|
+
});
|
|
783
1039
|
|
|
784
1040
|
owner.activeDeliveries.delete(delivery.entryId);
|
|
785
1041
|
if (delivery.settled) {
|
|
@@ -823,9 +1079,10 @@ async function processOpenCodeDeliveries(owner: OpenCodeDroneState): Promise<voi
|
|
|
823
1079
|
* the separate MCP-child process, which must never fall back to a newest-session heuristic.
|
|
824
1080
|
*/
|
|
825
1081
|
export async function injectInitialKickoff(launch: OpenCodeLaunchKickoff): Promise<boolean> {
|
|
826
|
-
|
|
1082
|
+
const owner = state;
|
|
1083
|
+
if (!owner?.connected) { log('kickoff: not connected', owner); return false; }
|
|
827
1084
|
if (!isOpenCode256BitIdentity(launch.correlationIdentity)) {
|
|
828
|
-
log('kickoff: correlation identity missing or unverifiable');
|
|
1085
|
+
log('kickoff: correlation identity missing or unverifiable', owner);
|
|
829
1086
|
return false;
|
|
830
1087
|
}
|
|
831
1088
|
|
|
@@ -834,7 +1091,7 @@ export async function injectInitialKickoff(launch: OpenCodeLaunchKickoff): Promi
|
|
|
834
1091
|
for (let i = 0; i < 30; i++) {
|
|
835
1092
|
try {
|
|
836
1093
|
await listSessions();
|
|
837
|
-
log(`kickoff: server ready (attempt ${i + 1})
|
|
1094
|
+
log(`kickoff: server ready (attempt ${i + 1})`, owner);
|
|
838
1095
|
break;
|
|
839
1096
|
} catch {
|
|
840
1097
|
// not ready yet
|
|
@@ -847,17 +1104,18 @@ export async function injectInitialKickoff(launch: OpenCodeLaunchKickoff): Promi
|
|
|
847
1104
|
for (let i = 0; i < 30; i++) {
|
|
848
1105
|
const binding = await findLaunchSession(launch.correlationIdentity);
|
|
849
1106
|
if (binding) {
|
|
1107
|
+
if (state !== owner) return false;
|
|
850
1108
|
saveBinding(binding.session, binding.knownRootSessionIds);
|
|
851
|
-
log(`kickoff: bound session ${binding.session.id.slice(0, 8)}
|
|
1109
|
+
log(`kickoff: bound session ${binding.session.id.slice(0, 8)}…`, owner);
|
|
852
1110
|
return true;
|
|
853
1111
|
}
|
|
854
1112
|
await new Promise((r) => setTimeout(r, 1000));
|
|
855
1113
|
}
|
|
856
1114
|
|
|
857
|
-
log('kickoff: no session found');
|
|
1115
|
+
log('kickoff: no session found', owner);
|
|
858
1116
|
return false;
|
|
859
1117
|
} catch (err) {
|
|
860
|
-
log(`kickoff error: ${err}
|
|
1118
|
+
log(`kickoff error: ${err}`, owner);
|
|
861
1119
|
return false;
|
|
862
1120
|
}
|
|
863
1121
|
}
|
|
@@ -879,7 +1137,7 @@ export function injectOpenCodeEntry(
|
|
|
879
1137
|
): Promise<boolean> {
|
|
880
1138
|
const owner = state;
|
|
881
1139
|
if (!owner?.connected) {
|
|
882
|
-
log(`entry ${entryId} rejected: OpenCode is not connected
|
|
1140
|
+
log(`entry ${entryId} rejected: OpenCode is not connected`, owner);
|
|
883
1141
|
return Promise.resolve(false);
|
|
884
1142
|
}
|
|
885
1143
|
|
|
@@ -893,27 +1151,27 @@ export function injectOpenCodeEntry(
|
|
|
893
1151
|
pendingEntryId !== entryId && pending.sourceEntryId === sourceEntryId,
|
|
894
1152
|
);
|
|
895
1153
|
if (pendingSource) {
|
|
896
|
-
log(`entry ${entryId} reconciles pending source ${sourceEntryId}
|
|
1154
|
+
log(`entry ${entryId} reconciles pending source ${sourceEntryId}`, owner);
|
|
897
1155
|
return injectOpenCodeEntry(text, pendingSource[0], false, sourceEntryId, isSourcePending);
|
|
898
1156
|
}
|
|
899
1157
|
for (const [deliveredEntryId, record] of owner.deliveredEntries) {
|
|
900
1158
|
if (deliveredEntryId !== entryId && record.sourceEntryId === sourceEntryId) {
|
|
901
1159
|
if (record.text !== text) return Promise.resolve(false);
|
|
902
|
-
log(`entry ${entryId} source ${sourceEntryId} already delivered
|
|
1160
|
+
log(`entry ${entryId} source ${sourceEntryId} already delivered`, owner);
|
|
903
1161
|
return Promise.resolve(true);
|
|
904
1162
|
}
|
|
905
1163
|
}
|
|
906
1164
|
for (const [unconfirmedEntryId, record] of owner.unconfirmedEntries) {
|
|
907
1165
|
if (unconfirmedEntryId !== entryId && record.sourceEntryId === sourceEntryId) {
|
|
908
1166
|
if (record.text !== text) return Promise.resolve(false);
|
|
909
|
-
log(`entry ${entryId} source ${sourceEntryId} remains unconfirmed
|
|
1167
|
+
log(`entry ${entryId} source ${sourceEntryId} remains unconfirmed`, owner);
|
|
910
1168
|
return Promise.resolve(true);
|
|
911
1169
|
}
|
|
912
1170
|
}
|
|
913
1171
|
for (const active of owner.activeDeliveries.values()) {
|
|
914
1172
|
if (active.entryId !== entryId && active.sourceEntryId === sourceEntryId) {
|
|
915
1173
|
if (active.text !== text) return Promise.resolve(false);
|
|
916
|
-
log(`entry ${entryId} joined active source ${sourceEntryId}
|
|
1174
|
+
log(`entry ${entryId} joined active source ${sourceEntryId}`, owner);
|
|
917
1175
|
return active.promise;
|
|
918
1176
|
}
|
|
919
1177
|
}
|
|
@@ -921,26 +1179,27 @@ export function injectOpenCodeEntry(
|
|
|
921
1179
|
const delivered = owner.deliveredEntries.get(entryId);
|
|
922
1180
|
if (delivered !== undefined) {
|
|
923
1181
|
if (delivered.text !== text || delivered.sourceEntryId !== sourceEntryId) {
|
|
924
|
-
log(`entry ${entryId} replay text mismatch
|
|
1182
|
+
log(`entry ${entryId} replay text mismatch`, owner);
|
|
925
1183
|
rememberBounded(owner.failedEntries, entryId, text, sourceEntryId);
|
|
926
1184
|
return Promise.resolve(false);
|
|
927
1185
|
}
|
|
928
|
-
log(`entry ${entryId} replay already delivered
|
|
1186
|
+
log(`entry ${entryId} replay already delivered`, owner);
|
|
929
1187
|
return Promise.resolve(true);
|
|
930
1188
|
}
|
|
931
1189
|
|
|
932
1190
|
const unconfirmed = owner.unconfirmedEntries.get(entryId);
|
|
933
1191
|
if (unconfirmed !== undefined) {
|
|
934
1192
|
if (unconfirmed.text !== text || unconfirmed.sourceEntryId !== sourceEntryId) {
|
|
935
|
-
log(`entry ${entryId} unconfirmed replay text mismatch
|
|
1193
|
+
log(`entry ${entryId} unconfirmed replay text mismatch`, owner);
|
|
936
1194
|
rememberBounded(owner.failedEntries, entryId, text, sourceEntryId);
|
|
937
1195
|
return Promise.resolve(false);
|
|
938
1196
|
}
|
|
939
|
-
log(`entry ${entryId} replay remains unconfirmed
|
|
1197
|
+
log(`entry ${entryId} replay remains unconfirmed`, owner);
|
|
940
1198
|
const pending = owner.pendingSubmissions.get(entryId);
|
|
941
1199
|
const accepted = pending !== undefined;
|
|
942
1200
|
if (pending) {
|
|
943
1201
|
scheduleOpenCodeReconciliation(owner, {
|
|
1202
|
+
sequence: ++owner.nextObservationSequence,
|
|
944
1203
|
entryId,
|
|
945
1204
|
sourceEntryId,
|
|
946
1205
|
text,
|
|
@@ -959,11 +1218,11 @@ export function injectOpenCodeEntry(
|
|
|
959
1218
|
const active = owner.activeDeliveries.get(entryId);
|
|
960
1219
|
if (active) {
|
|
961
1220
|
if (active.text !== text || active.sourceEntryId !== sourceEntryId) {
|
|
962
|
-
log(`entry ${entryId} active text mismatch
|
|
1221
|
+
log(`entry ${entryId} active text mismatch`, owner);
|
|
963
1222
|
rememberBounded(owner.failedEntries, entryId, text, sourceEntryId);
|
|
964
1223
|
return Promise.resolve(false);
|
|
965
1224
|
}
|
|
966
|
-
log(`entry ${entryId} replay joined active delivery
|
|
1225
|
+
log(`entry ${entryId} replay joined active delivery`, owner);
|
|
967
1226
|
return active.promise;
|
|
968
1227
|
}
|
|
969
1228
|
|
|
@@ -972,6 +1231,7 @@ export function injectOpenCodeEntry(
|
|
|
972
1231
|
resolveDelivery = resolve;
|
|
973
1232
|
});
|
|
974
1233
|
const delivery: OpenCodeDelivery = {
|
|
1234
|
+
sequence: ++owner.nextObservationSequence,
|
|
975
1235
|
entryId,
|
|
976
1236
|
sourceEntryId,
|
|
977
1237
|
text,
|
|
@@ -1017,16 +1277,21 @@ export function settleOpenCodeEntry(sourceEntryId: string): void {
|
|
|
1017
1277
|
}
|
|
1018
1278
|
|
|
1019
1279
|
export async function probeOpenCodeDroneArmed(): Promise<boolean | null> {
|
|
1020
|
-
|
|
1280
|
+
const owner = state;
|
|
1281
|
+
if (!owner?.connected) return null;
|
|
1282
|
+
const observationSequence = ++owner.nextObservationSequence;
|
|
1021
1283
|
const binding = restoreBinding();
|
|
1022
1284
|
if (!binding) return false;
|
|
1023
1285
|
|
|
1024
1286
|
try {
|
|
1025
1287
|
const session = await getSession(binding.sessionId);
|
|
1288
|
+
if (state !== owner) return null;
|
|
1026
1289
|
if (session && isBoundSession(session, binding)) return true;
|
|
1290
|
+
recordOpenCodeFailure(owner, openCodeHttpError(404, 'session'), observationSequence);
|
|
1027
1291
|
clearBinding();
|
|
1028
1292
|
return false;
|
|
1029
|
-
} catch {
|
|
1293
|
+
} catch (error) {
|
|
1294
|
+
if (state === owner) recordOpenCodeFailure(owner, error, observationSequence);
|
|
1030
1295
|
return false;
|
|
1031
1296
|
}
|
|
1032
1297
|
}
|
|
@@ -1041,6 +1306,10 @@ export interface OpenCodeConnectionState {
|
|
|
1041
1306
|
sessionId: string | null;
|
|
1042
1307
|
totalEntriesInjected: number;
|
|
1043
1308
|
totalEntriesRetried: number;
|
|
1309
|
+
lastInjectionAt: number | null;
|
|
1310
|
+
lastInjectionResult: OpenCodeInjectionResult | null;
|
|
1311
|
+
lastAcceptedEntryId: string | null;
|
|
1312
|
+
lastFailureCode: string | null;
|
|
1044
1313
|
deliveryStates: Record<OpenCodeDeliveryState, number>;
|
|
1045
1314
|
}
|
|
1046
1315
|
|
|
@@ -1059,10 +1328,24 @@ export function getOpenCodeConnectionState(): OpenCodeConnectionState {
|
|
|
1059
1328
|
sessionId: state?.sessionId ?? null,
|
|
1060
1329
|
totalEntriesInjected: state?.totalEntriesInjected ?? 0,
|
|
1061
1330
|
totalEntriesRetried: state?.totalEntriesRetried ?? 0,
|
|
1331
|
+
lastInjectionAt: state?.lastObservation.lastInjectionAt ?? null,
|
|
1332
|
+
lastInjectionResult: state?.lastObservation.lastInjectionResult ?? null,
|
|
1333
|
+
lastAcceptedEntryId: state?.lastObservation.lastAcceptedEntryId ?? null,
|
|
1334
|
+
lastFailureCode: state?.lastObservation.lastFailureCode ?? null,
|
|
1062
1335
|
deliveryStates,
|
|
1063
1336
|
};
|
|
1064
1337
|
}
|
|
1065
1338
|
|
|
1339
|
+
export function __getOpenCodeDiagnosticLogPathForTests(): string {
|
|
1340
|
+
if (!state) throw new Error('OpenCode drone is not connected');
|
|
1341
|
+
return diagnosticLogPath(state);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
export function __getOpenCodeLastObservationForTests(): OpenCodeLastObservation {
|
|
1345
|
+
if (!state) throw new Error('OpenCode drone is not connected');
|
|
1346
|
+
return { ...state.lastObservation };
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1066
1349
|
export function computeOpenCodePort(droneId: string, base: number = 14096): number {
|
|
1067
1350
|
let hash = 0;
|
|
1068
1351
|
for (let i = 0; i < droneId.length; i++) {
|
|
@@ -1141,4 +1424,12 @@ export function __resetOpenCodeDroneForTests(): void {
|
|
|
1141
1424
|
}
|
|
1142
1425
|
}
|
|
1143
1426
|
bindingPathsForTests.clear();
|
|
1427
|
+
for (const path of diagnosticLogPathsForTests) {
|
|
1428
|
+
try {
|
|
1429
|
+
unlinkSync(path);
|
|
1430
|
+
} catch {
|
|
1431
|
+
// Already removed.
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
diagnosticLogPathsForTests.clear();
|
|
1144
1435
|
}
|