|
|
@@ -10,20 +10,17 @@ import { readPluginInventory } from '@deepseek-ai/dsh-host-plugin-inventory'
|
|
|
import { readProfileManifest, resolveBundleDir, loadOverlayPatches, composeEntries, reconcileProfilePatches, readProfilePatches } from '@deepseek-ai/dsh-app-boot'
|
|
|
import type {} from '@deepseek-ai/dsh-hmr'
|
|
|
import type { ProfileContext } from '@deepseek-ai/dsh-app-boot'
|
|
|
-import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
|
-import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
|
import { bundleManifest, runProfilePnpm, saveManifest } from './operations.ts'
|
|
|
import { writePluginEnabled } from './patch.ts'
|
|
|
+import { ManagementFailure } from './failure.ts'
|
|
|
import { approveBuilds, readPendingBuilds } from './build-approval.ts'
|
|
|
import type { BundleInfo, ChangeResult, InstallBundleOptions, ManagementError, PackageResult, PluginEntryId, PluginInfo } from './types.ts'
|
|
|
export type * from './types.ts'
|
|
|
|
|
|
-/** Limits for package diagnostics and change notifications. */
|
|
|
+/** Limits for package diagnostics and lock acquisition. */
|
|
|
export interface Config {
|
|
|
/** Maximum retained pnpm diagnostic bytes per operation. */
|
|
|
outputBytes?: number
|
|
|
- /** Delay for combining consecutive management notices in one durable injection. */
|
|
|
- notificationDelayMs?: number
|
|
|
/** Maximum time to wait for another process's profile package operation. */
|
|
|
lockWaitMs?: number
|
|
|
}
|
|
|
@@ -51,12 +48,6 @@ function managementError(error: unknown): ManagementError {
|
|
|
: { code: 'operation-error', diagnostic: error instanceof Error ? error.message : String(error) }
|
|
|
}
|
|
|
|
|
|
-/** Expected management rejection; presentation belongs to the caller's locale. */
|
|
|
-class ManagementFailure extends Error {
|
|
|
- readonly code: ManagementError['code']
|
|
|
- constructor(code: ManagementError['code']) { super(code); this.code = code }
|
|
|
-}
|
|
|
-
|
|
|
declare module '@deepseek-ai/cordis' {
|
|
|
interface Context {
|
|
|
/** Persistent management of the current profile's composition and packages. */
|
|
|
@@ -69,21 +60,14 @@ export class PluginManager extends TypertRemoteService {
|
|
|
static inject = ['loader', 'profileContext']
|
|
|
static Config: z<Config> = z.object({
|
|
|
outputBytes: z.number().step(1).min(1).default(16384),
|
|
|
- notificationDelayMs: z.number().step(1).min(0).default(250),
|
|
|
lockWaitMs: z.number().step(1).min(0).default(120000),
|
|
|
})
|
|
|
private readonly ownerEntryId: string | undefined
|
|
|
private readonly packageOperations = new Set<Promise<PackageResult>>()
|
|
|
private readonly profile: ProfileContext
|
|
|
private readonly outputBytes: number
|
|
|
- private readonly notificationDelayMs: number
|
|
|
private readonly lockWaitMs: number
|
|
|
private readonly ownerContext: Context
|
|
|
- private pendingNotice = ''
|
|
|
- private omittedNotices = 0
|
|
|
- private noticeTimer: ReturnType<typeof setTimeout> | undefined
|
|
|
- private noticeDelivered: PromiseWithResolvers<void> | undefined
|
|
|
- private readonly noticeAgents = new Set<Agent>()
|
|
|
private readonly abort = new AbortController()
|
|
|
|
|
|
constructor(ctx: Context, config: Config) {
|
|
|
@@ -92,13 +76,10 @@ export class PluginManager extends TypertRemoteService {
|
|
|
this.ownerContext = ctx
|
|
|
this.profile = ctx.profileContext
|
|
|
this.outputBytes = (config as Required<Config>).outputBytes
|
|
|
- this.notificationDelayMs = (config as Required<Config>).notificationDelayMs
|
|
|
this.lockWaitMs = (config as Required<Config>).lockWaitMs
|
|
|
ctx.effect(() => async () => {
|
|
|
this.abort.abort()
|
|
|
await Promise.allSettled([...this.packageOperations])
|
|
|
- clearTimeout(this.noticeTimer)
|
|
|
- this.flushNotice()
|
|
|
}, 'plugin-manager: package cancellation')
|
|
|
}
|
|
|
|
|
|
@@ -207,7 +188,10 @@ export class PluginManager extends TypertRemoteService {
|
|
|
try {
|
|
|
result.packageResult = await this.runPnpm(['add', spec])
|
|
|
if (result.packageResult.exitCode !== 0) {
|
|
|
- result.pendingBuilds = await readPendingBuilds(this.profile.dir)
|
|
|
+ try { result.pendingBuilds = await readPendingBuilds(this.profile.dir) }
|
|
|
+ catch (error) {
|
|
|
+ this.ownerContext.logger.warn('Could not read pending build approvals after pnpm failed', error)
|
|
|
+ }
|
|
|
throw new Error(result.packageResult.output)
|
|
|
}
|
|
|
const after = readProfileManifest('dsh', this.profile.dir).dependencies ?? {}
|
|
|
@@ -337,8 +321,7 @@ export class PluginManager extends TypertRemoteService {
|
|
|
operation: (result: ChangeResult) => Promise<ChangeResult['application'] | void>,
|
|
|
request: Pick<ChangeResult, 'stage' | 'target' | 'enabled'>,
|
|
|
): Promise<ChangeResult> {
|
|
|
- let notice: Promise<void> | undefined
|
|
|
- const locked = () => withFileLock(join(this.profile.dir, 'package.json'), async () => {
|
|
|
+ return withFileLock(join(this.profile.dir, 'package.json'), async () => {
|
|
|
this.abort.signal.throwIfAborted()
|
|
|
const before = this.diskState()
|
|
|
const result: ChangeResult = { ...request, changed: false,
|
|
|
@@ -350,12 +333,8 @@ export class PluginManager extends TypertRemoteService {
|
|
|
result.error = managementError(error)
|
|
|
}
|
|
|
result.changed = before !== this.diskState()
|
|
|
- notice = this.notify(result)
|
|
|
return result
|
|
|
}, { waitMs: this.lockWaitMs })
|
|
|
- const result = await locked()
|
|
|
- await notice
|
|
|
- return result
|
|
|
}
|
|
|
|
|
|
private diskState(): string {
|
|
|
@@ -367,46 +346,6 @@ export class PluginManager extends TypertRemoteService {
|
|
|
}
|
|
|
}).join('\u0000')
|
|
|
}
|
|
|
-
|
|
|
- private notify(result: ChangeResult): Promise<void> {
|
|
|
- const agents = this.ownerContext.get('agents')?.list() ?? []
|
|
|
- if (agents.length === 0) return Promise.resolve()
|
|
|
- for (const agent of agents) this.noticeAgents.add(agent)
|
|
|
- this.noticeDelivered ??= Promise.withResolvers<void>()
|
|
|
- const delivered = this.noticeDelivered.promise
|
|
|
- const notice = JSON.stringify({ profile: this.profile.name, ...result })
|
|
|
- if (Buffer.byteLength(this.pendingNotice + notice) > this.outputBytes) {
|
|
|
- this.omittedNotices += 1
|
|
|
- } else {
|
|
|
- this.pendingNotice += `${notice}\n`
|
|
|
- }
|
|
|
- if (this.noticeTimer !== undefined) return delivered
|
|
|
- this.noticeTimer = setTimeout(() => { this.flushNotice() }, this.notificationDelayMs)
|
|
|
- return delivered
|
|
|
- }
|
|
|
-
|
|
|
- private flushNotice(): void {
|
|
|
- this.noticeTimer = undefined
|
|
|
- const text = this.pendingNotice + (this.omittedNotices === 0 ? '' : JSON.stringify({ omitted: this.omittedNotices, refresh: 'plugin_manager' }) + '\n')
|
|
|
- this.pendingNotice = ''
|
|
|
- this.omittedNotices = 0
|
|
|
- const delivered = this.noticeDelivered
|
|
|
- this.noticeDelivered = undefined
|
|
|
- if (delivered === undefined) return
|
|
|
- const agents = [...this.noticeAgents]
|
|
|
- this.noticeAgents.clear()
|
|
|
- for (const agent of agents) {
|
|
|
- try {
|
|
|
- agent.inject(createUserMessage({
|
|
|
- content: [{ type: 'text', text }],
|
|
|
- source: { kind: 'plugin', plugin: 'plugin-manager' },
|
|
|
- }))
|
|
|
- } catch (error) {
|
|
|
- this.ownerContext.logger.warn('Plugin management notification could not reach an Agent', error)
|
|
|
- }
|
|
|
- }
|
|
|
- delivered.resolve()
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
export default PluginManager
|