borgmcp 4.3.0 → 4.5.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/agent-integration-health.d.ts +2 -0
- package/dist/agent-integration-health.d.ts.map +1 -1
- package/dist/agent-integration-health.js +18 -1
- package/dist/agent-integration-health.js.map +1 -1
- package/dist/assimilate-cmd.d.ts +3 -0
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +12 -0
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/assimilate-deps.d.ts.map +1 -1
- package/dist/assimilate-deps.js +4 -2
- package/dist/assimilate-deps.js.map +1 -1
- package/dist/backends/launch-all-terminals.d.ts.map +1 -1
- package/dist/backends/launch-all-terminals.js +6 -3
- package/dist/backends/launch-all-terminals.js.map +1 -1
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +7 -4
- package/dist/cli-help.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -4
- package/dist/index.js.map +1 -1
- package/dist/opencode-drone.d.ts +19 -0
- package/dist/opencode-drone.d.ts.map +1 -1
- package/dist/opencode-drone.js +374 -67
- 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/private-root.d.ts +2 -0
- package/dist/private-root.d.ts.map +1 -1
- package/dist/private-root.js +35 -1
- package/dist/private-root.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/seats.d.ts +5 -0
- package/dist/seats.d.ts.map +1 -1
- package/dist/seats.js +10 -0
- package/dist/seats.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/server-handshake.d.ts.map +1 -1
- package/dist/server-handshake.js +1 -0
- package/dist/server-handshake.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/terminal-title.d.ts.map +1 -1
- package/dist/terminal-title.js +3 -2
- package/dist/terminal-title.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 +21 -10
- package/package.json +1 -1
- package/src/agent-integration-health.ts +20 -1
- package/src/assimilate-cmd.ts +17 -0
- package/src/assimilate-deps.ts +4 -1
- package/src/backends/launch-all-terminals.ts +6 -3
- package/src/cli-help.ts +7 -4
- package/src/index.ts +30 -3
- package/src/opencode-drone.ts +461 -73
- package/src/opencode-seat-identity.ts +1 -0
- package/src/private-root.ts +38 -1
- package/src/roster-render.ts +2 -3
- package/src/seats.ts +15 -0
- package/src/server-errors.ts +50 -0
- package/src/server-handshake.ts +1 -0
- package/src/stream-status.ts +7 -2
- package/src/terminal-title.ts +4 -2
- package/src/update-cmd.ts +118 -41
package/dist/opencode-drone.js
CHANGED
|
@@ -1,17 +1,114 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createHash } from 'crypto';
|
|
1
|
+
import { closeSync, constants, existsSync, fchmodSync, fstatSync, openSync, readFileSync, readSync, renameSync, unlinkSync, writeFileSync, writeSync, } from 'fs';
|
|
2
|
+
import { createHash, randomUUID } from 'crypto';
|
|
3
3
|
import { createServer } from 'node:net';
|
|
4
4
|
import { join } from 'path';
|
|
5
5
|
import { tmpdir } from 'os';
|
|
6
|
+
import { borgConfigRoot, ensurePrivateBorgConfigRoot, ensurePrivateBorgConfigRootSync, } from './private-root.js';
|
|
6
7
|
import { OPENCODE_INJECTED_ENTRY_METADATA_KEY, OPENCODE_WAKE_IDENTITY_METADATA_KEY, OPENCODE_LAUNCH_CORRELATION_METADATA_KEY, } from './opencode-plugin.js';
|
|
7
8
|
import { createOpenCodeLaunchTrust, isOpenCode256BitIdentity, OPENCODE_SERVER_USERNAME, } from './opencode-launch-trust.js';
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
import { OpenCodeAuthenticationError, OpenCodeHttpError, OpenCodeResponseError, OpenCodeUnreachableError, } from './server-errors.js';
|
|
10
|
+
const OPEN_CODE_DIAGNOSTIC_LOG_MAX_BYTES = 64 * 1024;
|
|
11
|
+
const diagnosticLogPathsForTests = new Set();
|
|
12
|
+
function stateIdentityDigest(current) {
|
|
13
|
+
const key = [current.serverUrl, current.directory, current.cubeName, current.droneLabel].join('\0');
|
|
14
|
+
return createHash('sha256').update(key).digest('hex').slice(0, 24);
|
|
15
|
+
}
|
|
16
|
+
export function openCodeStartupDiagnosticLogPath() {
|
|
17
|
+
return join(borgConfigRoot(), 'opencode-drone-startup.log');
|
|
18
|
+
}
|
|
19
|
+
function diagnosticLogPath(owner) {
|
|
20
|
+
const root = borgConfigRoot();
|
|
21
|
+
const path = owner
|
|
22
|
+
? join(root, `opencode-drone-${stateIdentityDigest(owner)}.log`)
|
|
23
|
+
: openCodeStartupDiagnosticLogPath();
|
|
24
|
+
diagnosticLogPathsForTests.add(path);
|
|
25
|
+
return path;
|
|
26
|
+
}
|
|
27
|
+
function log(msg, owner = state, throwOnFailure = false) {
|
|
10
28
|
const line = `[${new Date().toISOString()}] ${msg}\n`;
|
|
29
|
+
let descriptor = null;
|
|
30
|
+
let temporaryDescriptor = null;
|
|
31
|
+
let temporary = null;
|
|
11
32
|
try {
|
|
12
|
-
|
|
33
|
+
ensurePrivateBorgConfigRootSync(borgConfigRoot());
|
|
34
|
+
const path = diagnosticLogPath(owner);
|
|
35
|
+
// The private root is the primary boundary. Where available, no-follow also
|
|
36
|
+
// closes the final replacement gap between root verification and this open.
|
|
37
|
+
const noFollow = constants.O_NOFOLLOW ?? 0;
|
|
38
|
+
descriptor = openSync(path, constants.O_RDWR |
|
|
39
|
+
constants.O_APPEND |
|
|
40
|
+
constants.O_CREAT |
|
|
41
|
+
noFollow, 0o600);
|
|
42
|
+
if (!fstatSync(descriptor).isFile()) {
|
|
43
|
+
throw Object.assign(new Error('OpenCode diagnostic log is not a regular file'), { code: 'EINVAL' });
|
|
44
|
+
}
|
|
45
|
+
fchmodSync(descriptor, 0o600);
|
|
46
|
+
writeSync(descriptor, line, null, 'utf8');
|
|
47
|
+
const size = fstatSync(descriptor).size;
|
|
48
|
+
if (size <= OPEN_CODE_DIAGNOSTIC_LOG_MAX_BYTES)
|
|
49
|
+
return;
|
|
50
|
+
const tail = Buffer.allocUnsafe(OPEN_CODE_DIAGNOSTIC_LOG_MAX_BYTES);
|
|
51
|
+
let bytesRead = 0;
|
|
52
|
+
while (bytesRead < tail.length) {
|
|
53
|
+
const count = readSync(descriptor, tail, bytesRead, tail.length - bytesRead, size - tail.length + bytesRead);
|
|
54
|
+
if (count === 0)
|
|
55
|
+
break;
|
|
56
|
+
bytesRead += count;
|
|
57
|
+
}
|
|
58
|
+
const completeTail = tail.subarray(0, bytesRead);
|
|
59
|
+
const firstNewline = completeTail.indexOf(0x0a);
|
|
60
|
+
const bounded = firstNewline >= 0 ? completeTail.subarray(firstNewline + 1) : completeTail;
|
|
61
|
+
temporary = `${path}.${randomUUID()}.tmp`;
|
|
62
|
+
temporaryDescriptor = openSync(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | noFollow, 0o600);
|
|
63
|
+
if (!fstatSync(temporaryDescriptor).isFile()) {
|
|
64
|
+
throw Object.assign(new Error('OpenCode diagnostic temporary is not a regular file'), { code: 'EINVAL' });
|
|
65
|
+
}
|
|
66
|
+
fchmodSync(temporaryDescriptor, 0o600);
|
|
67
|
+
let bytesWritten = 0;
|
|
68
|
+
while (bytesWritten < bounded.length) {
|
|
69
|
+
const count = writeSync(temporaryDescriptor, bounded, bytesWritten, bounded.length - bytesWritten);
|
|
70
|
+
if (count === 0)
|
|
71
|
+
throw Object.assign(new Error('OpenCode diagnostic temporary write stalled'), { code: 'EIO' });
|
|
72
|
+
bytesWritten += count;
|
|
73
|
+
}
|
|
74
|
+
closeSync(temporaryDescriptor);
|
|
75
|
+
temporaryDescriptor = null;
|
|
76
|
+
closeSync(descriptor);
|
|
77
|
+
descriptor = null;
|
|
78
|
+
renameSync(temporary, path);
|
|
79
|
+
temporary = null;
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
const code = error?.code ?? 'unknown';
|
|
83
|
+
process.stderr.write(`OpenCode diagnostic log write failed (${code})\n`);
|
|
84
|
+
if (throwOnFailure)
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
if (temporaryDescriptor !== null) {
|
|
89
|
+
try {
|
|
90
|
+
closeSync(temporaryDescriptor);
|
|
91
|
+
}
|
|
92
|
+
catch { /* The primary write error is already reported. */ }
|
|
93
|
+
}
|
|
94
|
+
if (descriptor !== null) {
|
|
95
|
+
try {
|
|
96
|
+
closeSync(descriptor);
|
|
97
|
+
}
|
|
98
|
+
catch { /* The primary write error is already reported. */ }
|
|
99
|
+
}
|
|
100
|
+
if (temporary !== null) {
|
|
101
|
+
try {
|
|
102
|
+
unlinkSync(temporary);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Already absent or inaccessible; the randomized name cannot be reused.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
13
108
|
}
|
|
14
|
-
|
|
109
|
+
}
|
|
110
|
+
export function writeOpenCodeStartupDiagnostic(message) {
|
|
111
|
+
log(message, null, true);
|
|
15
112
|
}
|
|
16
113
|
let state = null;
|
|
17
114
|
const OPEN_CODE_DELIVERY_RETRY_DELAYS_MS = [0, 250, 1_000, 3_000];
|
|
@@ -46,8 +143,9 @@ function abandonOpenCodeDeliveries(current) {
|
|
|
46
143
|
}
|
|
47
144
|
export async function connectOpenCodeDrone(deps) {
|
|
48
145
|
if (!isOpenCode256BitIdentity(deps.apiPassword)) {
|
|
49
|
-
throw new
|
|
146
|
+
throw new OpenCodeAuthenticationError('OpenCode API password is missing or unverifiable');
|
|
50
147
|
}
|
|
148
|
+
await ensurePrivateBorgConfigRoot(borgConfigRoot());
|
|
51
149
|
abandonOpenCodeDeliveries(state);
|
|
52
150
|
state = {
|
|
53
151
|
serverUrl: deps.serverUrl,
|
|
@@ -69,8 +167,18 @@ export async function connectOpenCodeDrone(deps) {
|
|
|
69
167
|
pendingSubmissions: new Map(),
|
|
70
168
|
reconcilingEntryIds: new Set(),
|
|
71
169
|
processingDeliveries: false,
|
|
170
|
+
nextObservationSequence: 0,
|
|
171
|
+
lastObservation: {
|
|
172
|
+
injectionSequence: 0,
|
|
173
|
+
acceptedSequence: 0,
|
|
174
|
+
failureSequence: 0,
|
|
175
|
+
lastInjectionAt: null,
|
|
176
|
+
lastInjectionResult: null,
|
|
177
|
+
lastAcceptedEntryId: null,
|
|
178
|
+
lastFailureCode: null,
|
|
179
|
+
},
|
|
72
180
|
};
|
|
73
|
-
log(`connected url=${deps.serverUrl} dir=${deps.directory}
|
|
181
|
+
log(`connected url=${deps.serverUrl} dir=${deps.directory}`, state);
|
|
74
182
|
}
|
|
75
183
|
// ---------------------------------------------------------------------------
|
|
76
184
|
// Raw fetch wrappers
|
|
@@ -82,7 +190,7 @@ function apiUrl(path) {
|
|
|
82
190
|
function authenticatedHeaders(headers = {}) {
|
|
83
191
|
const password = state?.apiPassword;
|
|
84
192
|
if (!isOpenCode256BitIdentity(password)) {
|
|
85
|
-
throw new
|
|
193
|
+
throw new OpenCodeAuthenticationError('OpenCode API password is missing or unverifiable');
|
|
86
194
|
}
|
|
87
195
|
return {
|
|
88
196
|
...headers,
|
|
@@ -99,6 +207,11 @@ async function rawGet(path) {
|
|
|
99
207
|
const body = await res.text();
|
|
100
208
|
return { status: res.status, body };
|
|
101
209
|
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
if (error instanceof OpenCodeAuthenticationError)
|
|
212
|
+
throw error;
|
|
213
|
+
throw new OpenCodeUnreachableError(controller.signal.aborted ? 'timeout' : 'transient', controller.signal.aborted ? 'OpenCode request timed out' : 'OpenCode request failed', { cause: error });
|
|
214
|
+
}
|
|
102
215
|
finally {
|
|
103
216
|
clearTimeout(timer);
|
|
104
217
|
}
|
|
@@ -117,29 +230,100 @@ async function rawPost(path, bodyObj) {
|
|
|
117
230
|
const body = await res.text();
|
|
118
231
|
return { status: res.status, body };
|
|
119
232
|
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
if (error instanceof OpenCodeAuthenticationError)
|
|
235
|
+
throw error;
|
|
236
|
+
throw new OpenCodeUnreachableError(controller.signal.aborted ? 'timeout' : 'transient', controller.signal.aborted ? 'OpenCode request timed out' : 'OpenCode request failed', { cause: error });
|
|
237
|
+
}
|
|
120
238
|
finally {
|
|
121
239
|
clearTimeout(timer);
|
|
122
240
|
}
|
|
123
241
|
}
|
|
242
|
+
function openCodeHttpError(status, operation) {
|
|
243
|
+
const code = status === 401
|
|
244
|
+
? 'unauthorized'
|
|
245
|
+
: status === 404
|
|
246
|
+
? 'not-found'
|
|
247
|
+
: status >= 500 || status === 429
|
|
248
|
+
? 'transient'
|
|
249
|
+
: 'incompatible-api';
|
|
250
|
+
return new OpenCodeHttpError(status, code, `OpenCode ${operation} request failed (${status})`);
|
|
251
|
+
}
|
|
252
|
+
function parseOpenCodeJson(body) {
|
|
253
|
+
try {
|
|
254
|
+
return JSON.parse(body);
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
throw new OpenCodeResponseError('OpenCode returned malformed JSON', { cause: error });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function isRecord(value) {
|
|
261
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
262
|
+
}
|
|
263
|
+
function decodeSession(value) {
|
|
264
|
+
if (!isRecord(value)
|
|
265
|
+
|| typeof value.id !== 'string'
|
|
266
|
+
|| value.id.length === 0
|
|
267
|
+
|| typeof value.directory !== 'string'
|
|
268
|
+
|| !isRecord(value.time)
|
|
269
|
+
|| typeof value.time.created !== 'number'
|
|
270
|
+
|| !Number.isFinite(value.time.created)
|
|
271
|
+
|| (value.parentID !== undefined && typeof value.parentID !== 'string')
|
|
272
|
+
|| (value.agent !== undefined && typeof value.agent !== 'string')
|
|
273
|
+
|| (value.model !== undefined && (!isRecord(value.model)
|
|
274
|
+
|| typeof value.model.providerID !== 'string'
|
|
275
|
+
|| typeof value.model.modelID !== 'string'))) {
|
|
276
|
+
throw new OpenCodeResponseError();
|
|
277
|
+
}
|
|
278
|
+
return value;
|
|
279
|
+
}
|
|
280
|
+
function decodeSessions(body) {
|
|
281
|
+
const value = parseOpenCodeJson(body);
|
|
282
|
+
if (!Array.isArray(value))
|
|
283
|
+
throw new OpenCodeResponseError();
|
|
284
|
+
return value.map(decodeSession);
|
|
285
|
+
}
|
|
286
|
+
function decodeMessages(body) {
|
|
287
|
+
const value = parseOpenCodeJson(body);
|
|
288
|
+
if (!Array.isArray(value))
|
|
289
|
+
throw new OpenCodeResponseError();
|
|
290
|
+
return value.map((message) => {
|
|
291
|
+
if (!isRecord(message))
|
|
292
|
+
throw new OpenCodeResponseError();
|
|
293
|
+
if (!isRecord(message.info)
|
|
294
|
+
|| typeof message.info.role !== 'string'
|
|
295
|
+
|| (message.info.id !== undefined && typeof message.info.id !== 'string')
|
|
296
|
+
|| (message.info.time !== undefined && (!isRecord(message.info.time)
|
|
297
|
+
|| (message.info.time.created !== undefined && typeof message.info.time.created !== 'number'))))
|
|
298
|
+
throw new OpenCodeResponseError();
|
|
299
|
+
if (!Array.isArray(message.parts)
|
|
300
|
+
|| message.parts.some((part) => !isRecord(part)
|
|
301
|
+
|| (part.type !== undefined && typeof part.type !== 'string')
|
|
302
|
+
|| (part.text !== undefined && typeof part.text !== 'string')
|
|
303
|
+
|| (part.metadata !== undefined && !isRecord(part.metadata))))
|
|
304
|
+
throw new OpenCodeResponseError();
|
|
305
|
+
return message;
|
|
306
|
+
});
|
|
307
|
+
}
|
|
124
308
|
async function listSessions() {
|
|
125
309
|
const { status, body } = await rawGet('/session');
|
|
126
310
|
if (status !== 200)
|
|
127
|
-
throw
|
|
128
|
-
return
|
|
311
|
+
throw openCodeHttpError(status, 'sessions');
|
|
312
|
+
return decodeSessions(body);
|
|
129
313
|
}
|
|
130
314
|
async function getSession(id) {
|
|
131
315
|
const { status, body } = await rawGet(`/session/${id}`);
|
|
132
316
|
if (status === 404)
|
|
133
317
|
return null;
|
|
134
318
|
if (status !== 200)
|
|
135
|
-
throw
|
|
136
|
-
return
|
|
319
|
+
throw openCodeHttpError(status, 'session');
|
|
320
|
+
return decodeSession(parseOpenCodeJson(body));
|
|
137
321
|
}
|
|
138
322
|
async function listSessionMessages(id) {
|
|
139
323
|
const { status, body } = await rawGet(`/session/${id}/message`);
|
|
140
324
|
if (status !== 200)
|
|
141
|
-
throw
|
|
142
|
-
return
|
|
325
|
+
throw openCodeHttpError(status, 'session messages');
|
|
326
|
+
return decodeMessages(body);
|
|
143
327
|
}
|
|
144
328
|
async function findInjectedMessage(sessionId, expectedWakeIdentity) {
|
|
145
329
|
const messages = await listSessionMessages(sessionId);
|
|
@@ -165,9 +349,7 @@ async function promptSession(id, bodyObj) {
|
|
|
165
349
|
// ---------------------------------------------------------------------------
|
|
166
350
|
function bindingPath() {
|
|
167
351
|
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`);
|
|
352
|
+
const path = join(tmpdir(), `borg-opencode-session-${stateIdentityDigest(current)}.json`);
|
|
171
353
|
bindingPathsForTests.add(path);
|
|
172
354
|
return path;
|
|
173
355
|
}
|
|
@@ -310,25 +492,20 @@ function restoreBinding() {
|
|
|
310
492
|
return binding;
|
|
311
493
|
}
|
|
312
494
|
function isBoundSession(session, binding) {
|
|
313
|
-
return session.id === binding.sessionId && session.directory ===
|
|
495
|
+
return session.id === binding.sessionId && session.directory === binding.directory;
|
|
314
496
|
}
|
|
315
497
|
function isTopLevelSession(session) {
|
|
316
498
|
return !session.parentID;
|
|
317
499
|
}
|
|
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 {
|
|
500
|
+
async function findUnseenTopLevelSession(knownRootSessionIds, directory) {
|
|
501
|
+
const sessions = await listSessions();
|
|
502
|
+
const roots = sessions.filter((session) => session.directory === directory
|
|
503
|
+
&& isTopLevelSession(session));
|
|
504
|
+
const matched = roots.filter((session) => !knownRootSessionIds.includes(session.id));
|
|
505
|
+
if (matched.length === 0)
|
|
330
506
|
return null;
|
|
331
|
-
|
|
507
|
+
const best = matched.reduce((a, b) => a.time.created > b.time.created ? a : b);
|
|
508
|
+
return { session: best, knownRootSessionIds: roots.map((session) => session.id) };
|
|
332
509
|
}
|
|
333
510
|
function launchCorrelationMatchCount(messages, correlationIdentity) {
|
|
334
511
|
let count = 0;
|
|
@@ -351,8 +528,13 @@ function launchCorrelationMatchCount(messages, correlationIdentity) {
|
|
|
351
528
|
* therefore allowed only when it received this launch's correlation metadata.
|
|
352
529
|
*/
|
|
353
530
|
async function findLaunchSession(correlationIdentity) {
|
|
531
|
+
const owner = state;
|
|
532
|
+
const observationSequence = ++owner.nextObservationSequence;
|
|
354
533
|
try {
|
|
355
|
-
const
|
|
534
|
+
const listedSessions = await listSessions();
|
|
535
|
+
if (state !== owner)
|
|
536
|
+
return null;
|
|
537
|
+
const sessions = listedSessions.filter((session) => session.directory === owner.directory);
|
|
356
538
|
const knownRootSessionIds = sessions
|
|
357
539
|
.filter(isTopLevelSession)
|
|
358
540
|
.map((session) => session.id);
|
|
@@ -360,24 +542,34 @@ async function findLaunchSession(correlationIdentity) {
|
|
|
360
542
|
session,
|
|
361
543
|
matchCount: launchCorrelationMatchCount(await listSessionMessages(session.id), correlationIdentity),
|
|
362
544
|
})));
|
|
545
|
+
if (state !== owner)
|
|
546
|
+
return null;
|
|
363
547
|
const totalMatches = candidates.reduce((total, candidate) => total + candidate.matchCount, 0);
|
|
364
548
|
if (totalMatches !== 1)
|
|
365
549
|
return null;
|
|
366
550
|
const matched = candidates.find((candidate) => candidate.matchCount === 1);
|
|
367
551
|
return matched ? { session: matched.session, knownRootSessionIds } : null;
|
|
368
552
|
}
|
|
369
|
-
catch {
|
|
553
|
+
catch (error) {
|
|
554
|
+
if (state === owner)
|
|
555
|
+
recordOpenCodeFailure(owner, error, observationSequence);
|
|
370
556
|
return null;
|
|
371
557
|
}
|
|
372
558
|
}
|
|
373
|
-
async function resolveInjectionSession() {
|
|
559
|
+
async function resolveInjectionSession(owner, observationSequence) {
|
|
560
|
+
if (state !== owner)
|
|
561
|
+
return null;
|
|
374
562
|
const binding = restoreBinding();
|
|
375
563
|
if (!binding)
|
|
376
564
|
return null;
|
|
377
565
|
const bound = await getSession(binding.sessionId);
|
|
566
|
+
if (state !== owner)
|
|
567
|
+
return null;
|
|
378
568
|
if (!bound || !isBoundSession(bound, binding)) {
|
|
379
569
|
clearBinding();
|
|
380
|
-
const replacement = await findUnseenTopLevelSession(binding.knownRootSessionIds);
|
|
570
|
+
const replacement = await findUnseenTopLevelSession(binding.knownRootSessionIds, owner.directory);
|
|
571
|
+
if (state !== owner)
|
|
572
|
+
return null;
|
|
381
573
|
if (!replacement)
|
|
382
574
|
return null;
|
|
383
575
|
saveBinding(replacement.session, replacement.knownRootSessionIds);
|
|
@@ -386,7 +578,15 @@ async function resolveInjectionSession() {
|
|
|
386
578
|
// `/new` creates an unseen top-level session. Keep the launch-time root
|
|
387
579
|
// snapshot so an old, unrelated root is never mistaken for a user switch.
|
|
388
580
|
// Children never supersede the bound root.
|
|
389
|
-
|
|
581
|
+
let switched = null;
|
|
582
|
+
try {
|
|
583
|
+
switched = await findUnseenTopLevelSession(binding.knownRootSessionIds, owner.directory);
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
recordOpenCodeFailure(owner, error, observationSequence);
|
|
587
|
+
}
|
|
588
|
+
if (state !== owner)
|
|
589
|
+
return null;
|
|
390
590
|
if (switched) {
|
|
391
591
|
saveBinding(switched.session, switched.knownRootSessionIds);
|
|
392
592
|
return switched.session;
|
|
@@ -403,6 +603,50 @@ function rememberBounded(entries, entryId, text, sourceEntryId) {
|
|
|
403
603
|
entries.delete(oldest);
|
|
404
604
|
}
|
|
405
605
|
}
|
|
606
|
+
function openCodeFailureCode(error) {
|
|
607
|
+
const code = error?.code;
|
|
608
|
+
if (typeof code === 'string' && code.length > 0)
|
|
609
|
+
return code;
|
|
610
|
+
return error instanceof Error && error.name ? error.name : 'unknown';
|
|
611
|
+
}
|
|
612
|
+
function updateLastOpenCodeObservation(owner, sequence, update) {
|
|
613
|
+
// Attempts, acceptances, and failures resolve independently; an observation
|
|
614
|
+
// may be stale for one field without being stale for the others.
|
|
615
|
+
const current = owner.lastObservation;
|
|
616
|
+
const updatesInjection = 'lastInjectionAt' in update || 'lastInjectionResult' in update;
|
|
617
|
+
const updatesAccepted = 'lastAcceptedEntryId' in update;
|
|
618
|
+
const updatesFailure = 'lastFailureCode' in update;
|
|
619
|
+
owner.lastObservation = {
|
|
620
|
+
...current,
|
|
621
|
+
...(updatesInjection && sequence >= current.injectionSequence
|
|
622
|
+
? {
|
|
623
|
+
injectionSequence: sequence,
|
|
624
|
+
...('lastInjectionAt' in update
|
|
625
|
+
? { lastInjectionAt: update.lastInjectionAt }
|
|
626
|
+
: {}),
|
|
627
|
+
...('lastInjectionResult' in update
|
|
628
|
+
? { lastInjectionResult: update.lastInjectionResult }
|
|
629
|
+
: {}),
|
|
630
|
+
}
|
|
631
|
+
: {}),
|
|
632
|
+
...(updatesAccepted && sequence >= current.acceptedSequence
|
|
633
|
+
? { acceptedSequence: sequence, lastAcceptedEntryId: update.lastAcceptedEntryId }
|
|
634
|
+
: {}),
|
|
635
|
+
...(updatesFailure && sequence >= current.failureSequence
|
|
636
|
+
? { failureSequence: sequence, lastFailureCode: update.lastFailureCode }
|
|
637
|
+
: {}),
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
function recordOpenCodeFailure(owner, error, observationSequence) {
|
|
641
|
+
updateLastOpenCodeObservation(owner, observationSequence, {
|
|
642
|
+
lastFailureCode: openCodeFailureCode(error),
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
function recordOpenCodeAcceptance(owner, delivery) {
|
|
646
|
+
updateLastOpenCodeObservation(owner, delivery.sequence, {
|
|
647
|
+
lastAcceptedEntryId: delivery.entryId,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
406
650
|
function clearPendingSubmission(owner, entryId) {
|
|
407
651
|
if (!owner.pendingSubmissions.delete(entryId))
|
|
408
652
|
return;
|
|
@@ -418,6 +662,11 @@ function confirmOpenCodeDelivery(owner, delivery) {
|
|
|
418
662
|
clearPendingSubmission(owner, delivery.entryId);
|
|
419
663
|
rememberBounded(owner.deliveredEntries, delivery.entryId, delivery.text, delivery.sourceEntryId);
|
|
420
664
|
owner.totalEntriesInjected++;
|
|
665
|
+
updateLastOpenCodeObservation(owner, delivery.sequence, {
|
|
666
|
+
lastAcceptedEntryId: delivery.entryId,
|
|
667
|
+
lastInjectionResult: 'delivered',
|
|
668
|
+
lastFailureCode: null,
|
|
669
|
+
});
|
|
421
670
|
}
|
|
422
671
|
function scheduleOpenCodeReconciliation(owner, delivery) {
|
|
423
672
|
if (!delivery.sessionId || owner.reconcilingEntryIds.has(delivery.entryId))
|
|
@@ -443,7 +692,8 @@ function scheduleOpenCodeReconciliation(owner, delivery) {
|
|
|
443
692
|
}
|
|
444
693
|
}
|
|
445
694
|
catch (err) {
|
|
446
|
-
|
|
695
|
+
recordOpenCodeFailure(owner, err, delivery.sequence);
|
|
696
|
+
log(`entry ${delivery.entryId} reconciliation unavailable: ${err}`, owner);
|
|
447
697
|
}
|
|
448
698
|
}
|
|
449
699
|
}
|
|
@@ -492,14 +742,16 @@ async function deliverOpenCodeEntry(owner, delivery) {
|
|
|
492
742
|
}
|
|
493
743
|
if (!target) {
|
|
494
744
|
try {
|
|
495
|
-
target = await resolveInjectionSession();
|
|
745
|
+
target = await resolveInjectionSession(owner, delivery.sequence);
|
|
496
746
|
}
|
|
497
747
|
catch (err) {
|
|
498
|
-
|
|
748
|
+
recordOpenCodeFailure(owner, err, delivery.sequence);
|
|
749
|
+
log(`entry ${delivery.entryId} target unavailable: ${err}`, owner);
|
|
499
750
|
continue;
|
|
500
751
|
}
|
|
501
752
|
if (!target) {
|
|
502
|
-
|
|
753
|
+
recordOpenCodeFailure(owner, openCodeHttpError(404, 'session'), delivery.sequence);
|
|
754
|
+
log(`entry ${delivery.entryId} target unavailable: no bound session`, owner);
|
|
503
755
|
return 'failed';
|
|
504
756
|
}
|
|
505
757
|
delivery.sessionId = target.id;
|
|
@@ -512,13 +764,14 @@ async function deliverOpenCodeEntry(owner, delivery) {
|
|
|
512
764
|
? null
|
|
513
765
|
: await findInjectedMessage(confirmationSessionId, delivery.sourceEntryId));
|
|
514
766
|
if (deliveredIdentity) {
|
|
515
|
-
log(`entry ${delivery.entryId} already present in session ${confirmationSessionId}
|
|
767
|
+
log(`entry ${delivery.entryId} already present in session ${confirmationSessionId}`, owner);
|
|
516
768
|
clearPendingSubmission(owner, delivery.entryId);
|
|
517
769
|
return 'delivered';
|
|
518
770
|
}
|
|
519
771
|
}
|
|
520
772
|
catch (err) {
|
|
521
|
-
|
|
773
|
+
recordOpenCodeFailure(owner, err, delivery.sequence);
|
|
774
|
+
log(`entry ${delivery.entryId} confirmation unavailable: ${err}`, owner);
|
|
522
775
|
continue;
|
|
523
776
|
}
|
|
524
777
|
const submittedBefore = pendingSubmission !== undefined;
|
|
@@ -540,7 +793,7 @@ async function deliverOpenCodeEntry(owner, delivery) {
|
|
|
540
793
|
});
|
|
541
794
|
if (!persistCurrentBinding()) {
|
|
542
795
|
owner.pendingSubmissions.delete(delivery.entryId);
|
|
543
|
-
log(`entry ${delivery.entryId} submission skipped: pending intent was not durable
|
|
796
|
+
log(`entry ${delivery.entryId} submission skipped: pending intent was not durable`, owner);
|
|
544
797
|
return 'failed';
|
|
545
798
|
}
|
|
546
799
|
// prompt_async is not idempotent. Persist the intent before the one POST;
|
|
@@ -559,16 +812,20 @@ async function deliverOpenCodeEntry(owner, delivery) {
|
|
|
559
812
|
});
|
|
560
813
|
}
|
|
561
814
|
catch (err) {
|
|
562
|
-
|
|
815
|
+
recordOpenCodeFailure(owner, err, delivery.sequence);
|
|
816
|
+
log(`entry ${delivery.entryId} submission outcome unavailable: ${err}`, owner);
|
|
563
817
|
}
|
|
564
818
|
delivery.state = 'delivered-unconfirmed';
|
|
565
819
|
if (status !== null && status !== 200 && status !== 204) {
|
|
820
|
+
recordOpenCodeFailure(owner, openCodeHttpError(status, 'prompt'), delivery.sequence);
|
|
566
821
|
clearPendingSubmission(owner, delivery.entryId);
|
|
567
822
|
if (status === 404)
|
|
568
823
|
clearBinding();
|
|
569
824
|
return 'failed';
|
|
570
825
|
}
|
|
571
826
|
delivery.acceptedSubmission = true;
|
|
827
|
+
if (status === 200 || status === 204)
|
|
828
|
+
recordOpenCodeAcceptance(owner, delivery);
|
|
572
829
|
}
|
|
573
830
|
for (let confirmationAttempt = 0; confirmationAttempt < OPEN_CODE_DELIVERY_RETRY_DELAYS_MS.length; confirmationAttempt++) {
|
|
574
831
|
if (confirmationAttempt > 0) {
|
|
@@ -588,7 +845,8 @@ async function deliverOpenCodeEntry(owner, delivery) {
|
|
|
588
845
|
}
|
|
589
846
|
}
|
|
590
847
|
catch (err) {
|
|
591
|
-
|
|
848
|
+
recordOpenCodeFailure(owner, err, delivery.sequence);
|
|
849
|
+
log(`entry ${delivery.entryId} post-acceptance confirmation unavailable: ${err}`, owner);
|
|
592
850
|
}
|
|
593
851
|
}
|
|
594
852
|
return 'delivered-unconfirmed';
|
|
@@ -605,13 +863,27 @@ async function processOpenCodeDeliveries(owner) {
|
|
|
605
863
|
try {
|
|
606
864
|
while (state === owner && owner.deliveryQueue.length > 0) {
|
|
607
865
|
const delivery = owner.deliveryQueue.shift();
|
|
866
|
+
updateLastOpenCodeObservation(owner, delivery.sequence, {
|
|
867
|
+
lastInjectionAt: Date.now(),
|
|
868
|
+
lastInjectionResult: null,
|
|
869
|
+
lastFailureCode: null,
|
|
870
|
+
});
|
|
608
871
|
let outcome = 'failed';
|
|
609
872
|
try {
|
|
610
873
|
outcome = await deliverOpenCodeEntry(owner, delivery);
|
|
611
874
|
}
|
|
612
875
|
catch (err) {
|
|
613
|
-
|
|
876
|
+
recordOpenCodeFailure(owner, err, delivery.sequence);
|
|
877
|
+
log(`entry ${delivery.entryId} delivery error: ${err}`, owner);
|
|
614
878
|
}
|
|
879
|
+
updateLastOpenCodeObservation(owner, delivery.sequence, {
|
|
880
|
+
lastInjectionResult: outcome,
|
|
881
|
+
lastFailureCode: outcome === 'delivered'
|
|
882
|
+
? null
|
|
883
|
+
: outcome === 'failed'
|
|
884
|
+
? (owner.lastObservation.lastFailureCode ?? 'unknown')
|
|
885
|
+
: owner.lastObservation.lastFailureCode,
|
|
886
|
+
});
|
|
615
887
|
owner.activeDeliveries.delete(delivery.entryId);
|
|
616
888
|
if (delivery.settled) {
|
|
617
889
|
delivery.resolve(true);
|
|
@@ -647,12 +919,13 @@ async function processOpenCodeDeliveries(owner) {
|
|
|
647
919
|
* the separate MCP-child process, which must never fall back to a newest-session heuristic.
|
|
648
920
|
*/
|
|
649
921
|
export async function injectInitialKickoff(launch) {
|
|
650
|
-
|
|
651
|
-
|
|
922
|
+
const owner = state;
|
|
923
|
+
if (!owner?.connected) {
|
|
924
|
+
log('kickoff: not connected', owner);
|
|
652
925
|
return false;
|
|
653
926
|
}
|
|
654
927
|
if (!isOpenCode256BitIdentity(launch.correlationIdentity)) {
|
|
655
|
-
log('kickoff: correlation identity missing or unverifiable');
|
|
928
|
+
log('kickoff: correlation identity missing or unverifiable', owner);
|
|
656
929
|
return false;
|
|
657
930
|
}
|
|
658
931
|
try {
|
|
@@ -660,7 +933,7 @@ export async function injectInitialKickoff(launch) {
|
|
|
660
933
|
for (let i = 0; i < 30; i++) {
|
|
661
934
|
try {
|
|
662
935
|
await listSessions();
|
|
663
|
-
log(`kickoff: server ready (attempt ${i + 1})
|
|
936
|
+
log(`kickoff: server ready (attempt ${i + 1})`, owner);
|
|
664
937
|
break;
|
|
665
938
|
}
|
|
666
939
|
catch {
|
|
@@ -673,17 +946,19 @@ export async function injectInitialKickoff(launch) {
|
|
|
673
946
|
for (let i = 0; i < 30; i++) {
|
|
674
947
|
const binding = await findLaunchSession(launch.correlationIdentity);
|
|
675
948
|
if (binding) {
|
|
949
|
+
if (state !== owner)
|
|
950
|
+
return false;
|
|
676
951
|
saveBinding(binding.session, binding.knownRootSessionIds);
|
|
677
|
-
log(`kickoff: bound session ${binding.session.id.slice(0, 8)}
|
|
952
|
+
log(`kickoff: bound session ${binding.session.id.slice(0, 8)}…`, owner);
|
|
678
953
|
return true;
|
|
679
954
|
}
|
|
680
955
|
await new Promise((r) => setTimeout(r, 1000));
|
|
681
956
|
}
|
|
682
|
-
log('kickoff: no session found');
|
|
957
|
+
log('kickoff: no session found', owner);
|
|
683
958
|
return false;
|
|
684
959
|
}
|
|
685
960
|
catch (err) {
|
|
686
|
-
log(`kickoff error: ${err}
|
|
961
|
+
log(`kickoff error: ${err}`, owner);
|
|
687
962
|
return false;
|
|
688
963
|
}
|
|
689
964
|
}
|
|
@@ -698,7 +973,7 @@ export async function injectInitialKickoff(launch) {
|
|
|
698
973
|
export function injectOpenCodeEntry(text, entryId = createHash('sha256').update(text).digest('hex'), allowSubmit = true, sourceEntryId = entryId, isSourcePending) {
|
|
699
974
|
const owner = state;
|
|
700
975
|
if (!owner?.connected) {
|
|
701
|
-
log(`entry ${entryId} rejected: OpenCode is not connected
|
|
976
|
+
log(`entry ${entryId} rejected: OpenCode is not connected`, owner);
|
|
702
977
|
return Promise.resolve(false);
|
|
703
978
|
}
|
|
704
979
|
// Rehydrate durable source markers before source-level deduplication. A
|
|
@@ -708,14 +983,14 @@ export function injectOpenCodeEntry(text, entryId = createHash('sha256').update(
|
|
|
708
983
|
restoreBinding();
|
|
709
984
|
const pendingSource = [...owner.pendingSubmissions].find(([pendingEntryId, pending]) => pendingEntryId !== entryId && pending.sourceEntryId === sourceEntryId);
|
|
710
985
|
if (pendingSource) {
|
|
711
|
-
log(`entry ${entryId} reconciles pending source ${sourceEntryId}
|
|
986
|
+
log(`entry ${entryId} reconciles pending source ${sourceEntryId}`, owner);
|
|
712
987
|
return injectOpenCodeEntry(text, pendingSource[0], false, sourceEntryId, isSourcePending);
|
|
713
988
|
}
|
|
714
989
|
for (const [deliveredEntryId, record] of owner.deliveredEntries) {
|
|
715
990
|
if (deliveredEntryId !== entryId && record.sourceEntryId === sourceEntryId) {
|
|
716
991
|
if (record.text !== text)
|
|
717
992
|
return Promise.resolve(false);
|
|
718
|
-
log(`entry ${entryId} source ${sourceEntryId} already delivered
|
|
993
|
+
log(`entry ${entryId} source ${sourceEntryId} already delivered`, owner);
|
|
719
994
|
return Promise.resolve(true);
|
|
720
995
|
}
|
|
721
996
|
}
|
|
@@ -723,7 +998,7 @@ export function injectOpenCodeEntry(text, entryId = createHash('sha256').update(
|
|
|
723
998
|
if (unconfirmedEntryId !== entryId && record.sourceEntryId === sourceEntryId) {
|
|
724
999
|
if (record.text !== text)
|
|
725
1000
|
return Promise.resolve(false);
|
|
726
|
-
log(`entry ${entryId} source ${sourceEntryId} remains unconfirmed
|
|
1001
|
+
log(`entry ${entryId} source ${sourceEntryId} remains unconfirmed`, owner);
|
|
727
1002
|
return Promise.resolve(true);
|
|
728
1003
|
}
|
|
729
1004
|
}
|
|
@@ -731,32 +1006,33 @@ export function injectOpenCodeEntry(text, entryId = createHash('sha256').update(
|
|
|
731
1006
|
if (active.entryId !== entryId && active.sourceEntryId === sourceEntryId) {
|
|
732
1007
|
if (active.text !== text)
|
|
733
1008
|
return Promise.resolve(false);
|
|
734
|
-
log(`entry ${entryId} joined active source ${sourceEntryId}
|
|
1009
|
+
log(`entry ${entryId} joined active source ${sourceEntryId}`, owner);
|
|
735
1010
|
return active.promise;
|
|
736
1011
|
}
|
|
737
1012
|
}
|
|
738
1013
|
const delivered = owner.deliveredEntries.get(entryId);
|
|
739
1014
|
if (delivered !== undefined) {
|
|
740
1015
|
if (delivered.text !== text || delivered.sourceEntryId !== sourceEntryId) {
|
|
741
|
-
log(`entry ${entryId} replay text mismatch
|
|
1016
|
+
log(`entry ${entryId} replay text mismatch`, owner);
|
|
742
1017
|
rememberBounded(owner.failedEntries, entryId, text, sourceEntryId);
|
|
743
1018
|
return Promise.resolve(false);
|
|
744
1019
|
}
|
|
745
|
-
log(`entry ${entryId} replay already delivered
|
|
1020
|
+
log(`entry ${entryId} replay already delivered`, owner);
|
|
746
1021
|
return Promise.resolve(true);
|
|
747
1022
|
}
|
|
748
1023
|
const unconfirmed = owner.unconfirmedEntries.get(entryId);
|
|
749
1024
|
if (unconfirmed !== undefined) {
|
|
750
1025
|
if (unconfirmed.text !== text || unconfirmed.sourceEntryId !== sourceEntryId) {
|
|
751
|
-
log(`entry ${entryId} unconfirmed replay text mismatch
|
|
1026
|
+
log(`entry ${entryId} unconfirmed replay text mismatch`, owner);
|
|
752
1027
|
rememberBounded(owner.failedEntries, entryId, text, sourceEntryId);
|
|
753
1028
|
return Promise.resolve(false);
|
|
754
1029
|
}
|
|
755
|
-
log(`entry ${entryId} replay remains unconfirmed
|
|
1030
|
+
log(`entry ${entryId} replay remains unconfirmed`, owner);
|
|
756
1031
|
const pending = owner.pendingSubmissions.get(entryId);
|
|
757
1032
|
const accepted = pending !== undefined;
|
|
758
1033
|
if (pending) {
|
|
759
1034
|
scheduleOpenCodeReconciliation(owner, {
|
|
1035
|
+
sequence: ++owner.nextObservationSequence,
|
|
760
1036
|
entryId,
|
|
761
1037
|
sourceEntryId,
|
|
762
1038
|
text,
|
|
@@ -774,11 +1050,11 @@ export function injectOpenCodeEntry(text, entryId = createHash('sha256').update(
|
|
|
774
1050
|
const active = owner.activeDeliveries.get(entryId);
|
|
775
1051
|
if (active) {
|
|
776
1052
|
if (active.text !== text || active.sourceEntryId !== sourceEntryId) {
|
|
777
|
-
log(`entry ${entryId} active text mismatch
|
|
1053
|
+
log(`entry ${entryId} active text mismatch`, owner);
|
|
778
1054
|
rememberBounded(owner.failedEntries, entryId, text, sourceEntryId);
|
|
779
1055
|
return Promise.resolve(false);
|
|
780
1056
|
}
|
|
781
|
-
log(`entry ${entryId} replay joined active delivery
|
|
1057
|
+
log(`entry ${entryId} replay joined active delivery`, owner);
|
|
782
1058
|
return active.promise;
|
|
783
1059
|
}
|
|
784
1060
|
let resolveDelivery;
|
|
@@ -786,6 +1062,7 @@ export function injectOpenCodeEntry(text, entryId = createHash('sha256').update(
|
|
|
786
1062
|
resolveDelivery = resolve;
|
|
787
1063
|
});
|
|
788
1064
|
const delivery = {
|
|
1065
|
+
sequence: ++owner.nextObservationSequence,
|
|
789
1066
|
entryId,
|
|
790
1067
|
sourceEntryId,
|
|
791
1068
|
text,
|
|
@@ -833,19 +1110,26 @@ export function settleOpenCodeEntry(sourceEntryId) {
|
|
|
833
1110
|
persistCurrentBinding();
|
|
834
1111
|
}
|
|
835
1112
|
export async function probeOpenCodeDroneArmed() {
|
|
836
|
-
|
|
1113
|
+
const owner = state;
|
|
1114
|
+
if (!owner?.connected)
|
|
837
1115
|
return null;
|
|
1116
|
+
const observationSequence = ++owner.nextObservationSequence;
|
|
838
1117
|
const binding = restoreBinding();
|
|
839
1118
|
if (!binding)
|
|
840
1119
|
return false;
|
|
841
1120
|
try {
|
|
842
1121
|
const session = await getSession(binding.sessionId);
|
|
1122
|
+
if (state !== owner)
|
|
1123
|
+
return null;
|
|
843
1124
|
if (session && isBoundSession(session, binding))
|
|
844
1125
|
return true;
|
|
1126
|
+
recordOpenCodeFailure(owner, openCodeHttpError(404, 'session'), observationSequence);
|
|
845
1127
|
clearBinding();
|
|
846
1128
|
return false;
|
|
847
1129
|
}
|
|
848
|
-
catch {
|
|
1130
|
+
catch (error) {
|
|
1131
|
+
if (state === owner)
|
|
1132
|
+
recordOpenCodeFailure(owner, error, observationSequence);
|
|
849
1133
|
return false;
|
|
850
1134
|
}
|
|
851
1135
|
}
|
|
@@ -868,9 +1152,23 @@ export function getOpenCodeConnectionState() {
|
|
|
868
1152
|
sessionId: state?.sessionId ?? null,
|
|
869
1153
|
totalEntriesInjected: state?.totalEntriesInjected ?? 0,
|
|
870
1154
|
totalEntriesRetried: state?.totalEntriesRetried ?? 0,
|
|
1155
|
+
lastInjectionAt: state?.lastObservation.lastInjectionAt ?? null,
|
|
1156
|
+
lastInjectionResult: state?.lastObservation.lastInjectionResult ?? null,
|
|
1157
|
+
lastAcceptedEntryId: state?.lastObservation.lastAcceptedEntryId ?? null,
|
|
1158
|
+
lastFailureCode: state?.lastObservation.lastFailureCode ?? null,
|
|
871
1159
|
deliveryStates,
|
|
872
1160
|
};
|
|
873
1161
|
}
|
|
1162
|
+
export function __getOpenCodeDiagnosticLogPathForTests() {
|
|
1163
|
+
if (!state)
|
|
1164
|
+
throw new Error('OpenCode drone is not connected');
|
|
1165
|
+
return diagnosticLogPath(state);
|
|
1166
|
+
}
|
|
1167
|
+
export function __getOpenCodeLastObservationForTests() {
|
|
1168
|
+
if (!state)
|
|
1169
|
+
throw new Error('OpenCode drone is not connected');
|
|
1170
|
+
return { ...state.lastObservation };
|
|
1171
|
+
}
|
|
874
1172
|
export function computeOpenCodePort(droneId, base = 14096) {
|
|
875
1173
|
let hash = 0;
|
|
876
1174
|
for (let i = 0; i < droneId.length; i++) {
|
|
@@ -938,5 +1236,14 @@ export function __resetOpenCodeDroneForTests() {
|
|
|
938
1236
|
}
|
|
939
1237
|
}
|
|
940
1238
|
bindingPathsForTests.clear();
|
|
1239
|
+
for (const path of diagnosticLogPathsForTests) {
|
|
1240
|
+
try {
|
|
1241
|
+
unlinkSync(path);
|
|
1242
|
+
}
|
|
1243
|
+
catch {
|
|
1244
|
+
// Already removed.
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
diagnosticLogPathsForTests.clear();
|
|
941
1248
|
}
|
|
942
1249
|
//# sourceMappingURL=opencode-drone.js.map
|