|
@@ -5,7 +5,8 @@
|
|
|
* @module @deepseek-ai/dsh-agent
|
|
* @module @deepseek-ai/dsh-agent
|
|
|
*/
|
|
*/
|
|
|
|
|
|
|
|
-import { Context, getTraceable, Service, symbols } from 'cordis'
|
|
|
|
|
|
|
+import { Context, FiberState, getTraceable, Service, symbols } from 'cordis'
|
|
|
|
|
+import type { Fiber } from 'cordis'
|
|
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
|
import { isPromise } from 'node:util/types'
|
|
import { isPromise } from 'node:util/types'
|
|
|
import { scopeTarget } from '@deepseek-ai/dsh-scope'
|
|
import { scopeTarget } from '@deepseek-ai/dsh-scope'
|
|
@@ -190,6 +191,12 @@ interface AgentEntry {
|
|
|
detachRequested: boolean
|
|
detachRequested: boolean
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/** One tracked boundary plus its inherited nesting chain. */
|
|
|
|
|
+interface InitiatorRun {
|
|
|
|
|
+ active: boolean
|
|
|
|
|
+ readonly parent: InitiatorRun | undefined
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/** Plain holder prevents Cordis from tracing the factory field before the caller context is known. */
|
|
/** Plain holder prevents Cordis from tracing the factory field before the caller context is known. */
|
|
|
interface FactorySlot {
|
|
interface FactorySlot {
|
|
|
readonly target: AgentFactory
|
|
readonly target: AgentFactory
|
|
@@ -205,6 +212,7 @@ export class AgentRegistry extends Service {
|
|
|
private store = new Map<SessionId, AgentEntry>()
|
|
private store = new Map<SessionId, AgentEntry>()
|
|
|
private factory: FactorySlot | undefined
|
|
private factory: FactorySlot | undefined
|
|
|
private readonly initiators = new AsyncLocalStorage<Agent | undefined>()
|
|
private readonly initiators = new AsyncLocalStorage<Agent | undefined>()
|
|
|
|
|
+ private readonly initiatorRuns = new AsyncLocalStorage<InitiatorRun>()
|
|
|
private initiatorState: 'active' | 'closing' | 'disposed' = 'active'
|
|
private initiatorState: 'active' | 'closing' | 'disposed' = 'active'
|
|
|
private activeInitiatorRuns = 0
|
|
private activeInitiatorRuns = 0
|
|
|
private initiatorDrain: PromiseWithResolvers<void> | undefined
|
|
private initiatorDrain: PromiseWithResolvers<void> | undefined
|
|
@@ -219,6 +227,11 @@ export class AgentRegistry extends Service {
|
|
|
// accessor body never needs to resolve a scope itself. Effect-scoped:
|
|
// accessor body never needs to resolve a scope itself. Effect-scoped:
|
|
|
// unwinds with this service's fiber.
|
|
// unwinds with this service's fiber.
|
|
|
ctx.accessor('agent', { get: () => undefined })
|
|
ctx.accessor('agent', { get: () => undefined })
|
|
|
|
|
+ ctx.on('internal/status', (fiber) => {
|
|
|
|
|
+ if (fiber.state === FiberState.UNLOADING && this.hasLifecycleAncestor(fiber)) {
|
|
|
|
|
+ this.closeInitiators()
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
ctx.effect(function* (this: AgentRegistry) {
|
|
ctx.effect(function* (this: AgentRegistry) {
|
|
|
yield () => this.disposeInitiators()
|
|
yield () => this.disposeInitiators()
|
|
|
yield () => { this.closeInitiators() }
|
|
yield () => { this.closeInitiators() }
|
|
@@ -249,6 +262,8 @@ export class AgentRegistry extends Service {
|
|
|
/**
|
|
/**
|
|
|
* Run an operation with one exact Agent as its process-local initiator. The
|
|
* Run an operation with one exact Agent as its process-local initiator. The
|
|
|
* exact synchronous value or Promise returned by the operation is preserved.
|
|
* exact synchronous value or Promise returned by the operation is preserved.
|
|
|
|
|
+ * If its inherited async chain starts an owning-fiber unload, the nested
|
|
|
|
|
+ * boundary lineage is excluded from the drain so teardown cannot wait on itself.
|
|
|
* @param agent - initiating Agent to inherit; presence is neither liveness proof nor authorization.
|
|
* @param agent - initiating Agent to inherit; presence is neither liveness proof nor authorization.
|
|
|
* @param operation - synchronous or asynchronous operation to invoke.
|
|
* @param operation - synchronous or asynchronous operation to invoke.
|
|
|
* @returns the exact value returned by `operation`.
|
|
* @returns the exact value returned by `operation`.
|
|
@@ -261,6 +276,8 @@ export class AgentRegistry extends Service {
|
|
|
/**
|
|
/**
|
|
|
* Run an operation inside a boundary that hides any inherited initiating
|
|
* Run an operation inside a boundary that hides any inherited initiating
|
|
|
* Agent. The exact synchronous value or Promise is preserved.
|
|
* Agent. The exact synchronous value or Promise is preserved.
|
|
|
|
|
+ * If its inherited async chain starts an owning-fiber unload, the nested
|
|
|
|
|
+ * boundary lineage is excluded from the drain so teardown cannot wait on itself.
|
|
|
* @param operation - synchronous or asynchronous operation to invoke without an initiator.
|
|
* @param operation - synchronous or asynchronous operation to invoke without an initiator.
|
|
|
* @returns the exact value returned by `operation`.
|
|
* @returns the exact value returned by `operation`.
|
|
|
* @throws when the initiator scope is closing/disposed, or when `operation` throws.
|
|
* @throws when the initiator scope is closing/disposed, or when `operation` throws.
|
|
@@ -537,42 +554,77 @@ export class AgentRegistry extends Service {
|
|
|
private disposeInitiators(): Promise<void> {
|
|
private disposeInitiators(): Promise<void> {
|
|
|
return (this.initiatorDisposal ??= (async () => {
|
|
return (this.initiatorDisposal ??= (async () => {
|
|
|
this.closeInitiators()
|
|
this.closeInitiators()
|
|
|
|
|
+ this.releaseReentrantInitiatorRuns()
|
|
|
if (this.activeInitiatorRuns !== 0) {
|
|
if (this.activeInitiatorRuns !== 0) {
|
|
|
this.initiatorDrain ??= Promise.withResolvers<void>()
|
|
this.initiatorDrain ??= Promise.withResolvers<void>()
|
|
|
await this.initiatorDrain.promise
|
|
await this.initiatorDrain.promise
|
|
|
}
|
|
}
|
|
|
this.initiatorState = 'disposed'
|
|
this.initiatorState = 'disposed'
|
|
|
this.initiators.disable()
|
|
this.initiators.disable()
|
|
|
|
|
+ this.initiatorRuns.disable()
|
|
|
})())
|
|
})())
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** Establish one tracked initiator or clearing boundary. */
|
|
/** Establish one tracked initiator or clearing boundary. */
|
|
|
private runWithInitiator<T>(agent: Agent | undefined, operation: () => T): T {
|
|
private runWithInitiator<T>(agent: Agent | undefined, operation: () => T): T {
|
|
|
if (this.initiatorState !== 'active') throw new Error(DISPOSED_INITIATOR_MESSAGE)
|
|
if (this.initiatorState !== 'active') throw new Error(DISPOSED_INITIATOR_MESSAGE)
|
|
|
|
|
+ const run: InitiatorRun = {
|
|
|
|
|
+ active: true,
|
|
|
|
|
+ parent: this.initiatorRuns.getStore(),
|
|
|
|
|
+ }
|
|
|
this.activeInitiatorRuns += 1
|
|
this.activeInitiatorRuns += 1
|
|
|
let result: T
|
|
let result: T
|
|
|
try {
|
|
try {
|
|
|
- result = this.initiators.run(agent, operation)
|
|
|
|
|
|
|
+ result = this.initiatorRuns.run(run, () => this.initiators.run(agent, operation))
|
|
|
} catch (error: unknown) {
|
|
} catch (error: unknown) {
|
|
|
- this.releaseInitiatorRun()
|
|
|
|
|
|
|
+ this.releaseInitiatorRun(run)
|
|
|
throw error
|
|
throw error
|
|
|
}
|
|
}
|
|
|
if (isPromise(result)) {
|
|
if (isPromise(result)) {
|
|
|
- void result.then(
|
|
|
|
|
- () => { this.releaseInitiatorRun() },
|
|
|
|
|
- () => { this.releaseInitiatorRun() },
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ void Promise.prototype.then.call(
|
|
|
|
|
+ result,
|
|
|
|
|
+ () => { this.releaseInitiatorRun(run) },
|
|
|
|
|
+ () => { this.releaseInitiatorRun(run) },
|
|
|
|
|
+ )
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ // A branded Promise may expose a failing @@species. Observer setup did
|
|
|
|
|
+ // not attach, so preserve the exact return without leaking the run.
|
|
|
|
|
+ this.releaseInitiatorRun(run)
|
|
|
|
|
+ }
|
|
|
} else {
|
|
} else {
|
|
|
- this.releaseInitiatorRun()
|
|
|
|
|
|
|
+ this.releaseInitiatorRun(run)
|
|
|
}
|
|
}
|
|
|
return result
|
|
return result
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /** Whether one unloading fiber owns this service's lifecycle. */
|
|
|
|
|
+ private hasLifecycleAncestor(candidate: Fiber): boolean {
|
|
|
|
|
+ let fiber = this.ctx.fiber
|
|
|
|
|
+ while (true) {
|
|
|
|
|
+ if (fiber === candidate) return true
|
|
|
|
|
+ const parent = fiber.parent.fiber
|
|
|
|
|
+ if (parent === fiber) return false
|
|
|
|
|
+ fiber = parent
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private assertInitiatorsReadable(): void {
|
|
private assertInitiatorsReadable(): void {
|
|
|
if (this.initiatorState === 'disposed') throw new Error(DISPOSED_INITIATOR_MESSAGE)
|
|
if (this.initiatorState === 'disposed') throw new Error(DISPOSED_INITIATOR_MESSAGE)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private releaseInitiatorRun(): void {
|
|
|
|
|
|
|
+ /** Exclude the boundary chain that initiated this teardown from its own drain. */
|
|
|
|
|
+ private releaseReentrantInitiatorRuns(): void {
|
|
|
|
|
+ let run = this.initiatorRuns.getStore()
|
|
|
|
|
+ while (run !== undefined) {
|
|
|
|
|
+ this.releaseInitiatorRun(run)
|
|
|
|
|
+ run = run.parent
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private releaseInitiatorRun(run: InitiatorRun): void {
|
|
|
|
|
+ if (!run.active) return
|
|
|
|
|
+ run.active = false
|
|
|
this.activeInitiatorRuns -= 1
|
|
this.activeInitiatorRuns -= 1
|
|
|
if (this.activeInitiatorRuns !== 0) return
|
|
if (this.activeInitiatorRuns !== 0) return
|
|
|
this.initiatorDrain?.resolve()
|
|
this.initiatorDrain?.resolve()
|