@oh-my-pi/pi-coding-agent 17.2.5 → 17.2.7
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/CHANGELOG.md +35 -1
- package/dist/{CHANGELOG-q2w43aw3.md → CHANGELOG-1p8yvde7.md} +35 -1
- package/dist/cli.js +3340 -3339
- package/dist/types/advisor/advise-tool.d.ts +10 -4
- package/dist/types/autoresearch/tools/init-experiment.d.ts +19 -7
- package/dist/types/autoresearch/tools/log-experiment.d.ts +19 -11
- package/dist/types/autoresearch/tools/run-experiment.d.ts +4 -2
- package/dist/types/autoresearch/tools/update-notes.d.ts +5 -2
- package/dist/types/cli/gc-cli.d.ts +1 -0
- package/dist/types/commit/agentic/tools/analyze-file.d.ts +5 -2
- package/dist/types/commit/agentic/tools/git-file-diff.d.ts +5 -2
- package/dist/types/commit/agentic/tools/git-hunk.d.ts +6 -2
- package/dist/types/commit/agentic/tools/git-overview.d.ts +5 -2
- package/dist/types/commit/agentic/tools/propose-changelog.d.ts +29 -7
- package/dist/types/commit/agentic/tools/propose-commit.d.ts +14 -4
- package/dist/types/commit/agentic/tools/recent-commits.d.ts +4 -2
- package/dist/types/commit/agentic/tools/schemas.d.ts +7 -3
- package/dist/types/commit/agentic/tools/split-commit.d.ts +37 -10
- package/dist/types/commit/changelog/generate.d.ts +13 -3
- package/dist/types/commit/shared-llm.d.ts +13 -4
- package/dist/types/config/config-file.d.ts +1 -1
- package/dist/types/config/models-config-schema-bundle.d.ts +1992 -981
- package/dist/types/config/models-config-schema.d.ts +1201 -580
- package/dist/types/config/models-config.d.ts +355 -371
- package/dist/types/edit/hashline/params.d.ts +4 -2
- package/dist/types/edit/modes/apply-patch.d.ts +4 -2
- package/dist/types/edit/modes/patch.d.ts +16 -5
- package/dist/types/edit/modes/replace.d.ts +8 -3
- package/dist/types/eval/bridge-timeout.d.ts +5 -2
- package/dist/types/eval/completion-bridge.d.ts +13 -0
- package/dist/types/eval/py/tool-bridge.d.ts +12 -0
- package/dist/types/extensibility/custom-commands/types.d.ts +4 -4
- package/dist/types/extensibility/custom-tools/types.d.ts +4 -4
- package/dist/types/extensibility/extensions/types.d.ts +4 -4
- package/dist/types/extensibility/hooks/types.d.ts +4 -4
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +1 -1
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
- package/dist/types/extensibility/legacy-typebox.d.ts +8 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +6 -13
- package/dist/types/goals/tools/goal-tool.d.ts +12 -4
- package/dist/types/launch/client.d.ts +9 -1
- package/dist/types/launch/protocol.d.ts +17 -0
- package/dist/types/lsp/index.d.ts +15 -5
- package/dist/types/lsp/types.d.ts +15 -5
- package/dist/types/modes/components/btw-panel.d.ts +3 -0
- package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +4 -1
- package/dist/types/modes/types.d.ts +3 -1
- package/dist/types/security/contracts/schemas.d.ts +1115 -537
- package/dist/types/security/publication.d.ts +92 -34
- package/dist/types/session/agent-session-types.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +26 -2
- package/dist/types/session/launch-completion.d.ts +10 -0
- package/dist/types/session/session-entries.d.ts +15 -1
- package/dist/types/session/session-manager.d.ts +7 -0
- package/dist/types/session/yield-queue.d.ts +6 -1
- package/dist/types/task/types.d.ts +63 -18
- package/dist/types/tools/ask.d.ts +34 -8
- package/dist/types/tools/ast-edit.d.ts +16 -4
- package/dist/types/tools/ast-grep.d.ts +12 -4
- package/dist/types/tools/bash.d.ts +20 -11
- package/dist/types/tools/browser.d.ts +64 -20
- package/dist/types/tools/checkpoint.d.ts +16 -8
- package/dist/types/tools/computer.d.ts +1 -1
- package/dist/types/tools/debug.d.ts +118 -48
- package/dist/types/tools/eval.d.ts +11 -5
- package/dist/types/tools/gh.d.ts +74 -26
- package/dist/types/tools/glob.d.ts +16 -6
- package/dist/types/tools/grep.d.ts +18 -6
- package/dist/types/tools/hub/index.d.ts +116 -46
- package/dist/types/tools/image-gen.d.ts +25 -8
- package/dist/types/tools/index.d.ts +9 -0
- package/dist/types/tools/inspect-image.d.ts +10 -4
- package/dist/types/tools/learn.d.ts +26 -8
- package/dist/types/tools/manage-skill.d.ts +16 -6
- package/dist/types/tools/memory-edit.d.ts +18 -6
- package/dist/types/tools/memory-recall.d.ts +8 -4
- package/dist/types/tools/memory-reflect.d.ts +10 -4
- package/dist/types/tools/memory-retain.d.ts +14 -4
- package/dist/types/tools/read.d.ts +8 -4
- package/dist/types/tools/security-scan.d.ts +82 -30
- package/dist/types/tools/todo.d.ts +30 -10
- package/dist/types/tools/tts.d.ts +12 -5
- package/dist/types/tools/vibe.d.ts +42 -18
- package/dist/types/tools/write.d.ts +10 -4
- package/dist/types/web/search/index.d.ts +20 -6
- package/package.json +13 -13
- package/scripts/legacy-pi-virtual-module.ts +2 -2
- package/src/advisor/advise-tool.ts +1 -1
- package/src/advisor/config.ts +1 -1
- package/src/autoresearch/tools/init-experiment.ts +1 -2
- package/src/autoresearch/tools/log-experiment.ts +1 -2
- package/src/autoresearch/tools/run-experiment.ts +1 -1
- package/src/autoresearch/tools/update-notes.ts +1 -1
- package/src/cli/gc-cli.ts +641 -21
- package/src/commit/agentic/tools/analyze-file.ts +1 -1
- package/src/commit/agentic/tools/git-file-diff.ts +1 -1
- package/src/commit/agentic/tools/git-hunk.ts +1 -1
- package/src/commit/agentic/tools/git-overview.ts +1 -1
- package/src/commit/agentic/tools/propose-changelog.ts +1 -1
- package/src/commit/agentic/tools/propose-commit.ts +1 -1
- package/src/commit/agentic/tools/recent-commits.ts +1 -1
- package/src/commit/agentic/tools/schemas.ts +1 -1
- package/src/commit/agentic/tools/split-commit.ts +1 -1
- package/src/commit/analysis/summary.ts +1 -1
- package/src/commit/changelog/generate.ts +1 -1
- package/src/config/config-file.ts +2 -2
- package/src/config/model-discovery.ts +14 -14
- package/src/config/model-registry.ts +10 -8
- package/src/config/models-config-schema-bundle.ts +1 -8
- package/src/config/settings.ts +1 -1
- package/src/discovery/codex.ts +22 -9
- package/src/edit/hashline/params.ts +1 -1
- package/src/edit/modes/apply-patch.ts +1 -1
- package/src/edit/modes/patch.ts +1 -1
- package/src/edit/modes/replace.ts +2 -1
- package/src/eval/agent-bridge.ts +4 -2
- package/src/eval/bridge-timeout.ts +5 -2
- package/src/eval/completion-bridge.ts +3 -2
- package/src/eval/executor-base.ts +20 -1
- package/src/eval/js/context-manager.ts +94 -6
- package/src/eval/py/tool-bridge.ts +29 -9
- package/src/export/html/index.ts +10 -3
- package/src/export/share.ts +4 -0
- package/src/extensibility/custom-commands/loader.ts +3 -3
- package/src/extensibility/custom-commands/types.ts +4 -4
- package/src/extensibility/custom-tools/loader.ts +3 -3
- package/src/extensibility/custom-tools/types.ts +5 -4
- package/src/extensibility/extensions/loader.ts +3 -3
- package/src/extensibility/extensions/types.ts +5 -4
- package/src/extensibility/hooks/loader.ts +3 -3
- package/src/extensibility/hooks/types.ts +4 -4
- package/src/extensibility/legacy-pi-ai-shim.ts +1 -1
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +2 -2
- package/src/extensibility/legacy-typebox.ts +101 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +13 -22
- package/src/goals/tools/goal-tool.ts +1 -1
- package/src/launch/broker.ts +222 -6
- package/src/launch/client.ts +161 -9
- package/src/launch/protocol.ts +52 -0
- package/src/lsp/types.ts +1 -1
- package/src/main.ts +13 -6
- package/src/mcp/config-writer.ts +1 -1
- package/src/modes/components/btw-panel.ts +41 -4
- package/src/modes/controllers/btw-controller.ts +55 -7
- package/src/modes/controllers/command-controller.ts +31 -0
- package/src/modes/controllers/input-controller.ts +24 -7
- package/src/modes/interactive-mode.ts +16 -2
- package/src/modes/theme/theme.ts +3 -3
- package/src/modes/types.ts +8 -1
- package/src/prompts/session/launch-completion.md +1 -0
- package/src/sdk.ts +15 -0
- package/src/security/contracts/schemas.ts +202 -184
- package/src/security/contracts/validation.ts +6 -2
- package/src/security/publication.ts +1 -1
- package/src/security/store.ts +2 -2
- package/src/session/agent-session-types.ts +6 -0
- package/src/session/agent-session.ts +190 -14
- package/src/session/launch-completion.ts +37 -0
- package/src/session/session-context.ts +26 -0
- package/src/session/session-entries.ts +17 -1
- package/src/session/session-manager.ts +21 -0
- package/src/session/yield-queue.ts +121 -16
- package/src/slash-commands/builtin-registry.ts +10 -1
- package/src/task/types.ts +1 -1
- package/src/tools/ask.ts +1 -1
- package/src/tools/ast-edit.ts +1 -1
- package/src/tools/ast-grep.ts +1 -1
- package/src/tools/bash.ts +61 -3
- package/src/tools/browser/cmux/cmux-tab.ts +92 -4
- package/src/tools/browser.ts +1 -1
- package/src/tools/checkpoint.ts +1 -1
- package/src/tools/computer.ts +1 -1
- package/src/tools/debug.ts +1 -1
- package/src/tools/eval.ts +1 -1
- package/src/tools/gh.ts +1 -2
- package/src/tools/glob.ts +1 -1
- package/src/tools/grep.ts +1 -1
- package/src/tools/hub/index.ts +1 -1
- package/src/tools/hub/launch.ts +122 -6
- package/src/tools/image-gen.ts +1 -1
- package/src/tools/index.ts +9 -0
- package/src/tools/inspect-image.ts +1 -1
- package/src/tools/learn.ts +1 -1
- package/src/tools/manage-skill.ts +1 -1
- package/src/tools/memory-edit.ts +1 -1
- package/src/tools/memory-recall.ts +1 -1
- package/src/tools/memory-reflect.ts +1 -1
- package/src/tools/memory-retain.ts +1 -1
- package/src/tools/read.ts +1 -1
- package/src/tools/security-scan.ts +1 -1
- package/src/tools/todo.ts +1 -1
- package/src/tools/tts.ts +1 -1
- package/src/tools/vibe.ts +2 -1
- package/src/tools/write.ts +1 -1
- package/src/web/search/index.ts +2 -1
- package/dist/types/config/file-lock.d.ts +0 -29
- package/dist/types/extensibility/typebox.d.ts +0 -181
- package/src/config/file-lock.ts +0 -164
- package/src/extensibility/typebox.ts +0 -958
package/src/launch/broker.ts
CHANGED
|
@@ -15,14 +15,17 @@ import {
|
|
|
15
15
|
DAEMON_PTY_COLUMNS,
|
|
16
16
|
DAEMON_PTY_ROWS,
|
|
17
17
|
DAEMON_RUNTIME_DIR_ENV,
|
|
18
|
+
type DaemonCompletionNotification,
|
|
18
19
|
type DaemonOperation,
|
|
19
20
|
type DaemonReadySpec,
|
|
20
21
|
type DaemonRpcResult,
|
|
21
22
|
type DaemonSignal,
|
|
22
23
|
type DaemonSnapshot,
|
|
23
24
|
type DaemonSpec,
|
|
25
|
+
type DaemonWireRequest,
|
|
24
26
|
parseDaemonSnapshot,
|
|
25
27
|
parseDaemonSpec,
|
|
28
|
+
parseDaemonWireMessage,
|
|
26
29
|
parseDaemonWireRequest,
|
|
27
30
|
} from "./protocol";
|
|
28
31
|
import { resolveDaemonSpawnOptions } from "./spawn-options";
|
|
@@ -81,6 +84,9 @@ interface ManagedDaemon {
|
|
|
81
84
|
readyPattern?: RegExp;
|
|
82
85
|
restartTimer?: NodeJS.Timeout;
|
|
83
86
|
consecutiveFailures: number;
|
|
87
|
+
completionCapable: boolean;
|
|
88
|
+
pendingCompletions: DaemonCompletionNotification[];
|
|
89
|
+
completionSubscriptionId?: string;
|
|
84
90
|
persistQueue: Promise<void>;
|
|
85
91
|
}
|
|
86
92
|
|
|
@@ -107,6 +113,10 @@ function settledState(state: DaemonSnapshot["state"]): boolean {
|
|
|
107
113
|
return terminalState(state) || state === "restarting";
|
|
108
114
|
}
|
|
109
115
|
|
|
116
|
+
function publishesCompletionOwners(request: DaemonWireRequest): boolean {
|
|
117
|
+
return request.completionEvents === true && (request.completionAcks?.length ?? 0) === 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
110
120
|
/**
|
|
111
121
|
* Order daemons for the `list` response: non-terminal (active) daemons first,
|
|
112
122
|
* oldest to newest, so the process the user is acting on is immediately visible
|
|
@@ -352,6 +362,9 @@ class DaemonBroker {
|
|
|
352
362
|
*/
|
|
353
363
|
readonly #startingNames = new Set<string>();
|
|
354
364
|
readonly #clients = new Set<net.Socket>();
|
|
365
|
+
readonly #ownerSockets = new Map<string, { socket: net.Socket; subscriptionId: string | undefined }>();
|
|
366
|
+
readonly #completionSubscriptions = new Map<string, string | undefined>();
|
|
367
|
+
readonly #pendingCompletions = new Map<string, Map<string, DaemonCompletionNotification>>();
|
|
355
368
|
readonly #finished = Promise.withResolvers<void>();
|
|
356
369
|
readonly #sockets = new Set<net.Socket>();
|
|
357
370
|
#server: net.Server | undefined;
|
|
@@ -393,6 +406,7 @@ class DaemonBroker {
|
|
|
393
406
|
await record.log?.close();
|
|
394
407
|
await record.persistQueue;
|
|
395
408
|
}
|
|
409
|
+
this.#ownerSockets.clear();
|
|
396
410
|
for (const socket of this.#sockets) socket.destroy();
|
|
397
411
|
this.#sockets.clear();
|
|
398
412
|
this.#clients.clear();
|
|
@@ -439,6 +453,9 @@ class DaemonBroker {
|
|
|
439
453
|
if (!authenticated) return;
|
|
440
454
|
this.#clients.delete(socket);
|
|
441
455
|
this.#scheduleIdleShutdown();
|
|
456
|
+
for (const [owner, registration] of this.#ownerSockets) {
|
|
457
|
+
if (registration.socket === socket) this.#ownerSockets.delete(owner);
|
|
458
|
+
}
|
|
442
459
|
});
|
|
443
460
|
}
|
|
444
461
|
|
|
@@ -450,6 +467,78 @@ class DaemonBroker {
|
|
|
450
467
|
id = request.id;
|
|
451
468
|
if (request.token !== this.#token) throw new Error("Daemon broker authentication failed");
|
|
452
469
|
onAuthenticated();
|
|
470
|
+
for (const owner of request.completionUnsubscribes ?? []) {
|
|
471
|
+
const subscriptionId = this.#completionSubscriptions.get(owner);
|
|
472
|
+
if (
|
|
473
|
+
!this.#completionSubscriptions.has(owner) ||
|
|
474
|
+
(subscriptionId !== undefined && subscriptionId !== request.completionSubscriptionId)
|
|
475
|
+
) {
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
this.#ownerSockets.delete(owner);
|
|
479
|
+
this.#completionSubscriptions.delete(owner);
|
|
480
|
+
await this.#setRecordCompletionCapability(owner, false);
|
|
481
|
+
this.#pendingCompletions.delete(owner);
|
|
482
|
+
}
|
|
483
|
+
for (const completionId of request.completionAcks ?? []) {
|
|
484
|
+
for (const [owner, pending] of this.#pendingCompletions) {
|
|
485
|
+
const registration = this.#ownerSockets.get(owner);
|
|
486
|
+
if (
|
|
487
|
+
!registration ||
|
|
488
|
+
registration.socket !== socket ||
|
|
489
|
+
registration.subscriptionId !== request.completionSubscriptionId
|
|
490
|
+
) {
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
const completion = pending.get(completionId);
|
|
494
|
+
if (!completion) continue;
|
|
495
|
+
pending.delete(completionId);
|
|
496
|
+
if (pending.size === 0) this.#pendingCompletions.delete(owner);
|
|
497
|
+
const record = this.#records.get(completion.daemon.name);
|
|
498
|
+
const index = record?.pendingCompletions.findIndex(item => item.completionId === completionId) ?? -1;
|
|
499
|
+
if (record && index >= 0) {
|
|
500
|
+
record.pendingCompletions.splice(index, 1);
|
|
501
|
+
this.#persist(record);
|
|
502
|
+
await record.persistQueue;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (publishesCompletionOwners(request)) {
|
|
507
|
+
const replayOwners = new Set(request.completionReplays ?? []);
|
|
508
|
+
const activeOwners = new Set(request.owners ?? []);
|
|
509
|
+
const detachedOwners = new Set(request.detachedOwners ?? []);
|
|
510
|
+
const advertisedOwners = new Set([...activeOwners, ...detachedOwners]);
|
|
511
|
+
for (const [owner, subscriptionId] of this.#completionSubscriptions) {
|
|
512
|
+
if (subscriptionId !== request.completionSubscriptionId || advertisedOwners.has(owner)) continue;
|
|
513
|
+
this.#ownerSockets.delete(owner);
|
|
514
|
+
this.#completionSubscriptions.delete(owner);
|
|
515
|
+
await this.#setRecordCompletionCapability(owner, false);
|
|
516
|
+
this.#pendingCompletions.delete(owner);
|
|
517
|
+
}
|
|
518
|
+
for (const owner of activeOwners) {
|
|
519
|
+
this.#completionSubscriptions.set(owner, request.completionSubscriptionId);
|
|
520
|
+
await this.#setRecordCompletionCapability(owner, true);
|
|
521
|
+
const previous = this.#ownerSockets.get(owner);
|
|
522
|
+
this.#ownerSockets.set(owner, {
|
|
523
|
+
socket,
|
|
524
|
+
subscriptionId: request.completionSubscriptionId,
|
|
525
|
+
});
|
|
526
|
+
if (previous?.socket === socket && !replayOwners.has(owner)) continue;
|
|
527
|
+
for (const completion of this.#pendingCompletions.get(owner)?.values() ?? []) {
|
|
528
|
+
socket.write(`${JSON.stringify(completion)}\n`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
for (const owner of detachedOwners) {
|
|
532
|
+
const subscriptionId = this.#completionSubscriptions.get(owner);
|
|
533
|
+
if (this.#completionSubscriptions.has(owner) && subscriptionId !== request.completionSubscriptionId) {
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
this.#completionSubscriptions.set(owner, request.completionSubscriptionId);
|
|
537
|
+
await this.#setRecordCompletionCapability(owner, true);
|
|
538
|
+
const registration = this.#ownerSockets.get(owner);
|
|
539
|
+
if (registration?.subscriptionId === request.completionSubscriptionId) this.#ownerSockets.delete(owner);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
453
542
|
const result = await this.#dispatch(request.operation);
|
|
454
543
|
socket.write(`${JSON.stringify({ id, ok: true, result })}\n`);
|
|
455
544
|
if (request.operation.op === "shutdown") setTimeout(() => void this.shutdown(), 10);
|
|
@@ -520,6 +609,9 @@ class DaemonBroker {
|
|
|
520
609
|
if (existing && !terminalState(existing.snapshot.state)) {
|
|
521
610
|
throw new Error(`Daemon ${spec.name} is already ${existing.snapshot.state}`);
|
|
522
611
|
}
|
|
612
|
+
if (existing && existing.pendingCompletions.length > 0) {
|
|
613
|
+
throw new Error(`Daemon ${spec.name} has unacknowledged completion notifications`);
|
|
614
|
+
}
|
|
523
615
|
if (spec.ready?.log) {
|
|
524
616
|
try {
|
|
525
617
|
new RegExp(spec.ready.log, "u");
|
|
@@ -556,6 +648,9 @@ class DaemonBroker {
|
|
|
556
648
|
readyPattern: spec.ready?.log ? new RegExp(spec.ready.log, "u") : undefined,
|
|
557
649
|
consecutiveFailures: 0,
|
|
558
650
|
persistQueue: Promise.resolve(),
|
|
651
|
+
completionCapable: owner !== undefined && this.#completionSubscriptions.has(owner),
|
|
652
|
+
completionSubscriptionId: owner === undefined ? undefined : this.#completionSubscriptions.get(owner),
|
|
653
|
+
pendingCompletions: [],
|
|
559
654
|
};
|
|
560
655
|
syncReadyPending(record);
|
|
561
656
|
this.#records.set(spec.name, record);
|
|
@@ -785,6 +880,14 @@ class DaemonBroker {
|
|
|
785
880
|
await this.#settle(record, generation);
|
|
786
881
|
}
|
|
787
882
|
|
|
883
|
+
async #monitorRecoveredDetached(record: ManagedDaemon, generation: number): Promise<void> {
|
|
884
|
+
while (!this.#shuttingDown && generation === record.generation && !settledState(record.snapshot.state)) {
|
|
885
|
+
await Bun.sleep(100);
|
|
886
|
+
if (this.#shuttingDown || generation !== record.generation) return;
|
|
887
|
+
await this.#refreshDetached(record);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
788
891
|
async #pollPort(record: ManagedDaemon, generation: number, ready: DaemonReadySpec): Promise<void> {
|
|
789
892
|
const host = ready.host ?? "127.0.0.1";
|
|
790
893
|
const port = ready.port;
|
|
@@ -812,6 +915,15 @@ class DaemonBroker {
|
|
|
812
915
|
return this.#settle(record, generation, result.exitCode, result.timedOut ? "timed out" : undefined);
|
|
813
916
|
}
|
|
814
917
|
|
|
918
|
+
#notifyCompletion(completion: DaemonCompletionNotification): void {
|
|
919
|
+
const pending = this.#pendingCompletions.get(completion.owner) ?? new Map<string, DaemonCompletionNotification>();
|
|
920
|
+
pending.set(completion.completionId, completion);
|
|
921
|
+
this.#pendingCompletions.set(completion.owner, pending);
|
|
922
|
+
const registration = this.#ownerSockets.get(completion.owner);
|
|
923
|
+
if (!registration || registration.socket.destroyed) return;
|
|
924
|
+
registration.socket.write(`${JSON.stringify(completion)}\n`);
|
|
925
|
+
}
|
|
926
|
+
|
|
815
927
|
async #settle(record: ManagedDaemon, generation: number, exitCode?: number, error?: string): Promise<void> {
|
|
816
928
|
// `restarting` is a settled state (child exited, relaunch timer armed). Any op that
|
|
817
929
|
// runs #refreshDetached on such a record must not re-settle it: re-entry double-counts
|
|
@@ -855,9 +967,29 @@ class DaemonBroker {
|
|
|
855
967
|
return;
|
|
856
968
|
}
|
|
857
969
|
record.snapshot.state = failed && !record.stopRequested ? "failed" : "exited";
|
|
970
|
+
const completion =
|
|
971
|
+
record.snapshot.owner !== undefined &&
|
|
972
|
+
!record.stopRequested &&
|
|
973
|
+
this.#completionSubscriptions.has(record.snapshot.owner)
|
|
974
|
+
? ({
|
|
975
|
+
event: "daemon-completed",
|
|
976
|
+
completionId: crypto.randomUUID(),
|
|
977
|
+
owner: record.snapshot.owner,
|
|
978
|
+
daemon: { ...record.snapshot },
|
|
979
|
+
} satisfies DaemonCompletionNotification)
|
|
980
|
+
: undefined;
|
|
981
|
+
if (completion) record.pendingCompletions.push(completion);
|
|
858
982
|
this.#persist(record);
|
|
859
983
|
await record.log?.close();
|
|
860
984
|
record.log = undefined;
|
|
985
|
+
await record.persistQueue;
|
|
986
|
+
if (
|
|
987
|
+
completion &&
|
|
988
|
+
this.#completionSubscriptions.has(completion.owner) &&
|
|
989
|
+
record.pendingCompletions.some(pending => pending.completionId === completion.completionId)
|
|
990
|
+
) {
|
|
991
|
+
this.#notifyCompletion(completion);
|
|
992
|
+
}
|
|
861
993
|
}
|
|
862
994
|
|
|
863
995
|
async #logs(operation: Extract<DaemonOperation, { op: "logs" }>): Promise<DaemonRpcResult> {
|
|
@@ -1027,9 +1159,21 @@ class DaemonBroker {
|
|
|
1027
1159
|
#persist(record: ManagedDaemon): void {
|
|
1028
1160
|
const metaPath = path.join(record.dir, META_FILE);
|
|
1029
1161
|
const tempPath = `${metaPath}.${process.pid}.tmp`;
|
|
1162
|
+
const metadata = {
|
|
1163
|
+
daemon: { ...record.snapshot },
|
|
1164
|
+
spec: record.spec,
|
|
1165
|
+
completionEvents: record.completionCapable,
|
|
1166
|
+
completionSubscriptionId: record.completionSubscriptionId,
|
|
1167
|
+
completionPending: record.pendingCompletions.length > 0,
|
|
1168
|
+
pendingCompletion: record.pendingCompletions.at(-1)?.daemon,
|
|
1169
|
+
pendingCompletions: record.pendingCompletions.map(completion => ({
|
|
1170
|
+
...completion,
|
|
1171
|
+
daemon: { ...completion.daemon },
|
|
1172
|
+
})),
|
|
1173
|
+
};
|
|
1030
1174
|
record.persistQueue = record.persistQueue
|
|
1031
1175
|
.then(async () => {
|
|
1032
|
-
await Bun.write(tempPath, JSON.stringify(
|
|
1176
|
+
await Bun.write(tempPath, JSON.stringify(metadata));
|
|
1033
1177
|
await fs.rename(tempPath, metaPath);
|
|
1034
1178
|
})
|
|
1035
1179
|
.catch(error => {
|
|
@@ -1040,6 +1184,28 @@ class DaemonBroker {
|
|
|
1040
1184
|
});
|
|
1041
1185
|
}
|
|
1042
1186
|
|
|
1187
|
+
async #setRecordCompletionCapability(owner: string, capable: boolean): Promise<void> {
|
|
1188
|
+
const subscriptionId = capable ? this.#completionSubscriptions.get(owner) : undefined;
|
|
1189
|
+
const persistence: Promise<void>[] = [];
|
|
1190
|
+
for (const record of this.#records.values()) {
|
|
1191
|
+
const clearPendingCompletions = !capable && record.pendingCompletions.length > 0;
|
|
1192
|
+
if (
|
|
1193
|
+
record.snapshot.owner !== owner ||
|
|
1194
|
+
(record.completionCapable === capable &&
|
|
1195
|
+
record.completionSubscriptionId === subscriptionId &&
|
|
1196
|
+
!clearPendingCompletions)
|
|
1197
|
+
) {
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
record.completionCapable = capable;
|
|
1201
|
+
record.completionSubscriptionId = subscriptionId;
|
|
1202
|
+
if (!capable) record.pendingCompletions = [];
|
|
1203
|
+
this.#persist(record);
|
|
1204
|
+
persistence.push(record.persistQueue);
|
|
1205
|
+
}
|
|
1206
|
+
await Promise.all(persistence);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1043
1209
|
async #recoverRecords(): Promise<void> {
|
|
1044
1210
|
const root = path.join(this.#runtimeDir, "daemons");
|
|
1045
1211
|
const entries = await fs.readdir(root, { withFileTypes: true }).catch(error => {
|
|
@@ -1057,11 +1223,9 @@ class DaemonBroker {
|
|
|
1057
1223
|
const snapshot = parseDaemonSnapshot(decoded.daemon);
|
|
1058
1224
|
const spec = parseDaemonSpec(decoded.spec);
|
|
1059
1225
|
const processRef = snapshot.pid === undefined ? null : Process.fromPid(snapshot.pid);
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
snapshot.state !== "stopping" &&
|
|
1064
|
-
processRef?.status() === "running";
|
|
1226
|
+
const recoverableExit = !terminalState(snapshot.state) && snapshot.state !== "stopping";
|
|
1227
|
+
const detached = spec.detached && recoverableExit && processRef?.status() === "running";
|
|
1228
|
+
const recoveredDead = recoverableExit && !detached;
|
|
1065
1229
|
if (!detached) {
|
|
1066
1230
|
// Reap only records that were still alive when the previous broker
|
|
1067
1231
|
// exited; already-terminal records keep their real exit time so
|
|
@@ -1088,12 +1252,64 @@ class DaemonBroker {
|
|
|
1088
1252
|
readyPattern: spec.ready?.log ? new RegExp(spec.ready.log, "u") : undefined,
|
|
1089
1253
|
consecutiveFailures: 0,
|
|
1090
1254
|
persistQueue: Promise.resolve(),
|
|
1255
|
+
completionCapable: "completionEvents" in decoded && decoded.completionEvents === true,
|
|
1256
|
+
completionSubscriptionId:
|
|
1257
|
+
"completionSubscriptionId" in decoded && typeof decoded.completionSubscriptionId === "string"
|
|
1258
|
+
? decoded.completionSubscriptionId
|
|
1259
|
+
: undefined,
|
|
1260
|
+
pendingCompletions: (() => {
|
|
1261
|
+
if ("pendingCompletions" in decoded && Array.isArray(decoded.pendingCompletions)) {
|
|
1262
|
+
return decoded.pendingCompletions.map(value => {
|
|
1263
|
+
const message = parseDaemonWireMessage(value);
|
|
1264
|
+
if (!("event" in message)) throw new Error("Pending daemon completion is not an event");
|
|
1265
|
+
return message;
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
const pendingSnapshot =
|
|
1269
|
+
"pendingCompletion" in decoded
|
|
1270
|
+
? parseDaemonSnapshot(decoded.pendingCompletion)
|
|
1271
|
+
: "completionPending" in decoded && decoded.completionPending === true
|
|
1272
|
+
? { ...snapshot }
|
|
1273
|
+
: undefined;
|
|
1274
|
+
return pendingSnapshot
|
|
1275
|
+
? [
|
|
1276
|
+
{
|
|
1277
|
+
event: "daemon-completed",
|
|
1278
|
+
completionId: crypto.randomUUID(),
|
|
1279
|
+
owner: pendingSnapshot.owner ?? snapshot.owner ?? "",
|
|
1280
|
+
daemon: pendingSnapshot,
|
|
1281
|
+
},
|
|
1282
|
+
]
|
|
1283
|
+
: [];
|
|
1284
|
+
})(),
|
|
1091
1285
|
};
|
|
1286
|
+
if (recoveredDead && record.completionCapable && snapshot.owner && record.pendingCompletions.length === 0) {
|
|
1287
|
+
record.pendingCompletions.push({
|
|
1288
|
+
event: "daemon-completed",
|
|
1289
|
+
completionId: crypto.randomUUID(),
|
|
1290
|
+
owner: snapshot.owner,
|
|
1291
|
+
daemon: { ...snapshot },
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1092
1294
|
syncReadyPending(record);
|
|
1093
1295
|
this.#records.set(snapshot.name, record);
|
|
1296
|
+
if (snapshot.owner && record.completionCapable && (detached || record.pendingCompletions.length > 0)) {
|
|
1297
|
+
this.#completionSubscriptions.set(snapshot.owner, record.completionSubscriptionId);
|
|
1298
|
+
}
|
|
1299
|
+
if (record.completionCapable) {
|
|
1300
|
+
for (const completion of record.pendingCompletions) this.#notifyCompletion(completion);
|
|
1301
|
+
}
|
|
1094
1302
|
if (detached && spec.ready?.port !== undefined && snapshot.state !== "ready") {
|
|
1095
1303
|
void this.#pollPort(record, record.generation, spec.ready);
|
|
1096
1304
|
}
|
|
1305
|
+
if (detached) {
|
|
1306
|
+
void this.#monitorRecoveredDetached(record, record.generation).catch(error => {
|
|
1307
|
+
logger.warn("Failed to monitor recovered detached daemon", {
|
|
1308
|
+
name: record.snapshot.name,
|
|
1309
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1310
|
+
});
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1097
1313
|
this.#persist(record);
|
|
1098
1314
|
} catch (error) {
|
|
1099
1315
|
logger.warn("Failed to recover daemon record", {
|
package/src/launch/client.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs/promises";
|
|
|
2
2
|
import * as net from "node:net";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
-
import { getGlobalDaemonRuntimeDir, isEexist, isEisdir, isEnoent, postmortem } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { getGlobalDaemonRuntimeDir, isEexist, isEisdir, isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils";
|
|
6
6
|
import { hostHasInheritableConsole } from "../eval/py/spawn-options";
|
|
7
7
|
import { resolveWorkerSpawnCmd, workerEnvFromParent } from "../subprocess/worker-client";
|
|
8
8
|
import { daemonBrokerEndpoint, daemonRuntimeDir } from "./paths";
|
|
@@ -11,11 +11,12 @@ import {
|
|
|
11
11
|
DAEMON_IDLE_GRACE_ENV,
|
|
12
12
|
DAEMON_PROJECT_DIR_ENV,
|
|
13
13
|
DAEMON_RUNTIME_DIR_ENV,
|
|
14
|
+
type DaemonCompletionNotification,
|
|
14
15
|
type DaemonOperation,
|
|
15
16
|
type DaemonRpcResult,
|
|
16
|
-
type
|
|
17
|
+
type DaemonWireMessage,
|
|
17
18
|
parseDaemonRpcResult,
|
|
18
|
-
|
|
19
|
+
parseDaemonWireMessage,
|
|
19
20
|
} from "./protocol";
|
|
20
21
|
import { resolveDaemonSpawnOptions } from "./spawn-options";
|
|
21
22
|
|
|
@@ -43,14 +44,26 @@ export interface DaemonBrokerClientOptions {
|
|
|
43
44
|
idleGraceMs?: number;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
export interface DaemonCompletionUnregisterOptions {
|
|
48
|
+
/** Detach this process without deleting broker-persisted pending notifications. */
|
|
49
|
+
preservePending?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
46
52
|
/** Persistent per-process connection to one project or global daemon broker. */
|
|
47
53
|
export interface DaemonBrokerClient {
|
|
54
|
+
onCompletion(
|
|
55
|
+
owner: string,
|
|
56
|
+
sink: (notification: DaemonCompletionNotification) => Promise<void> | void,
|
|
57
|
+
): (options?: DaemonCompletionUnregisterOptions) => void;
|
|
48
58
|
/** Canonical project directory or synthetic directory identifying a global scope. */
|
|
49
59
|
readonly projectDir: string;
|
|
50
60
|
request(operation: DaemonOperation, signal?: AbortSignal): Promise<DaemonRpcResult>;
|
|
51
61
|
close(): void;
|
|
52
62
|
}
|
|
53
63
|
|
|
64
|
+
/** A request reached the broker and the broker rejected the operation. */
|
|
65
|
+
export class DaemonBrokerRejectedError extends Error {}
|
|
66
|
+
|
|
54
67
|
async function canonicalProjectDir(projectDir: string): Promise<string> {
|
|
55
68
|
const resolved = path.resolve(projectDir);
|
|
56
69
|
try {
|
|
@@ -134,12 +147,20 @@ class SocketDaemonClient implements DaemonBrokerClient {
|
|
|
134
147
|
readonly #runtimeDir: string;
|
|
135
148
|
readonly #endpoint: string;
|
|
136
149
|
readonly #token: string;
|
|
150
|
+
readonly #seenCompletionIds = new Set<string>();
|
|
137
151
|
readonly #idleGraceMs: number | undefined;
|
|
138
152
|
readonly #pending = new Map<string, PendingRequest>();
|
|
153
|
+
readonly #completionSinks = new Map<string, (notification: DaemonCompletionNotification) => Promise<void> | void>();
|
|
154
|
+
readonly #completionUnsubscribes = new Set<string>();
|
|
155
|
+
readonly #preservedCompletionOwners = new Set<string>();
|
|
156
|
+
readonly #completionReplays = new Set<string>();
|
|
157
|
+
readonly #inFlightCompletionIds = new Set<string>();
|
|
158
|
+
readonly #completionSubscriptionId = crypto.randomUUID();
|
|
139
159
|
#socket: net.Socket | undefined;
|
|
140
160
|
#connectPromise: Promise<void> | undefined;
|
|
141
161
|
#buffer = "";
|
|
142
162
|
#closed = false;
|
|
163
|
+
#completionReconnectTimer: NodeJS.Timeout | undefined;
|
|
143
164
|
|
|
144
165
|
constructor(projectDir: string, runtimeDir: string, token: string, options: DaemonBrokerClientOptions) {
|
|
145
166
|
this.projectDir = projectDir;
|
|
@@ -156,6 +177,8 @@ class SocketDaemonClient implements DaemonBrokerClient {
|
|
|
156
177
|
const socket = this.#socket;
|
|
157
178
|
if (!socket || socket.destroyed) throw new Error("Daemon broker socket is unavailable");
|
|
158
179
|
|
|
180
|
+
const completionUnsubscribes = [...this.#completionUnsubscribes];
|
|
181
|
+
const completionReplays = [...this.#completionReplays];
|
|
159
182
|
const id = crypto.randomUUID();
|
|
160
183
|
const { promise, resolve, reject } = Promise.withResolvers<DaemonRpcResult>();
|
|
161
184
|
const timer = setTimeout(() => {
|
|
@@ -176,18 +199,88 @@ class SocketDaemonClient implements DaemonBrokerClient {
|
|
|
176
199
|
pending.removeAbort = () => signal.removeEventListener("abort", abort);
|
|
177
200
|
}
|
|
178
201
|
this.#pending.set(id, pending);
|
|
179
|
-
socket.write(
|
|
180
|
-
|
|
202
|
+
socket.write(
|
|
203
|
+
`${JSON.stringify({
|
|
204
|
+
id,
|
|
205
|
+
token: this.#token,
|
|
206
|
+
owners: [...this.#completionSinks.keys()],
|
|
207
|
+
detachedOwners: [...this.#preservedCompletionOwners],
|
|
208
|
+
completionEvents: true,
|
|
209
|
+
completionUnsubscribes,
|
|
210
|
+
completionReplays,
|
|
211
|
+
completionSubscriptionId: this.#completionSubscriptionId,
|
|
212
|
+
operation,
|
|
213
|
+
})}\n`,
|
|
214
|
+
);
|
|
215
|
+
const result = await promise;
|
|
216
|
+
for (const owner of completionUnsubscribes) {
|
|
217
|
+
if (!this.#completionSinks.has(owner)) this.#completionUnsubscribes.delete(owner);
|
|
218
|
+
}
|
|
219
|
+
for (const owner of completionReplays) {
|
|
220
|
+
if (this.#completionSinks.has(owner)) this.#completionReplays.delete(owner);
|
|
221
|
+
}
|
|
222
|
+
return result;
|
|
181
223
|
}
|
|
182
224
|
|
|
183
225
|
close(): void {
|
|
184
226
|
if (this.#closed) return;
|
|
185
227
|
this.#closed = true;
|
|
228
|
+
clearTimeout(this.#completionReconnectTimer);
|
|
229
|
+
this.#completionReconnectTimer = undefined;
|
|
186
230
|
this.#socket?.destroy();
|
|
231
|
+
this.#completionSinks.clear();
|
|
232
|
+
this.#preservedCompletionOwners.clear();
|
|
233
|
+
this.#completionReplays.clear();
|
|
187
234
|
this.#socket = undefined;
|
|
188
235
|
this.#rejectPending(new Error("Daemon broker client closed"));
|
|
189
236
|
}
|
|
190
237
|
|
|
238
|
+
onCompletion(
|
|
239
|
+
owner: string,
|
|
240
|
+
sink: (notification: DaemonCompletionNotification) => Promise<void> | void,
|
|
241
|
+
): (options?: DaemonCompletionUnregisterOptions) => void {
|
|
242
|
+
this.#completionUnsubscribes.delete(owner);
|
|
243
|
+
if (this.#preservedCompletionOwners.delete(owner)) this.#completionReplays.add(owner);
|
|
244
|
+
this.#completionSinks.set(owner, sink);
|
|
245
|
+
this.#publishCompletionOwners();
|
|
246
|
+
return options => {
|
|
247
|
+
if (this.#completionSinks.get(owner) !== sink) return;
|
|
248
|
+
this.#completionSinks.delete(owner);
|
|
249
|
+
if (options?.preservePending) {
|
|
250
|
+
this.#preservedCompletionOwners.add(owner);
|
|
251
|
+
} else {
|
|
252
|
+
this.#preservedCompletionOwners.delete(owner);
|
|
253
|
+
this.#completionUnsubscribes.add(owner);
|
|
254
|
+
}
|
|
255
|
+
if (this.#completionSinks.size === 0 && this.#completionReconnectTimer) {
|
|
256
|
+
clearTimeout(this.#completionReconnectTimer);
|
|
257
|
+
this.#completionReconnectTimer = undefined;
|
|
258
|
+
}
|
|
259
|
+
this.#publishCompletionOwners();
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
#publishCompletionOwners(): void {
|
|
264
|
+
if (this.#closed) return;
|
|
265
|
+
void this.request({ op: "ping" }).catch(() => this.#scheduleCompletionReconnect());
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
#scheduleCompletionReconnect(): void {
|
|
269
|
+
if (
|
|
270
|
+
this.#closed ||
|
|
271
|
+
this.#completionSinks.size === 0 ||
|
|
272
|
+
this.#completionReconnectTimer !== undefined ||
|
|
273
|
+
(this.#socket !== undefined && !this.#socket.destroyed)
|
|
274
|
+
) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
this.#completionReconnectTimer = setTimeout(() => {
|
|
278
|
+
this.#completionReconnectTimer = undefined;
|
|
279
|
+
this.#publishCompletionOwners();
|
|
280
|
+
}, CONNECT_RETRY_MS);
|
|
281
|
+
this.#completionReconnectTimer.unref();
|
|
282
|
+
}
|
|
283
|
+
|
|
191
284
|
async #connect(): Promise<void> {
|
|
192
285
|
if (this.#socket && !this.#socket.destroyed) return;
|
|
193
286
|
if (this.#connectPromise) return this.#connectPromise;
|
|
@@ -251,6 +344,7 @@ class SocketDaemonClient implements DaemonBrokerClient {
|
|
|
251
344
|
socket.on("close", () => {
|
|
252
345
|
if (this.#socket === socket) this.#socket = undefined;
|
|
253
346
|
this.#rejectPending(new Error("Daemon broker connection closed"));
|
|
347
|
+
this.#scheduleCompletionReconnect();
|
|
254
348
|
});
|
|
255
349
|
}
|
|
256
350
|
|
|
@@ -262,21 +356,32 @@ class SocketDaemonClient implements DaemonBrokerClient {
|
|
|
262
356
|
const line = this.#buffer.slice(0, newline);
|
|
263
357
|
this.#buffer = this.#buffer.slice(newline + 1);
|
|
264
358
|
if (line.length === 0) continue;
|
|
265
|
-
let
|
|
359
|
+
let decoded: unknown;
|
|
360
|
+
try {
|
|
361
|
+
decoded = JSON.parse(line);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
this.#rejectPending(error instanceof Error ? error : new Error(String(error)));
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
let message: DaemonWireMessage;
|
|
266
367
|
try {
|
|
267
|
-
|
|
268
|
-
response = parseDaemonWireResponse(decoded);
|
|
368
|
+
message = parseDaemonWireMessage(decoded);
|
|
269
369
|
} catch (error) {
|
|
270
370
|
this.#rejectPending(error instanceof Error ? error : new Error(String(error)));
|
|
271
371
|
continue;
|
|
272
372
|
}
|
|
373
|
+
if ("event" in message) {
|
|
374
|
+
void this.#deliverCompletion(message);
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const response = message;
|
|
273
378
|
const pending = this.#pending.get(response.id);
|
|
274
379
|
if (!pending) continue;
|
|
275
380
|
this.#pending.delete(response.id);
|
|
276
381
|
clearTimeout(pending.timer);
|
|
277
382
|
pending.removeAbort?.();
|
|
278
383
|
if (!response.ok) {
|
|
279
|
-
pending.reject(new
|
|
384
|
+
pending.reject(new DaemonBrokerRejectedError(response.error));
|
|
280
385
|
continue;
|
|
281
386
|
}
|
|
282
387
|
try {
|
|
@@ -287,6 +392,53 @@ class SocketDaemonClient implements DaemonBrokerClient {
|
|
|
287
392
|
}
|
|
288
393
|
}
|
|
289
394
|
|
|
395
|
+
async #deliverCompletion(message: DaemonCompletionNotification): Promise<void> {
|
|
396
|
+
if (this.#seenCompletionIds.has(message.completionId)) {
|
|
397
|
+
this.#ackCompletion(message.completionId);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (this.#inFlightCompletionIds.has(message.completionId)) return;
|
|
401
|
+
const sink = this.#completionSinks.get(message.owner);
|
|
402
|
+
if (!sink) return;
|
|
403
|
+
this.#inFlightCompletionIds.add(message.completionId);
|
|
404
|
+
try {
|
|
405
|
+
await sink(message);
|
|
406
|
+
if (this.#seenCompletionIds.size >= 512) {
|
|
407
|
+
const oldest = this.#seenCompletionIds.values().next().value;
|
|
408
|
+
if (oldest !== undefined) this.#seenCompletionIds.delete(oldest);
|
|
409
|
+
}
|
|
410
|
+
this.#seenCompletionIds.add(message.completionId);
|
|
411
|
+
this.#ackCompletion(message.completionId);
|
|
412
|
+
} catch (error) {
|
|
413
|
+
logger.warn("Daemon completion sink failed", {
|
|
414
|
+
owner: message.owner,
|
|
415
|
+
completionId: message.completionId,
|
|
416
|
+
error: error instanceof Error ? error.message : String(error),
|
|
417
|
+
});
|
|
418
|
+
this.#socket?.destroy();
|
|
419
|
+
} finally {
|
|
420
|
+
this.#inFlightCompletionIds.delete(message.completionId);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
#ackCompletion(completionId: string): void {
|
|
425
|
+
const socket = this.#socket;
|
|
426
|
+
if (!socket || socket.destroyed) return;
|
|
427
|
+
socket.write(
|
|
428
|
+
`${JSON.stringify({
|
|
429
|
+
id: crypto.randomUUID(),
|
|
430
|
+
token: this.#token,
|
|
431
|
+
owners: [...this.#completionSinks.keys()],
|
|
432
|
+
detachedOwners: [...this.#preservedCompletionOwners],
|
|
433
|
+
completionEvents: true,
|
|
434
|
+
completionAcks: [completionId],
|
|
435
|
+
completionUnsubscribes: [...this.#completionUnsubscribes],
|
|
436
|
+
completionSubscriptionId: this.#completionSubscriptionId,
|
|
437
|
+
operation: { op: "ping" },
|
|
438
|
+
})}\n`,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
290
442
|
#rejectPending(error: Error): void {
|
|
291
443
|
for (const pending of this.#pending.values()) {
|
|
292
444
|
clearTimeout(pending.timer);
|