@meistrari/agent-core 0.2.0-experimental.0 → 0.2.0-experimental.2
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/README.md +82 -0
- package/docs/consumer-capability-matrix.md +374 -0
- package/package.json +17 -10
- package/scripts/generate-codex-protocol.ts +711 -0
- package/src/agents/agent-operation.ts +8 -0
- package/src/agents/agent-provider.ts +3 -0
- package/src/agents/agent-run.ts +14 -2
- package/src/agents/agent-runtime-error.ts +28 -0
- package/src/agents/agent-session-events.ts +33 -0
- package/src/agents/agent-tool-runner.ts +12 -11
- package/src/agents/agent-tool.ts +12 -4
- package/src/agents/author-context.ts +1 -1
- package/src/agents/claude/claude-auth.ts +40 -0
- package/src/agents/claude/claude-browser-turn-metadata.ts +70 -0
- package/src/agents/claude/claude-code-process.ts +98 -0
- package/src/agents/claude/claude-command-mapper.ts +63 -3
- package/src/agents/claude/claude-desktop.ts +55 -0
- package/src/agents/claude/claude-event-mapper.ts +876 -124
- package/src/agents/claude/claude-file-change.ts +128 -0
- package/src/agents/claude/claude-provider.ts +344 -29
- package/src/agents/claude/claude-run.ts +922 -91
- package/src/agents/claude/claude-selected-repository-instructions.ts +91 -0
- package/src/agents/claude/claude-skill-catalog.ts +75 -0
- package/src/agents/claude/claude-steered-notifications.ts +180 -0
- package/src/agents/claude/claude-tool-mapper.ts +81 -2
- package/src/agents/claude/claude-workspace-skills.ts +87 -0
- package/src/agents/claude/index.ts +2 -0
- package/src/agents/codex/codex-auth-mapper.ts +27 -0
- package/src/agents/codex/codex-auth.ts +19 -0
- package/src/agents/codex/codex-browser-auth-broker.ts +320 -0
- package/src/agents/codex/codex-browser-auth-status.ts +55 -0
- package/src/agents/codex/codex-command-mapper.ts +38 -3
- package/src/agents/codex/codex-event-mapper.ts +375 -64
- package/src/agents/codex/codex-json-rpc-client.ts +66 -21
- package/src/agents/codex/codex-provider.ts +118 -236
- package/src/agents/codex/codex-run-auth.ts +113 -0
- package/src/agents/codex/codex-run.ts +428 -48
- package/src/agents/codex/codex-tool-mapper.ts +2 -2
- package/src/agents/codex/codex.errors.ts +8 -0
- package/src/agents/codex/generated/meta.gen.ts +144 -6
- package/src/agents/codex/generated/namespaces.gen.ts +92 -3
- package/src/agents/codex/generated/schema.gen.ts +24698 -15638
- package/src/agents/codex/lazy-codex-run-auth.ts +47 -0
- package/src/agents/index.ts +5 -0
- package/src/agents/input-attachment-preparation.ts +13 -0
- package/src/agents/mcp-tool-content.ts +82 -0
- package/src/agents/model-rebinding-run.ts +14 -3
- package/src/agents/selected-repository-instructions.ts +202 -0
- package/src/agents/session-context.ts +41 -0
- package/src/agents/subscription-token-refresh.ts +73 -0
- package/src/agents/user-input-request.ts +19 -20
- package/src/errors/index.ts +1 -1
- package/src/errors/validation-error.ts +6 -0
- package/src/protocol/agent-catalog.ts +159 -0
- package/src/protocol/agent-command.ts +54 -7
- package/src/protocol/agent-content.ts +36 -16
- package/src/protocol/agent-event.ts +210 -73
- package/src/protocol/agent-identity.ts +29 -0
- package/src/protocol/agent-json.ts +122 -8
- package/src/protocol/agent-metadata.ts +2 -2
- package/src/protocol/agent-model.ts +13 -1
- package/src/protocol/agent-notification.ts +110 -0
- package/src/protocol/agent-presentation.ts +71 -20
- package/src/protocol/agent-tool.ts +66 -10
- package/src/protocol/agent-usage.ts +4 -1
- package/src/protocol/agent-user-input.ts +4 -4
- package/src/protocol/agent-work.ts +12 -10
- package/src/protocol/index.ts +2 -0
- package/src/protocol/rejected-keys.ts +11 -0
- package/src/provenance.gen.ts +3 -3
- package/src/steps/contract.ts +4 -1
- package/src/steps/projector.ts +17 -1
- package/src/supervisor/agent-provider-factory.ts +60 -5
- package/src/supervisor/bootstrap-binder.ts +3 -2
- package/src/supervisor/index.ts +1 -1
- package/src/supervisor/persistence/supervisor-store.ts +22 -8
- package/src/supervisor/runtime-handler.ts +30 -2
- package/src/supervisor-protocol/agent-event-wrapper.ts +30 -0
- package/src/supervisor-protocol/bootstrap.ts +4 -1
- package/src/supervisor-protocol/command-body.ts +4 -1
- package/src/supervisor-protocol/envelopes/supervisor-to-control-plane.ts +1 -0
- package/src/testing/in-memory-runtime-control-plane.ts +118 -12
- package/src/testing/index.ts +1 -1
- package/src/worker-runtime-client/command-pump.ts +85 -10
- package/src/worker-runtime-client/connection-attempt.ts +95 -20
- package/src/worker-runtime-client/frame-processor.ts +242 -58
- package/src/worker-runtime-client/index.ts +8 -2
- package/src/worker-runtime-client/ports.ts +34 -2
- package/src/worker-runtime-client/rpc-request-manager.ts +152 -107
- package/src/worker-runtime-client/sandbox-connection-runtime.ts +72 -35
package/src/testing/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { createTestControlAuthority } from './es256-test-keys'
|
|
2
2
|
export type { TestControlAuthority } from './es256-test-keys'
|
|
3
|
-
export { InMemoryRuntimeControlPlane } from './in-memory-runtime-control-plane'
|
|
3
|
+
export { inMemoryJournalIdentityChangedCode, InMemoryRuntimeControlPlane } from './in-memory-runtime-control-plane'
|
|
4
4
|
export { createLoopbackSupervisorConnectionOpener } from './loopback-supervisor-connection'
|
|
5
5
|
export { createScriptedProvider } from './scripted-provider'
|
|
6
6
|
export type { ScriptedCommandContext } from './scripted-provider'
|
|
@@ -3,11 +3,19 @@ import type { CommandAckEnvelope } from '../supervisor-protocol/envelopes/superv
|
|
|
3
3
|
import type { CommandSource, ConnectionAuthorityStore, SupervisorConnection } from './ports'
|
|
4
4
|
import { encodeWireFrame } from '../supervisor-protocol/wire-codec'
|
|
5
5
|
|
|
6
|
+
export class CommandAckTimeoutError extends Error {
|
|
7
|
+
constructor() {
|
|
8
|
+
super('agent-core.command-ack-timeout')
|
|
9
|
+
this.name = 'CommandAckTimeoutError'
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
export class SessionCommandPump {
|
|
7
14
|
private afterSequence: number
|
|
8
15
|
private running = false
|
|
9
16
|
private stopped = false
|
|
10
17
|
private wakePending = false
|
|
18
|
+
private readonly idleWaiters = new Set<() => void>()
|
|
11
19
|
private pending: {
|
|
12
20
|
commandId: string
|
|
13
21
|
commandSeq: number
|
|
@@ -19,13 +27,20 @@ export class SessionCommandPump {
|
|
|
19
27
|
sessionSandboxId: string
|
|
20
28
|
runtimeConnectionId: string
|
|
21
29
|
afterSequence: number
|
|
30
|
+
individualReceipts?: boolean
|
|
31
|
+
ackTimeoutMs?: number
|
|
32
|
+
onFailure?: (error: unknown) => void
|
|
22
33
|
commands: CommandSource
|
|
23
|
-
authorityStore
|
|
34
|
+
authorityStore?: ConnectionAuthorityStore
|
|
24
35
|
connection: SupervisorConnection
|
|
25
36
|
logger: Logger
|
|
26
37
|
onRejected: (ack: Extract<CommandAckEnvelope, { status: 'rejected' }>) => Promise<void>
|
|
27
38
|
}) {
|
|
28
39
|
this.afterSequence = dependencies.afterSequence
|
|
40
|
+
if (dependencies.ackTimeoutMs !== undefined && (!Number.isSafeInteger(dependencies.ackTimeoutMs) || dependencies.ackTimeoutMs < 1))
|
|
41
|
+
throw new RangeError('ackTimeoutMs must be a positive safe integer')
|
|
42
|
+
if (dependencies.individualReceipts && (!dependencies.commands.pending || !dependencies.commands.received || !dependencies.commands.rejected))
|
|
43
|
+
throw new Error('wss-v1 requires individual command selection, receipt persistence and rejection persistence')
|
|
29
44
|
}
|
|
30
45
|
|
|
31
46
|
start(): void {
|
|
@@ -43,7 +58,7 @@ export class SessionCommandPump {
|
|
|
43
58
|
acknowledge(ack: CommandAckEnvelope): void {
|
|
44
59
|
const pending = this.pending
|
|
45
60
|
if (!pending) {
|
|
46
|
-
if (ack.status === 'received' && ack.commandSeq <= this.afterSequence)
|
|
61
|
+
if (ack.status === 'received' && (this.dependencies.individualReceipts || ack.commandSeq <= this.afterSequence))
|
|
47
62
|
return
|
|
48
63
|
this.dependencies.logger.warn({ commandSeq: ack.commandSeq }, 'closing after an unsolicited command acknowledgement')
|
|
49
64
|
this.dependencies.connection.close(1008, 'Command ACK has no pending command.')
|
|
@@ -75,6 +90,13 @@ export class SessionCommandPump {
|
|
|
75
90
|
return this.afterSequence
|
|
76
91
|
}
|
|
77
92
|
|
|
93
|
+
/** Stop dispatch and wait for any in-flight selection/send/receipt to settle. */
|
|
94
|
+
async close(): Promise<void> {
|
|
95
|
+
this.stop()
|
|
96
|
+
if (this.running)
|
|
97
|
+
await new Promise<void>(resolve => this.idleWaiters.add(resolve))
|
|
98
|
+
}
|
|
99
|
+
|
|
78
100
|
private async run(): Promise<void> {
|
|
79
101
|
if (this.running)
|
|
80
102
|
return
|
|
@@ -83,26 +105,58 @@ export class SessionCommandPump {
|
|
|
83
105
|
while (!this.stopped && this.wakePending) {
|
|
84
106
|
this.wakePending = false
|
|
85
107
|
while (!this.stopped) {
|
|
86
|
-
const command =
|
|
87
|
-
sessionId: this.dependencies.sessionId,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
108
|
+
const command = this.dependencies.individualReceipts
|
|
109
|
+
? await this.dependencies.commands.pending!({ sessionId: this.dependencies.sessionId, runtimeConnectionId: this.dependencies.runtimeConnectionId })
|
|
110
|
+
: await this.dependencies.commands.next({
|
|
111
|
+
sessionId: this.dependencies.sessionId,
|
|
112
|
+
runtimeConnectionId: this.dependencies.runtimeConnectionId,
|
|
113
|
+
afterSequence: this.afterSequence,
|
|
114
|
+
})
|
|
115
|
+
if (this.stopped)
|
|
116
|
+
return
|
|
91
117
|
if (!command)
|
|
92
118
|
break
|
|
93
119
|
const ack = await this.sendAndWait(command)
|
|
94
120
|
if (this.stopped)
|
|
95
121
|
return
|
|
96
122
|
if (ack.status === 'rejected') {
|
|
123
|
+
if (!this.dependencies.individualReceipts) {
|
|
124
|
+
await this.dependencies.onRejected(ack)
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
// Terminal for this command only: persist it so `pending` never offers it again,
|
|
128
|
+
// then keep delivering the rest of the session's work.
|
|
129
|
+
await this.dependencies.commands.rejected!({
|
|
130
|
+
sessionId: this.dependencies.sessionId,
|
|
131
|
+
runtimeConnectionId: this.dependencies.runtimeConnectionId,
|
|
132
|
+
commandId: ack.commandId,
|
|
133
|
+
commandSeq: ack.commandSeq,
|
|
134
|
+
errorCode: ack.errorCode,
|
|
135
|
+
detail: ack.detail,
|
|
136
|
+
})
|
|
97
137
|
await this.dependencies.onRejected(ack)
|
|
98
|
-
|
|
138
|
+
continue
|
|
99
139
|
}
|
|
100
|
-
this.
|
|
140
|
+
if (this.dependencies.individualReceipts) {
|
|
141
|
+
await this.dependencies.commands.received!({
|
|
142
|
+
sessionId: this.dependencies.sessionId,
|
|
143
|
+
runtimeConnectionId: this.dependencies.runtimeConnectionId,
|
|
144
|
+
commandId: ack.commandId,
|
|
145
|
+
commandSeq: ack.commandSeq,
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
this.afterSequence = Math.max(this.afterSequence, ack.commandSeq)
|
|
101
149
|
}
|
|
102
150
|
}
|
|
103
151
|
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
this.fail(error)
|
|
154
|
+
}
|
|
104
155
|
finally {
|
|
105
156
|
this.running = false
|
|
157
|
+
for (const resolve of this.idleWaiters)
|
|
158
|
+
resolve()
|
|
159
|
+
this.idleWaiters.clear()
|
|
106
160
|
if (!this.stopped && this.wakePending)
|
|
107
161
|
void this.run()
|
|
108
162
|
}
|
|
@@ -110,18 +164,39 @@ export class SessionCommandPump {
|
|
|
110
164
|
|
|
111
165
|
private async sendAndWait(command: NonNullable<Awaited<ReturnType<CommandSource['next']>>>): Promise<CommandAckEnvelope> {
|
|
112
166
|
const waiter = Promise.withResolvers<CommandAckEnvelope>()
|
|
167
|
+
const timeout = this.dependencies.ackTimeoutMs === undefined ? undefined : setTimeout(() => this.fail(new CommandAckTimeoutError()), this.dependencies.ackTimeoutMs)
|
|
113
168
|
this.pending = {
|
|
114
169
|
commandId: command.commandId,
|
|
115
170
|
commandSeq: command.commandSeq,
|
|
116
|
-
settle:
|
|
171
|
+
settle: (ack) => {
|
|
172
|
+
clearTimeout(timeout)
|
|
173
|
+
waiter.resolve(ack)
|
|
174
|
+
},
|
|
117
175
|
}
|
|
118
176
|
try {
|
|
119
177
|
await this.dependencies.connection.send(encodeWireFrame(command))
|
|
120
178
|
return await waiter.promise
|
|
121
179
|
}
|
|
122
180
|
finally {
|
|
181
|
+
clearTimeout(timeout)
|
|
123
182
|
if (this.pending?.commandId === command.commandId)
|
|
124
183
|
this.pending = undefined
|
|
125
184
|
}
|
|
126
185
|
}
|
|
186
|
+
|
|
187
|
+
private fail(error: unknown): void {
|
|
188
|
+
if (this.stopped)
|
|
189
|
+
return
|
|
190
|
+
this.dependencies.logger.warn({ err: error }, 'command delivery failed; reconnect and reconcile durable attempts')
|
|
191
|
+
try {
|
|
192
|
+
this.dependencies.onFailure?.(error)
|
|
193
|
+
}
|
|
194
|
+
catch (callbackError) {
|
|
195
|
+
this.dependencies.logger.warn({ err: callbackError }, 'command failure callback failed')
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
this.stop()
|
|
199
|
+
this.dependencies.connection.close(1011, 'Command delivery failed.')
|
|
200
|
+
}
|
|
201
|
+
}
|
|
127
202
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Logger } from '../logger'
|
|
2
|
+
import type { SessionBootstrapBody } from '../supervisor-protocol/bootstrap'
|
|
3
|
+
import type { SessionBootstrapEnvelope } from '../supervisor-protocol/envelopes/control-plane-to-supervisor'
|
|
2
4
|
import type { CommandAckEnvelope } from '../supervisor-protocol/envelopes/supervisor-to-control-plane'
|
|
3
5
|
import type {
|
|
4
6
|
BootstrapProvider,
|
|
@@ -16,6 +18,7 @@ import type {
|
|
|
16
18
|
} from './ports'
|
|
17
19
|
import { randomUUID } from 'node:crypto'
|
|
18
20
|
import { wireCommandBodySchema } from '../supervisor-protocol/command-body'
|
|
21
|
+
import { sessionBootstrapEnvelopeSchema } from '../supervisor-protocol/envelopes/control-plane-to-supervisor'
|
|
19
22
|
import { encodeWireFrame } from '../supervisor-protocol/wire-codec'
|
|
20
23
|
import { SessionCommandPump } from './command-pump'
|
|
21
24
|
import { BootstrapRejectedAckError, SupervisorFrameProcessor } from './frame-processor'
|
|
@@ -85,7 +88,7 @@ export class SessionSandboxConnectionAttempt {
|
|
|
85
88
|
const queuedFrames: Array<string | ArrayBuffer | Uint8Array> = []
|
|
86
89
|
const closed = Promise.withResolvers<{ code: number, reason: string }>()
|
|
87
90
|
const authorityAssertion = await authorityAssertionPromise
|
|
88
|
-
const connection = await
|
|
91
|
+
const connection = await openSupervisorConnectionBeforeDeadline(this.dependencies.openSupervisorConnection, {
|
|
89
92
|
providerSandboxId: claim.providerSandboxId,
|
|
90
93
|
trafficAccessToken,
|
|
91
94
|
sessionId: claim.sessionId,
|
|
@@ -97,7 +100,7 @@ export class SessionSandboxConnectionAttempt {
|
|
|
97
100
|
signal: this.controller.signal,
|
|
98
101
|
onMessage: frame => this.processor ? this.processor.receive(frame) : queuedFrames.push(frame),
|
|
99
102
|
onClose: close => closed.resolve(close),
|
|
100
|
-
}
|
|
103
|
+
}, Math.min(this.dependencies.establishmentDeadline, performance.now() + this.dependencies.timings.dialTimeoutMs), () => new ConnectionClosedError({
|
|
101
104
|
ready: false,
|
|
102
105
|
detail: 'Supervisor connection dial timed out.',
|
|
103
106
|
}))
|
|
@@ -136,30 +139,25 @@ export class SessionSandboxConnectionAttempt {
|
|
|
136
139
|
bootstrapBodyPromise,
|
|
137
140
|
secretsPromise,
|
|
138
141
|
])
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
'Session command 1 has an invalid body.'
|
|
146
|
-
|
|
142
|
+
let initialCommandBody: SessionBootstrapBody['initialCommand']
|
|
143
|
+
if (body.commandTransport !== 'wss-v1') {
|
|
144
|
+
if (!initialCommand || initialCommand.commandSeq !== 1)
|
|
145
|
+
throw new DeterministicEstablishmentError('agent-core.initial-command-invalid', 'Session command 1 is missing or invalid.')
|
|
146
|
+
const parsed = wireCommandBodySchema.safeParse(initialCommand.body)
|
|
147
|
+
if (!parsed.success)
|
|
148
|
+
throw new DeterministicEstablishmentError('agent-core.initial-command-invalid', 'Session command 1 has an invalid body.')
|
|
149
|
+
initialCommandBody = { commandId: initialCommand.commandId, commandSeq: 1, body: parsed.data }
|
|
147
150
|
}
|
|
148
|
-
|
|
149
|
-
await connection.send(encodeWireFrame({
|
|
151
|
+
await sendSupervisorBootstrap(connection, {
|
|
150
152
|
kind: 'session.bootstrap',
|
|
151
153
|
connectionGeneration: claim.runtimeConnectionGeneration,
|
|
152
154
|
body: {
|
|
153
155
|
...body,
|
|
154
156
|
sessionId: claim.sessionId,
|
|
155
|
-
initialCommand: {
|
|
156
|
-
commandId: initialCommand.commandId,
|
|
157
|
-
commandSeq: initialCommand.commandSeq,
|
|
158
|
-
body: initialBody,
|
|
159
|
-
},
|
|
157
|
+
...(initialCommandBody ? { initialCommand: initialCommandBody } : {}),
|
|
160
158
|
},
|
|
161
159
|
...secrets,
|
|
162
|
-
})
|
|
160
|
+
}, this.controller.signal)
|
|
163
161
|
|
|
164
162
|
const readiness = await beforeDeadline(Promise.race([
|
|
165
163
|
processor.waitForReadiness({ signal: this.controller.signal }),
|
|
@@ -176,15 +174,24 @@ export class SessionSandboxConnectionAttempt {
|
|
|
176
174
|
}
|
|
177
175
|
throw error
|
|
178
176
|
})
|
|
177
|
+
const wssV1 = body.commandTransport === 'wss-v1'
|
|
178
|
+
if (wssV1 && readiness.sourceId === undefined)
|
|
179
|
+
throw new DeterministicEstablishmentError('agent-core.journal-identity-missing', 'Supervisor did not announce its journal identity.')
|
|
180
|
+
// The store pins the journal identity before any command is dispatched or event persisted;
|
|
181
|
+
// a replaced journal is refused here instead of being resumed from a stale cursor.
|
|
179
182
|
const markedReady = await this.dependencies.authorityStore.markConnectionReady({
|
|
180
183
|
sessionSandboxId: claim.sessionSandboxId,
|
|
181
184
|
generation: claim.runtimeConnectionGeneration,
|
|
182
185
|
runtimeConnectionId: claim.runtimeConnectionId,
|
|
183
186
|
lastReceivedCommandSeq: readiness.lastReceivedCommandSeq,
|
|
184
187
|
agentRunSnapshot: readiness.agentRun,
|
|
188
|
+
...(readiness.sourceId === undefined ? {} : { journalSourceId: readiness.sourceId }),
|
|
185
189
|
})
|
|
186
|
-
if (
|
|
190
|
+
if (markedReady === false)
|
|
187
191
|
throw new ConnectionAuthorityLostError()
|
|
192
|
+
if (markedReady !== true)
|
|
193
|
+
throw new DeterministicEstablishmentError(markedReady.code, markedReady.detail)
|
|
194
|
+
processor.confirmReadiness()
|
|
188
195
|
this.ready = true
|
|
189
196
|
|
|
190
197
|
const pump = new SessionCommandPump({
|
|
@@ -192,11 +199,17 @@ export class SessionSandboxConnectionAttempt {
|
|
|
192
199
|
sessionSandboxId: claim.sessionSandboxId,
|
|
193
200
|
runtimeConnectionId: claim.runtimeConnectionId,
|
|
194
201
|
afterSequence: readiness.lastReceivedCommandSeq,
|
|
202
|
+
individualReceipts: wssV1,
|
|
195
203
|
commands: this.dependencies.commands,
|
|
196
204
|
authorityStore: this.dependencies.authorityStore,
|
|
197
205
|
connection,
|
|
198
206
|
logger: this.dependencies.logger,
|
|
199
207
|
onRejected: async (ack) => {
|
|
208
|
+
if (wssV1) {
|
|
209
|
+
// The pump already persisted the terminal rejection; the session keeps running.
|
|
210
|
+
this.dependencies.logger.warn({ commandSeq: ack.commandSeq, errorCode: ack.errorCode }, 'supervisor rejected a command; it will not be redispatched')
|
|
211
|
+
return
|
|
212
|
+
}
|
|
200
213
|
await this.dependencies.failures.markSessionFailed({
|
|
201
214
|
sessionId: claim.sessionId,
|
|
202
215
|
sessionSandboxId: claim.sessionSandboxId,
|
|
@@ -233,7 +246,10 @@ export class SessionSandboxConnectionAttempt {
|
|
|
233
246
|
}
|
|
234
247
|
|
|
235
248
|
async drain(): Promise<void> {
|
|
236
|
-
await
|
|
249
|
+
await drainSupervisorConnection([
|
|
250
|
+
() => this.processor?.drain(),
|
|
251
|
+
() => this.commandPump?.close(),
|
|
252
|
+
])
|
|
237
253
|
}
|
|
238
254
|
|
|
239
255
|
private async renewLease(): Promise<never> {
|
|
@@ -347,3 +363,62 @@ async function beforeDeadline<T>(
|
|
|
347
363
|
signal.removeEventListener('abort', abort!)
|
|
348
364
|
}
|
|
349
365
|
}
|
|
366
|
+
|
|
367
|
+
/** A cancelled/timed-out dial must never leave a late socket alive. */
|
|
368
|
+
export async function openSupervisorConnectionBeforeDeadline(
|
|
369
|
+
open: SupervisorConnectionOpener,
|
|
370
|
+
input: Parameters<SupervisorConnectionOpener>[0],
|
|
371
|
+
deadline: number,
|
|
372
|
+
timeoutError: () => Error,
|
|
373
|
+
): Promise<SupervisorConnection> {
|
|
374
|
+
input.signal.throwIfAborted()
|
|
375
|
+
const stop = new AbortController()
|
|
376
|
+
const signal = AbortSignal.any([input.signal, stop.signal])
|
|
377
|
+
let opened: SupervisorConnection | undefined
|
|
378
|
+
let retired = false
|
|
379
|
+
const retire = () => {
|
|
380
|
+
if (opened && !retired) {
|
|
381
|
+
retired = true
|
|
382
|
+
opened.close(1001, 'Connection dial cancelled.')
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const task = Promise.resolve().then(() => {
|
|
386
|
+
signal.throwIfAborted()
|
|
387
|
+
return open({ ...input, signal })
|
|
388
|
+
}).then((connection) => {
|
|
389
|
+
opened = connection
|
|
390
|
+
if (signal.aborted) {
|
|
391
|
+
retire()
|
|
392
|
+
throw signal.reason
|
|
393
|
+
}
|
|
394
|
+
return connection
|
|
395
|
+
})
|
|
396
|
+
try {
|
|
397
|
+
return await beforeDeadline(task, deadline, signal, timeoutError)
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
stop.abort(error)
|
|
401
|
+
retire()
|
|
402
|
+
// Observe late rejection even if the deadline elapsed before beforeDeadline raced it.
|
|
403
|
+
void task.catch(() => undefined)
|
|
404
|
+
throw error
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Release ownership only after every pending connection task has settled. */
|
|
409
|
+
export async function drainSupervisorConnection(tasks: readonly (() => void | Promise<void>)[]): Promise<void> {
|
|
410
|
+
const results = await Promise.allSettled(tasks.map(task => Promise.resolve().then(task)))
|
|
411
|
+
const errors = results.flatMap(result => result.status === 'rejected' ? [result.reason] : [])
|
|
412
|
+
if (errors.length === 1)
|
|
413
|
+
throw errors[0]
|
|
414
|
+
if (errors.length > 1)
|
|
415
|
+
throw new AggregateError(errors, 'Supervisor connection drain failed.')
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Validate the wire envelope and reject a bootstrap whose owner was cancelled while loading it. */
|
|
419
|
+
export async function sendSupervisorBootstrap(connection: SupervisorConnection, envelope: SessionBootstrapEnvelope, signal: AbortSignal): Promise<void> {
|
|
420
|
+
signal.throwIfAborted()
|
|
421
|
+
const encoded = encodeWireFrame(sessionBootstrapEnvelopeSchema.parse(envelope))
|
|
422
|
+
signal.throwIfAborted()
|
|
423
|
+
await connection.send(encoded)
|
|
424
|
+
}
|