fix(tasks): bound per-session background work
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
*/
|
||||
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import z from '@deepseek-ai/schemastery'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { AnonymousEntries, ScopedLayers, scopeOf } from '@deepseek-ai/dsh-scope'
|
||||
import type { ScopeLayer } from '@deepseek-ai/dsh-scope'
|
||||
@@ -23,6 +24,18 @@ import type {
|
||||
/** Timeout code that distinguishes a bounded wait from caller cancellation. */
|
||||
export const TASK_WAIT_TIMEOUT = 'TASK_WAIT_TIMEOUT'
|
||||
|
||||
/** Default maximum number of active tasks in one exact-owner bucket. */
|
||||
const DEFAULT_MAX_CONCURRENT_TASKS_PER_OWNER = 10
|
||||
|
||||
/** Configuration for the process-local task registry. */
|
||||
export interface Config {
|
||||
/** Maximum `running` plus `stopping` tasks per exact owner; omission defaults to 10. */
|
||||
maxConcurrentTasksPerOwner?: number
|
||||
}
|
||||
|
||||
/** Configuration after defaults and load-time validation. */
|
||||
type ResolvedConfig = Required<Config>
|
||||
|
||||
/** The registry's mutable per-task record (never handed out — see {@link LocalTaskService.snapshot}). */
|
||||
interface TrackedTask {
|
||||
id: TaskId
|
||||
@@ -76,6 +89,16 @@ class TaskLayer implements ScopeLayer {
|
||||
* semantics this implementation honors.
|
||||
*/
|
||||
export class LocalTaskService extends TaskService {
|
||||
static Config: z<Config> = z.object({
|
||||
maxConcurrentTasksPerOwner: z.number()
|
||||
.step(1)
|
||||
.min(1)
|
||||
.max(Number.MAX_SAFE_INTEGER)
|
||||
.default(DEFAULT_MAX_CONCURRENT_TASKS_PER_OWNER),
|
||||
})
|
||||
|
||||
/** Validated registry configuration. */
|
||||
readonly config: ResolvedConfig
|
||||
private store = new Map<TaskId, TrackedTask>()
|
||||
private counters = new Map<string, number>()
|
||||
/**
|
||||
@@ -97,8 +120,14 @@ export class LocalTaskService extends TaskService {
|
||||
/** Service context used by detached settlement continuations and teardown. */
|
||||
private readonly selfCtx: Context
|
||||
|
||||
constructor(ctx: Context) {
|
||||
constructor(ctx: Context, config: Config = {}) {
|
||||
super(ctx)
|
||||
const maxConcurrentTasksPerOwner = config.maxConcurrentTasksPerOwner
|
||||
?? DEFAULT_MAX_CONCURRENT_TASKS_PER_OWNER
|
||||
if (!Number.isSafeInteger(maxConcurrentTasksPerOwner) || maxConcurrentTasksPerOwner <= 0) {
|
||||
throw new TypeError('tasks-local: maxConcurrentTasksPerOwner must be a positive safe integer')
|
||||
}
|
||||
this.config = { maxConcurrentTasksPerOwner }
|
||||
this.selfCtx = ctx
|
||||
ctx.effect(() => () => this.disposeAll(), 'tasks teardown')
|
||||
}
|
||||
@@ -115,6 +144,13 @@ export class LocalTaskService extends TaskService {
|
||||
}
|
||||
if (spec.owner !== undefined) this.ensureOwnerCleanup(spec.owner)
|
||||
|
||||
const active = this.activeTaskCount(spec.owner)
|
||||
if (active >= this.config.maxConcurrentTasksPerOwner) {
|
||||
throw new Error(
|
||||
`background task limit reached for this owner (${active}/${this.config.maxConcurrentTasksPerOwner} active); use task_kill to stop an unneeded task, wait for it to finish, then retry`,
|
||||
)
|
||||
}
|
||||
|
||||
const hooks = spec.run()
|
||||
const count = (this.counters.get(spec.kind) ?? 0) + 1
|
||||
this.counters.set(spec.kind, count)
|
||||
@@ -285,6 +321,15 @@ export class LocalTaskService extends TaskService {
|
||||
.some(layer => !layer.surfaces.isEmpty())
|
||||
}
|
||||
|
||||
/** Count authoritative active records for one exact owner or the shared unowned bucket. */
|
||||
private activeTaskCount(owner: Agent | undefined): number {
|
||||
let count = 0
|
||||
for (const task of this.store.values()) {
|
||||
if (task.owner === owner && (task.status === 'running' || task.status === 'stopping')) count += 1
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
/**
|
||||
* The completion listeners that own `owner`'s notices: the global layer's
|
||||
* first, then each scoped layer along the owner's chain. A listener outside
|
||||
|
||||
Reference in New Issue
Block a user