borgmcp 3.9.0 → 3.11.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/SECURITY.md +1 -1
- package/dist/assimilate-cmd.d.ts +14 -1
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +19 -12
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +25 -3
- package/dist/claude.js.map +1 -1
- package/dist/cli-help.d.ts +2 -0
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +33 -6
- package/dist/cli-help.js.map +1 -1
- package/dist/clone-cmd.d.ts +28 -0
- package/dist/clone-cmd.d.ts.map +1 -0
- package/dist/clone-cmd.js +227 -0
- package/dist/clone-cmd.js.map +1 -0
- package/dist/clone-security.d.ts +9 -0
- package/dist/clone-security.d.ts.map +1 -0
- package/dist/clone-security.js +41 -0
- package/dist/clone-security.js.map +1 -0
- package/dist/cubes.js +2 -2
- package/dist/cubes.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/launch-all-cmd.d.ts +11 -0
- package/dist/launch-all-cmd.d.ts.map +1 -1
- package/dist/launch-all-cmd.js +24 -2
- package/dist/launch-all-cmd.js.map +1 -1
- package/dist/log-stream.d.ts +6 -2
- package/dist/log-stream.d.ts.map +1 -1
- package/dist/log-stream.js +36 -11
- package/dist/log-stream.js.map +1 -1
- package/dist/opencode-drone.d.ts +7 -2
- package/dist/opencode-drone.d.ts.map +1 -1
- package/dist/opencode-drone.js +291 -64
- package/dist/opencode-drone.js.map +1 -1
- package/dist/opencode-plugin.d.ts.map +1 -1
- package/dist/opencode-plugin.js +12 -2
- package/dist/opencode-plugin.js.map +1 -1
- package/dist/parse-clone-args.d.ts +17 -0
- package/dist/parse-clone-args.d.ts.map +1 -0
- package/dist/parse-clone-args.js +38 -0
- package/dist/parse-clone-args.js.map +1 -0
- package/dist/parse-quickstart-args.d.ts +18 -0
- package/dist/parse-quickstart-args.d.ts.map +1 -0
- package/dist/parse-quickstart-args.js +43 -0
- package/dist/parse-quickstart-args.js.map +1 -0
- package/dist/quickstart-cmd.d.ts +23 -0
- package/dist/quickstart-cmd.d.ts.map +1 -0
- package/dist/quickstart-cmd.js +357 -0
- package/dist/quickstart-cmd.js.map +1 -0
- package/dist/remote-client.d.ts +13 -0
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +25 -0
- package/dist/remote-client.js.map +1 -1
- package/dist/seat-commands.js +12 -12
- package/dist/seat-commands.js.map +1 -1
- package/dist/stream-status.d.ts.map +1 -1
- package/dist/stream-status.js +1 -0
- package/dist/stream-status.js.map +1 -1
- package/dist/unknown-subcommand.d.ts +1 -1
- package/dist/unknown-subcommand.d.ts.map +1 -1
- package/dist/unknown-subcommand.js +3 -1
- package/dist/unknown-subcommand.js.map +1 -1
- package/docs/RELEASING.md +94 -23
- package/package.json +1 -1
- package/src/assimilate-cmd.ts +35 -12
- package/src/claude.ts +25 -3
- package/src/cli-help.ts +39 -6
- package/src/clone-cmd.ts +243 -0
- package/src/clone-security.ts +40 -0
- package/src/cubes.ts +2 -2
- package/src/index.ts +4 -1
- package/src/launch-all-cmd.ts +37 -2
- package/src/log-stream.ts +49 -12
- package/src/opencode-drone.ts +328 -65
- package/src/opencode-plugin.ts +13 -1
- package/src/parse-clone-args.ts +44 -0
- package/src/parse-quickstart-args.ts +54 -0
- package/src/quickstart-cmd.ts +396 -0
- package/src/remote-client.ts +36 -0
- package/src/seat-commands.ts +12 -12
- package/src/stream-status.ts +3 -0
- package/src/unknown-subcommand.ts +3 -1
package/src/opencode-drone.ts
CHANGED
|
@@ -27,9 +27,11 @@ interface OpenCodeDroneState {
|
|
|
27
27
|
totalEntriesRetried: number;
|
|
28
28
|
deliveryQueue: OpenCodeDelivery[];
|
|
29
29
|
activeDeliveries: Map<string, OpenCodeDelivery>;
|
|
30
|
-
deliveredEntries: Map<string,
|
|
31
|
-
unconfirmedEntries: Map<string,
|
|
32
|
-
failedEntries: Map<string,
|
|
30
|
+
deliveredEntries: Map<string, OpenCodeDeliveryRecord>;
|
|
31
|
+
unconfirmedEntries: Map<string, OpenCodeDeliveryRecord>;
|
|
32
|
+
failedEntries: Map<string, OpenCodeDeliveryRecord>;
|
|
33
|
+
pendingSubmissions: Map<string, PendingOpenCodeSubmission>;
|
|
34
|
+
reconcilingEntryIds: Set<string>;
|
|
33
35
|
processingDeliveries: boolean;
|
|
34
36
|
}
|
|
35
37
|
|
|
@@ -72,20 +74,40 @@ export type OpenCodeDeliveryState =
|
|
|
72
74
|
|
|
73
75
|
interface OpenCodeDelivery {
|
|
74
76
|
entryId: string;
|
|
77
|
+
sourceEntryId: string;
|
|
75
78
|
text: string;
|
|
76
79
|
allowSubmit: boolean;
|
|
80
|
+
acceptedSubmission: boolean;
|
|
81
|
+
sessionId: string | null;
|
|
82
|
+
settled: boolean;
|
|
77
83
|
state: Exclude<OpenCodeDeliveryState, 'failed'>;
|
|
78
84
|
resolve: (delivered: boolean) => void;
|
|
79
85
|
promise: Promise<boolean>;
|
|
80
86
|
}
|
|
81
87
|
|
|
88
|
+
interface OpenCodeDeliveryRecord {
|
|
89
|
+
text: string;
|
|
90
|
+
sourceEntryId: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface PendingOpenCodeSubmission {
|
|
94
|
+
sourceEntryId: string;
|
|
95
|
+
/** Session that received the one allowed prompt_async submission. */
|
|
96
|
+
sessionId: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
82
99
|
type OpenCodeDeliveryOutcome = 'delivered' | 'delivered-unconfirmed' | 'failed';
|
|
83
100
|
|
|
84
101
|
const OPEN_CODE_DELIVERY_RETRY_DELAYS_MS = [0, 250, 1_000, 3_000] as const;
|
|
102
|
+
const OPEN_CODE_RECONCILIATION_DELAY_MS = 3_000;
|
|
103
|
+
// Keep one accepted-but-unconfirmed prompt from holding timers forever. It
|
|
104
|
+
// remains pending (never failed or resubmitted), and any later wake retry
|
|
105
|
+
// re-arms another confirmation-only window.
|
|
106
|
+
const OPEN_CODE_RECONCILIATION_ATTEMPTS = 20;
|
|
85
107
|
const OPEN_CODE_DELIVERY_HISTORY_LIMIT = 256;
|
|
86
108
|
|
|
87
109
|
interface SessionBinding {
|
|
88
|
-
version:
|
|
110
|
+
version: 4;
|
|
89
111
|
sessionId: string;
|
|
90
112
|
sessionCreatedAt: number;
|
|
91
113
|
knownRootSessionIds: string[];
|
|
@@ -93,6 +115,11 @@ interface SessionBinding {
|
|
|
93
115
|
directory: string;
|
|
94
116
|
droneLabel: string;
|
|
95
117
|
cubeName: string;
|
|
118
|
+
pendingSubmissions: Array<{
|
|
119
|
+
entryId: string;
|
|
120
|
+
sourceEntryId: string;
|
|
121
|
+
sessionId: string;
|
|
122
|
+
}>;
|
|
96
123
|
}
|
|
97
124
|
|
|
98
125
|
export interface OpenCodeLaunchKickoff {
|
|
@@ -151,6 +178,8 @@ export async function connectOpenCodeDrone(deps: ConnectDeps): Promise<void> {
|
|
|
151
178
|
deliveredEntries: new Map(),
|
|
152
179
|
unconfirmedEntries: new Map(),
|
|
153
180
|
failedEntries: new Map(),
|
|
181
|
+
pendingSubmissions: new Map(),
|
|
182
|
+
reconcilingEntryIds: new Set(),
|
|
154
183
|
processingDeliveries: false,
|
|
155
184
|
};
|
|
156
185
|
log(`connected url=${deps.serverUrl} dir=${deps.directory}`);
|
|
@@ -257,7 +286,7 @@ function bindingPath(): string {
|
|
|
257
286
|
|
|
258
287
|
function bindingMatchesState(binding: SessionBinding): boolean {
|
|
259
288
|
const current = state!;
|
|
260
|
-
return binding.version ===
|
|
289
|
+
return binding.version === 4
|
|
261
290
|
&& binding.serverUrl === current.serverUrl
|
|
262
291
|
&& binding.directory === current.directory
|
|
263
292
|
&& binding.droneLabel === current.droneLabel
|
|
@@ -265,7 +294,13 @@ function bindingMatchesState(binding: SessionBinding): boolean {
|
|
|
265
294
|
&& typeof binding.sessionId === 'string'
|
|
266
295
|
&& typeof binding.sessionCreatedAt === 'number'
|
|
267
296
|
&& Array.isArray(binding.knownRootSessionIds)
|
|
268
|
-
&& binding.knownRootSessionIds.every((id) => typeof id === 'string')
|
|
297
|
+
&& binding.knownRootSessionIds.every((id) => typeof id === 'string')
|
|
298
|
+
&& Array.isArray(binding.pendingSubmissions)
|
|
299
|
+
&& binding.pendingSubmissions.every((pending) =>
|
|
300
|
+
typeof pending?.entryId === 'string'
|
|
301
|
+
&& typeof pending?.sourceEntryId === 'string'
|
|
302
|
+
&& typeof pending.sessionId === 'string'
|
|
303
|
+
);
|
|
269
304
|
}
|
|
270
305
|
|
|
271
306
|
function readBinding(): SessionBinding | null {
|
|
@@ -285,6 +320,10 @@ function clearBinding(): void {
|
|
|
285
320
|
state.sessionId = null;
|
|
286
321
|
state.sessionCreatedAt = null;
|
|
287
322
|
state.knownRootSessionIds = [];
|
|
323
|
+
// A missing/replaced target does not prove that an earlier prompt_async was
|
|
324
|
+
// rejected. Keep its durable submission marker (and origin session) so a
|
|
325
|
+
// replacement binding and later MCP-child reconnect remain confirmation-only.
|
|
326
|
+
if (state.pendingSubmissions.size > 0) return;
|
|
288
327
|
try {
|
|
289
328
|
unlinkSync(path);
|
|
290
329
|
} catch {
|
|
@@ -292,18 +331,8 @@ function clearBinding(): void {
|
|
|
292
331
|
}
|
|
293
332
|
}
|
|
294
333
|
|
|
295
|
-
function
|
|
334
|
+
function writeBinding(binding: SessionBinding): boolean {
|
|
296
335
|
const current = state!;
|
|
297
|
-
const binding: SessionBinding = {
|
|
298
|
-
version: 2,
|
|
299
|
-
sessionId: session.id,
|
|
300
|
-
sessionCreatedAt: session.time.created,
|
|
301
|
-
knownRootSessionIds,
|
|
302
|
-
serverUrl: current.serverUrl,
|
|
303
|
-
directory: current.directory,
|
|
304
|
-
droneLabel: current.droneLabel,
|
|
305
|
-
cubeName: current.cubeName,
|
|
306
|
-
};
|
|
307
336
|
current.sessionId = binding.sessionId;
|
|
308
337
|
current.sessionCreatedAt = binding.sessionCreatedAt;
|
|
309
338
|
current.knownRootSessionIds = binding.knownRootSessionIds;
|
|
@@ -313,16 +342,58 @@ function saveBinding(session: OCSession, knownRootSessionIds: string[]): void {
|
|
|
313
342
|
const temporary = `${path}.${process.pid}.tmp`;
|
|
314
343
|
writeFileSync(temporary, JSON.stringify(binding), { mode: 0o600 });
|
|
315
344
|
renameSync(temporary, path);
|
|
345
|
+
return true;
|
|
316
346
|
} catch (err) {
|
|
317
347
|
log(`session binding write failed: ${err}`);
|
|
348
|
+
return false;
|
|
318
349
|
}
|
|
319
350
|
}
|
|
320
351
|
|
|
352
|
+
function saveBinding(session: OCSession, knownRootSessionIds: string[]): void {
|
|
353
|
+
const current = state!;
|
|
354
|
+
const binding: SessionBinding = {
|
|
355
|
+
version: 4,
|
|
356
|
+
sessionId: session.id,
|
|
357
|
+
sessionCreatedAt: session.time.created,
|
|
358
|
+
knownRootSessionIds,
|
|
359
|
+
serverUrl: current.serverUrl,
|
|
360
|
+
directory: current.directory,
|
|
361
|
+
droneLabel: current.droneLabel,
|
|
362
|
+
cubeName: current.cubeName,
|
|
363
|
+
pendingSubmissions: [...current.pendingSubmissions].map(([entryId, pending]) => ({
|
|
364
|
+
entryId,
|
|
365
|
+
sourceEntryId: pending.sourceEntryId,
|
|
366
|
+
sessionId: pending.sessionId,
|
|
367
|
+
})),
|
|
368
|
+
};
|
|
369
|
+
writeBinding(binding);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function persistCurrentBinding(): boolean {
|
|
373
|
+
const current = state;
|
|
374
|
+
if (!current?.sessionId || current.sessionCreatedAt === null) return false;
|
|
375
|
+
return writeBinding({
|
|
376
|
+
version: 4,
|
|
377
|
+
sessionId: current.sessionId,
|
|
378
|
+
sessionCreatedAt: current.sessionCreatedAt,
|
|
379
|
+
knownRootSessionIds: current.knownRootSessionIds,
|
|
380
|
+
serverUrl: current.serverUrl,
|
|
381
|
+
directory: current.directory,
|
|
382
|
+
droneLabel: current.droneLabel,
|
|
383
|
+
cubeName: current.cubeName,
|
|
384
|
+
pendingSubmissions: [...current.pendingSubmissions].map(([entryId, pending]) => ({
|
|
385
|
+
entryId,
|
|
386
|
+
sourceEntryId: pending.sourceEntryId,
|
|
387
|
+
sessionId: pending.sessionId,
|
|
388
|
+
})),
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
321
392
|
function restoreBinding(): SessionBinding | null {
|
|
322
393
|
if (!state) return null;
|
|
323
394
|
if (state.sessionId && state.sessionCreatedAt !== null) {
|
|
324
395
|
return {
|
|
325
|
-
version:
|
|
396
|
+
version: 4,
|
|
326
397
|
sessionId: state.sessionId,
|
|
327
398
|
sessionCreatedAt: state.sessionCreatedAt,
|
|
328
399
|
knownRootSessionIds: state.knownRootSessionIds,
|
|
@@ -330,6 +401,11 @@ function restoreBinding(): SessionBinding | null {
|
|
|
330
401
|
directory: state.directory,
|
|
331
402
|
droneLabel: state.droneLabel,
|
|
332
403
|
cubeName: state.cubeName,
|
|
404
|
+
pendingSubmissions: [...state.pendingSubmissions].map(([entryId, pending]) => ({
|
|
405
|
+
entryId,
|
|
406
|
+
sourceEntryId: pending.sourceEntryId,
|
|
407
|
+
sessionId: pending.sessionId,
|
|
408
|
+
})),
|
|
333
409
|
};
|
|
334
410
|
}
|
|
335
411
|
|
|
@@ -338,6 +414,13 @@ function restoreBinding(): SessionBinding | null {
|
|
|
338
414
|
state.sessionId = binding.sessionId;
|
|
339
415
|
state.sessionCreatedAt = binding.sessionCreatedAt;
|
|
340
416
|
state.knownRootSessionIds = binding.knownRootSessionIds;
|
|
417
|
+
state.pendingSubmissions = new Map(binding.pendingSubmissions.map((pending) => [
|
|
418
|
+
pending.entryId,
|
|
419
|
+
{
|
|
420
|
+
sourceEntryId: pending.sourceEntryId,
|
|
421
|
+
sessionId: pending.sessionId,
|
|
422
|
+
},
|
|
423
|
+
]));
|
|
341
424
|
return binding;
|
|
342
425
|
}
|
|
343
426
|
|
|
@@ -451,12 +534,13 @@ async function resolveInjectionSession(): Promise<OCSession | null> {
|
|
|
451
534
|
}
|
|
452
535
|
|
|
453
536
|
function rememberBounded(
|
|
454
|
-
entries: Map<string,
|
|
537
|
+
entries: Map<string, OpenCodeDeliveryRecord>,
|
|
455
538
|
entryId: string,
|
|
456
539
|
text: string,
|
|
540
|
+
sourceEntryId: string,
|
|
457
541
|
): void {
|
|
458
542
|
entries.delete(entryId);
|
|
459
|
-
entries.set(entryId, text);
|
|
543
|
+
entries.set(entryId, { text, sourceEntryId });
|
|
460
544
|
while (entries.size > OPEN_CODE_DELIVERY_HISTORY_LIMIT) {
|
|
461
545
|
const oldest = entries.keys().next().value;
|
|
462
546
|
if (typeof oldest !== 'string') break;
|
|
@@ -464,6 +548,60 @@ function rememberBounded(
|
|
|
464
548
|
}
|
|
465
549
|
}
|
|
466
550
|
|
|
551
|
+
function clearPendingSubmission(owner: OpenCodeDroneState, entryId: string): void {
|
|
552
|
+
if (!owner.pendingSubmissions.delete(entryId)) return;
|
|
553
|
+
if (state === owner) persistCurrentBinding();
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function confirmOpenCodeDelivery(
|
|
557
|
+
owner: OpenCodeDroneState,
|
|
558
|
+
delivery: Pick<OpenCodeDelivery, 'entryId' | 'sourceEntryId' | 'text'>,
|
|
559
|
+
): void {
|
|
560
|
+
const unconfirmed = owner.unconfirmedEntries.get(delivery.entryId);
|
|
561
|
+
if (unconfirmed && unconfirmed.text !== delivery.text) return;
|
|
562
|
+
owner.unconfirmedEntries.delete(delivery.entryId);
|
|
563
|
+
owner.failedEntries.delete(delivery.entryId);
|
|
564
|
+
clearPendingSubmission(owner, delivery.entryId);
|
|
565
|
+
rememberBounded(
|
|
566
|
+
owner.deliveredEntries,
|
|
567
|
+
delivery.entryId,
|
|
568
|
+
delivery.text,
|
|
569
|
+
delivery.sourceEntryId,
|
|
570
|
+
);
|
|
571
|
+
owner.totalEntriesInjected++;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function scheduleOpenCodeReconciliation(
|
|
575
|
+
owner: OpenCodeDroneState,
|
|
576
|
+
delivery: OpenCodeDelivery,
|
|
577
|
+
): void {
|
|
578
|
+
if (!delivery.sessionId || owner.reconcilingEntryIds.has(delivery.entryId)) return;
|
|
579
|
+
owner.reconcilingEntryIds.add(delivery.entryId);
|
|
580
|
+
const sessionId = delivery.sessionId;
|
|
581
|
+
void (async () => {
|
|
582
|
+
try {
|
|
583
|
+
for (let attempt = 0; attempt < OPEN_CODE_RECONCILIATION_ATTEMPTS; attempt++) {
|
|
584
|
+
await new Promise((resolve) => setTimeout(resolve, OPEN_CODE_RECONCILIATION_DELAY_MS));
|
|
585
|
+
if (state !== owner || !owner.connected || delivery.settled) return;
|
|
586
|
+
const record = owner.unconfirmedEntries.get(delivery.entryId);
|
|
587
|
+
if (!record || record.text !== delivery.text) return;
|
|
588
|
+
owner.totalEntriesRetried++;
|
|
589
|
+
try {
|
|
590
|
+
if (await findInjectedMessage(sessionId, delivery.entryId)) {
|
|
591
|
+
if (delivery.settled) return;
|
|
592
|
+
confirmOpenCodeDelivery(owner, delivery);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
} catch (err) {
|
|
596
|
+
log(`entry ${delivery.entryId} reconciliation unavailable: ${err}`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
} finally {
|
|
600
|
+
owner.reconcilingEntryIds.delete(delivery.entryId);
|
|
601
|
+
}
|
|
602
|
+
})();
|
|
603
|
+
}
|
|
604
|
+
|
|
467
605
|
function waitForDeliveryRetry(attempt: number): Promise<void> {
|
|
468
606
|
const delay = OPEN_CODE_DELIVERY_RETRY_DELAYS_MS[attempt] ?? 0;
|
|
469
607
|
return delay > 0
|
|
@@ -483,11 +621,13 @@ async function deliverOpenCodeEntry(
|
|
|
483
621
|
// ever becoming the active user turn. The unique inbox text correlates the
|
|
484
622
|
// generated message across confirmation and process-replay instead.
|
|
485
623
|
for (let attempt = 0; attempt < OPEN_CODE_DELIVERY_RETRY_DELAYS_MS.length; attempt++) {
|
|
624
|
+
if (delivery.settled) return 'delivered';
|
|
486
625
|
if (state !== owner || !owner.connected) return 'failed';
|
|
487
626
|
if (attempt > 0) {
|
|
488
627
|
delivery.state = 'retried';
|
|
489
628
|
owner.totalEntriesRetried++;
|
|
490
629
|
await waitForDeliveryRetry(attempt);
|
|
630
|
+
if (delivery.settled) return 'delivered';
|
|
491
631
|
if (state !== owner || !owner.connected) return 'failed';
|
|
492
632
|
}
|
|
493
633
|
|
|
@@ -502,11 +642,22 @@ async function deliverOpenCodeEntry(
|
|
|
502
642
|
log(`entry ${delivery.entryId} target unavailable: no bound session`);
|
|
503
643
|
return 'failed';
|
|
504
644
|
}
|
|
645
|
+
delivery.sessionId = target.id;
|
|
505
646
|
}
|
|
506
647
|
|
|
648
|
+
const pendingSubmission = owner.pendingSubmissions.get(delivery.entryId);
|
|
649
|
+
const confirmationSessionId = pendingSubmission?.sessionId ?? target.id;
|
|
650
|
+
delivery.sessionId = confirmationSessionId;
|
|
651
|
+
|
|
507
652
|
try {
|
|
508
|
-
|
|
509
|
-
|
|
653
|
+
const deliveredIdentity = await findInjectedMessage(confirmationSessionId, delivery.entryId) ?? (
|
|
654
|
+
delivery.sourceEntryId === delivery.entryId
|
|
655
|
+
? null
|
|
656
|
+
: await findInjectedMessage(confirmationSessionId, delivery.sourceEntryId)
|
|
657
|
+
);
|
|
658
|
+
if (deliveredIdentity) {
|
|
659
|
+
log(`entry ${delivery.entryId} already present in session ${confirmationSessionId}`);
|
|
660
|
+
clearPendingSubmission(owner, delivery.entryId);
|
|
510
661
|
return 'delivered';
|
|
511
662
|
}
|
|
512
663
|
} catch (err) {
|
|
@@ -514,33 +665,51 @@ async function deliverOpenCodeEntry(
|
|
|
514
665
|
continue;
|
|
515
666
|
}
|
|
516
667
|
|
|
517
|
-
|
|
668
|
+
const submittedBefore = pendingSubmission !== undefined;
|
|
669
|
+
if (!delivery.allowSubmit && !submittedBefore) {
|
|
518
670
|
continue;
|
|
519
671
|
}
|
|
520
672
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
text: delivery.text,
|
|
530
|
-
metadata: {
|
|
531
|
-
[OPENCODE_INJECTED_ENTRY_METADATA_KEY]: true,
|
|
532
|
-
[OPENCODE_WAKE_IDENTITY_METADATA_KEY]: delivery.entryId,
|
|
533
|
-
},
|
|
534
|
-
}],
|
|
673
|
+
if (submittedBefore) {
|
|
674
|
+
delivery.acceptedSubmission = true;
|
|
675
|
+
delivery.state = 'delivered-unconfirmed';
|
|
676
|
+
} else {
|
|
677
|
+
if (delivery.settled) return 'delivered';
|
|
678
|
+
owner.pendingSubmissions.set(delivery.entryId, {
|
|
679
|
+
sourceEntryId: delivery.sourceEntryId,
|
|
680
|
+
sessionId: target.id,
|
|
535
681
|
});
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
682
|
+
if (!persistCurrentBinding()) {
|
|
683
|
+
owner.pendingSubmissions.delete(delivery.entryId);
|
|
684
|
+
log(`entry ${delivery.entryId} submission skipped: pending intent was not durable`);
|
|
685
|
+
return 'failed';
|
|
686
|
+
}
|
|
539
687
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
688
|
+
// prompt_async is not idempotent. Persist the intent before the one POST;
|
|
689
|
+
// every recovery path is confirmation-only until that identity appears.
|
|
690
|
+
let status: number | null = null;
|
|
691
|
+
try {
|
|
692
|
+
status = await promptSession(target.id, {
|
|
693
|
+
parts: [{
|
|
694
|
+
type: 'text',
|
|
695
|
+
text: delivery.text,
|
|
696
|
+
metadata: {
|
|
697
|
+
[OPENCODE_INJECTED_ENTRY_METADATA_KEY]: true,
|
|
698
|
+
[OPENCODE_WAKE_IDENTITY_METADATA_KEY]: delivery.entryId,
|
|
699
|
+
},
|
|
700
|
+
}],
|
|
701
|
+
});
|
|
702
|
+
} catch (err) {
|
|
703
|
+
log(`entry ${delivery.entryId} submission outcome unavailable: ${err}`);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
delivery.state = 'delivered-unconfirmed';
|
|
707
|
+
if (status !== null && status !== 200 && status !== 204) {
|
|
708
|
+
clearPendingSubmission(owner, delivery.entryId);
|
|
709
|
+
if (status === 404) clearBinding();
|
|
710
|
+
return 'failed';
|
|
711
|
+
}
|
|
712
|
+
delivery.acceptedSubmission = true;
|
|
544
713
|
}
|
|
545
714
|
|
|
546
715
|
for (
|
|
@@ -552,11 +721,13 @@ async function deliverOpenCodeEntry(
|
|
|
552
721
|
delivery.state = 'retried';
|
|
553
722
|
owner.totalEntriesRetried++;
|
|
554
723
|
await waitForDeliveryRetry(confirmationAttempt);
|
|
724
|
+
if (delivery.settled) return 'delivered';
|
|
555
725
|
if (state !== owner || !owner.connected) return 'delivered-unconfirmed';
|
|
556
726
|
delivery.state = 'delivered-unconfirmed';
|
|
557
727
|
}
|
|
558
728
|
try {
|
|
559
|
-
if (await findInjectedMessage(
|
|
729
|
+
if (await findInjectedMessage(delivery.sessionId!, delivery.entryId)) {
|
|
730
|
+
clearPendingSubmission(owner, delivery.entryId);
|
|
560
731
|
return 'delivered';
|
|
561
732
|
}
|
|
562
733
|
} catch (err) {
|
|
@@ -587,19 +758,29 @@ async function processOpenCodeDeliveries(owner: OpenCodeDroneState): Promise<voi
|
|
|
587
758
|
}
|
|
588
759
|
|
|
589
760
|
owner.activeDeliveries.delete(delivery.entryId);
|
|
590
|
-
if (
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
owner.totalEntriesInjected++;
|
|
761
|
+
if (delivery.settled) {
|
|
762
|
+
delivery.resolve(true);
|
|
763
|
+
} else if (outcome === 'delivered') {
|
|
764
|
+
confirmOpenCodeDelivery(owner, delivery);
|
|
595
765
|
delivery.resolve(true);
|
|
596
766
|
} else if (outcome === 'delivered-unconfirmed') {
|
|
597
767
|
owner.failedEntries.delete(delivery.entryId);
|
|
598
|
-
rememberBounded(
|
|
599
|
-
|
|
768
|
+
rememberBounded(
|
|
769
|
+
owner.unconfirmedEntries,
|
|
770
|
+
delivery.entryId,
|
|
771
|
+
delivery.text,
|
|
772
|
+
delivery.sourceEntryId,
|
|
773
|
+
);
|
|
774
|
+
delivery.resolve(delivery.acceptedSubmission);
|
|
775
|
+
if (delivery.acceptedSubmission) scheduleOpenCodeReconciliation(owner, delivery);
|
|
600
776
|
} else {
|
|
601
777
|
owner.unconfirmedEntries.delete(delivery.entryId);
|
|
602
|
-
rememberBounded(
|
|
778
|
+
rememberBounded(
|
|
779
|
+
owner.failedEntries,
|
|
780
|
+
delivery.entryId,
|
|
781
|
+
delivery.text,
|
|
782
|
+
delivery.sourceEntryId,
|
|
783
|
+
);
|
|
603
784
|
delivery.resolve(false);
|
|
604
785
|
}
|
|
605
786
|
}
|
|
@@ -657,12 +838,15 @@ export async function injectInitialKickoff(launch: OpenCodeLaunchKickoff): Promi
|
|
|
657
838
|
* Queue one durable inbox entry for delivery into the bound OpenCode session.
|
|
658
839
|
* The delivery identity is stored in TextPart metadata, so retries and replay
|
|
659
840
|
* can confirm an earlier ambiguous submission without supplying an
|
|
660
|
-
* ordering-breaking caller message ID or exposing the identity in delivered
|
|
841
|
+
* ordering-breaking caller message ID or exposing the identity in delivered
|
|
842
|
+
* text. Retry nonces also carry their durable source entry ID so they reconcile
|
|
843
|
+
* one submission instead of creating a second prompt.
|
|
661
844
|
*/
|
|
662
845
|
export function injectOpenCodeEntry(
|
|
663
846
|
text: string,
|
|
664
847
|
entryId: string = createHash('sha256').update(text).digest('hex'),
|
|
665
848
|
allowSubmit: boolean = true,
|
|
849
|
+
sourceEntryId: string = entryId,
|
|
666
850
|
): Promise<boolean> {
|
|
667
851
|
const owner = state;
|
|
668
852
|
if (!owner?.connected) {
|
|
@@ -670,33 +854,84 @@ export function injectOpenCodeEntry(
|
|
|
670
854
|
return Promise.resolve(false);
|
|
671
855
|
}
|
|
672
856
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
857
|
+
// Rehydrate durable source markers before source-level deduplication. A
|
|
858
|
+
// freshly connected MCP child starts with an empty in-memory map; waiting
|
|
859
|
+
// until target resolution would let a different wake nonce queue a second
|
|
860
|
+
// submission before the prior source identity is visible locally.
|
|
861
|
+
restoreBinding();
|
|
862
|
+
const pendingSource = [...owner.pendingSubmissions].find(
|
|
863
|
+
([pendingEntryId, pending]) =>
|
|
864
|
+
pendingEntryId !== entryId && pending.sourceEntryId === sourceEntryId,
|
|
865
|
+
);
|
|
866
|
+
if (pendingSource) {
|
|
867
|
+
log(`entry ${entryId} reconciles pending source ${sourceEntryId}`);
|
|
868
|
+
return injectOpenCodeEntry(text, pendingSource[0], false, sourceEntryId);
|
|
869
|
+
}
|
|
870
|
+
for (const [deliveredEntryId, record] of owner.deliveredEntries) {
|
|
871
|
+
if (deliveredEntryId !== entryId && record.sourceEntryId === sourceEntryId) {
|
|
872
|
+
if (record.text !== text) return Promise.resolve(false);
|
|
873
|
+
log(`entry ${entryId} source ${sourceEntryId} already delivered`);
|
|
874
|
+
return Promise.resolve(true);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
for (const [unconfirmedEntryId, record] of owner.unconfirmedEntries) {
|
|
878
|
+
if (unconfirmedEntryId !== entryId && record.sourceEntryId === sourceEntryId) {
|
|
879
|
+
if (record.text !== text) return Promise.resolve(false);
|
|
880
|
+
log(`entry ${entryId} source ${sourceEntryId} remains unconfirmed`);
|
|
881
|
+
return Promise.resolve(true);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
for (const active of owner.activeDeliveries.values()) {
|
|
885
|
+
if (active.entryId !== entryId && active.sourceEntryId === sourceEntryId) {
|
|
886
|
+
if (active.text !== text) return Promise.resolve(false);
|
|
887
|
+
log(`entry ${entryId} joined active source ${sourceEntryId}`);
|
|
888
|
+
return active.promise;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const delivered = owner.deliveredEntries.get(entryId);
|
|
893
|
+
if (delivered !== undefined) {
|
|
894
|
+
if (delivered.text !== text || delivered.sourceEntryId !== sourceEntryId) {
|
|
676
895
|
log(`entry ${entryId} replay text mismatch`);
|
|
677
|
-
rememberBounded(owner.failedEntries, entryId, text);
|
|
896
|
+
rememberBounded(owner.failedEntries, entryId, text, sourceEntryId);
|
|
678
897
|
return Promise.resolve(false);
|
|
679
898
|
}
|
|
680
899
|
log(`entry ${entryId} replay already delivered`);
|
|
681
900
|
return Promise.resolve(true);
|
|
682
901
|
}
|
|
683
902
|
|
|
684
|
-
const
|
|
685
|
-
if (
|
|
686
|
-
if (
|
|
903
|
+
const unconfirmed = owner.unconfirmedEntries.get(entryId);
|
|
904
|
+
if (unconfirmed !== undefined) {
|
|
905
|
+
if (unconfirmed.text !== text || unconfirmed.sourceEntryId !== sourceEntryId) {
|
|
687
906
|
log(`entry ${entryId} unconfirmed replay text mismatch`);
|
|
688
|
-
rememberBounded(owner.failedEntries, entryId, text);
|
|
907
|
+
rememberBounded(owner.failedEntries, entryId, text, sourceEntryId);
|
|
689
908
|
return Promise.resolve(false);
|
|
690
909
|
}
|
|
691
910
|
log(`entry ${entryId} replay remains unconfirmed`);
|
|
692
|
-
|
|
911
|
+
const pending = owner.pendingSubmissions.get(entryId);
|
|
912
|
+
const accepted = pending !== undefined;
|
|
913
|
+
if (pending) {
|
|
914
|
+
scheduleOpenCodeReconciliation(owner, {
|
|
915
|
+
entryId,
|
|
916
|
+
sourceEntryId,
|
|
917
|
+
text,
|
|
918
|
+
allowSubmit: false,
|
|
919
|
+
acceptedSubmission: true,
|
|
920
|
+
sessionId: pending.sessionId,
|
|
921
|
+
settled: false,
|
|
922
|
+
state: 'delivered-unconfirmed',
|
|
923
|
+
resolve: () => {},
|
|
924
|
+
promise: Promise.resolve(true),
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
return Promise.resolve(accepted);
|
|
693
928
|
}
|
|
694
929
|
|
|
695
930
|
const active = owner.activeDeliveries.get(entryId);
|
|
696
931
|
if (active) {
|
|
697
|
-
if (active.text !== text) {
|
|
932
|
+
if (active.text !== text || active.sourceEntryId !== sourceEntryId) {
|
|
698
933
|
log(`entry ${entryId} active text mismatch`);
|
|
699
|
-
rememberBounded(owner.failedEntries, entryId, text);
|
|
934
|
+
rememberBounded(owner.failedEntries, entryId, text, sourceEntryId);
|
|
700
935
|
return Promise.resolve(false);
|
|
701
936
|
}
|
|
702
937
|
log(`entry ${entryId} replay joined active delivery`);
|
|
@@ -709,8 +944,12 @@ export function injectOpenCodeEntry(
|
|
|
709
944
|
});
|
|
710
945
|
const delivery: OpenCodeDelivery = {
|
|
711
946
|
entryId,
|
|
947
|
+
sourceEntryId,
|
|
712
948
|
text,
|
|
713
949
|
allowSubmit,
|
|
950
|
+
acceptedSubmission: false,
|
|
951
|
+
sessionId: null,
|
|
952
|
+
settled: false,
|
|
714
953
|
state: 'queued',
|
|
715
954
|
resolve: resolveDelivery,
|
|
716
955
|
promise,
|
|
@@ -723,6 +962,30 @@ export function injectOpenCodeEntry(
|
|
|
723
962
|
return promise;
|
|
724
963
|
}
|
|
725
964
|
|
|
965
|
+
/** Stop retrying every delivery identity derived from a durable entry that the
|
|
966
|
+
* agent has already consumed. Confirmed history stays available for dedup. */
|
|
967
|
+
export function settleOpenCodeEntry(sourceEntryId: string): void {
|
|
968
|
+
const owner = state;
|
|
969
|
+
if (!owner) return;
|
|
970
|
+
|
|
971
|
+
let bindingChanged = false;
|
|
972
|
+
for (const [entryId, record] of owner.unconfirmedEntries) {
|
|
973
|
+
if (record.sourceEntryId !== sourceEntryId) continue;
|
|
974
|
+
owner.unconfirmedEntries.delete(entryId);
|
|
975
|
+
bindingChanged = owner.pendingSubmissions.delete(entryId) || bindingChanged;
|
|
976
|
+
}
|
|
977
|
+
for (const [entryId, record] of owner.failedEntries) {
|
|
978
|
+
if (record.sourceEntryId === sourceEntryId) owner.failedEntries.delete(entryId);
|
|
979
|
+
}
|
|
980
|
+
for (const delivery of owner.activeDeliveries.values()) {
|
|
981
|
+
if (delivery.sourceEntryId !== sourceEntryId) continue;
|
|
982
|
+
delivery.settled = true;
|
|
983
|
+
delivery.resolve(true);
|
|
984
|
+
bindingChanged = owner.pendingSubmissions.delete(delivery.entryId) || bindingChanged;
|
|
985
|
+
}
|
|
986
|
+
if (bindingChanged) persistCurrentBinding();
|
|
987
|
+
}
|
|
988
|
+
|
|
726
989
|
export async function probeOpenCodeDroneArmed(): Promise<boolean | null> {
|
|
727
990
|
if (!state?.connected) return null;
|
|
728
991
|
const binding = restoreBinding();
|
package/src/opencode-plugin.ts
CHANGED
|
@@ -138,7 +138,19 @@ export function createOpenCodePluginCore(
|
|
|
138
138
|
try {
|
|
139
139
|
const history = await deps.listMessages(input.sessionID);
|
|
140
140
|
const nudge = deps.audit([...history, current]);
|
|
141
|
-
if (nudge)
|
|
141
|
+
if (nudge) {
|
|
142
|
+
// OpenCode's chat.message hook receives resolved parts after their
|
|
143
|
+
// id/sessionID/messageID fields have been assigned. A newly pushed
|
|
144
|
+
// part skips that assignment and fails the durable PartUpdated
|
|
145
|
+
// aggregate in Session.updatePart. Preserve the resolved identity by
|
|
146
|
+
// replacing an existing text part with an augmented copy instead.
|
|
147
|
+
const index = output.parts.findIndex((part) =>
|
|
148
|
+
part?.type === 'text' && typeof part.text === 'string');
|
|
149
|
+
if (index >= 0) {
|
|
150
|
+
const part = output.parts[index];
|
|
151
|
+
output.parts[index] = { ...part, text: `${part.text}\n\n${nudge}` };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
142
154
|
} catch {
|
|
143
155
|
// Audit is advisory and must never block a prompt.
|
|
144
156
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { redactCloneSecrets } from './clone-security.js';
|
|
2
|
+
|
|
3
|
+
export interface CloneArgs {
|
|
4
|
+
repositoryUrl: string;
|
|
5
|
+
destination?: string;
|
|
6
|
+
noLaunch: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type ParseCloneResult =
|
|
10
|
+
| { ok: true; args: CloneArgs }
|
|
11
|
+
| { ok: false; error: string };
|
|
12
|
+
|
|
13
|
+
export function parseCloneArgs(rawArgs: readonly string[]): ParseCloneResult {
|
|
14
|
+
let repositoryUrl: string | undefined;
|
|
15
|
+
let destination: string | undefined;
|
|
16
|
+
let noLaunch = false;
|
|
17
|
+
for (const arg of rawArgs) {
|
|
18
|
+
if (arg === '--no-launch') {
|
|
19
|
+
if (noLaunch) return { ok: false, error: '--no-launch was provided more than once' };
|
|
20
|
+
noLaunch = true;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (arg.startsWith('-')) {
|
|
24
|
+
const option = arg.startsWith('--') ? arg.split('=', 1)[0] : arg.slice(0, 2);
|
|
25
|
+
return { ok: false, error: `unknown option ${option}; the only option is --no-launch` };
|
|
26
|
+
}
|
|
27
|
+
if (repositoryUrl === undefined) repositoryUrl = arg;
|
|
28
|
+
else if (destination === undefined) destination = arg;
|
|
29
|
+
else return { ok: false, error: 'unexpected extra argument' };
|
|
30
|
+
}
|
|
31
|
+
if (!repositoryUrl) return { ok: false, error: 'a repository URL is required' };
|
|
32
|
+
return {
|
|
33
|
+
ok: true,
|
|
34
|
+
args: {
|
|
35
|
+
repositoryUrl,
|
|
36
|
+
...(destination === undefined ? {} : { destination }),
|
|
37
|
+
noLaunch,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function safeCloneParseError(result: Extract<ParseCloneResult, { ok: false }>): string {
|
|
43
|
+
return redactCloneSecrets(result.error);
|
|
44
|
+
}
|