mirror of
https://github.com/stablyai/orca.git
synced 2026-11-14 01:47:24 +00:00
* fix(plugins): don't count time asleep as worker idle time The idle reaper measured idleness with the wall clock, so after a sleep longer than the reap window its first sweep reaped every worker before their own timers could fire. A sweep gap well past the 60s interval now starts a fresh idle window. * fix(plugins): count a sleep before the first idle sweep as asleep, not idle Start the reaper's sleep-detection clock when the first worker starts, so the first sweep after waking can see the gap even if no sweep ran before the lid closed. * fix(plugins): keep worker manager under max-lines and restore real timers after sleep tests Move the idle-candidate filter into PluginWorkerIdleClock so the rebased manager stays within the 300-line lint cap, and reset fake timers after the idle-reap tests so the sleep-before-first-sweep case does not leak them. --------- Co-authored-by: petersindex <287557875+petersindex@users.noreply.github.com> Co-authored-by: Neil <neil@stably.ai>
107 lines
4.2 KiB
TypeScript
107 lines
4.2 KiB
TypeScript
import { z } from 'zod'
|
|
import {
|
|
PLUGIN_COMMAND_LIMIT,
|
|
PLUGIN_EVENT_NAMES,
|
|
pluginCommandIdSchema
|
|
} from '../manifest/plugin-manifest'
|
|
import { PLUGIN_CAPABILITY_KINDS } from '../capabilities/plugin-capabilities'
|
|
import { pluginEvent, type PluginEvent } from '../frames/plugin-protocol'
|
|
import { pluginWorkerPanelPostSchema } from './plugin-panel-bridge-protocol'
|
|
|
|
/**
|
|
* The worker fork channel, on the shared envelope (plugin-protocol.ts). Validate both sides:
|
|
* third-party worker code cannot supply trusted structure. Host calls are the worker's `req`s;
|
|
* the methods and events below are the host's lifecycle traffic.
|
|
*/
|
|
|
|
export const WORKER_EVENTS = {
|
|
/** host → worker, {@link PluginWorkerInit}. */
|
|
init: 'init',
|
|
/** worker → host, `{ commands }` registered on activate. */
|
|
ready: 'ready',
|
|
log: 'log',
|
|
fatal: 'fatal',
|
|
shutdown: 'shutdown',
|
|
/** worker → host, {@link PluginWorkerPanelPost}: `orca.panels.post`. */
|
|
panelPost: 'panel.post',
|
|
/** host → worker, {@link PluginDataChanged}: a panel or the person changed storage or settings. */
|
|
dataChanged: 'data.changed'
|
|
} as const
|
|
|
|
/** host → worker requests; the worker answers each with a `res`. */
|
|
export const WORKER_METHODS = {
|
|
invokeCommand: 'command.invoke',
|
|
deliverEvent: 'event.deliver',
|
|
/** A panel's `plugin.request`, answered by the handler from `orca.panels.handle`. */
|
|
panelRequest: 'panel.request',
|
|
/** One hook, answered with a decision (plugin-worker-hooks.ts). */
|
|
runHook: 'hook.run'
|
|
} as const
|
|
|
|
export const pluginWorkerInitSchema = z.object({
|
|
/** Qualified `<publisher>.<id>` key. */
|
|
pluginId: z.string().min(1),
|
|
pluginRoot: z.string().min(1),
|
|
mainEntry: z.string().min(1),
|
|
/** Worker-side early refusals only; host rechecks every call regardless of declared grants. */
|
|
grantedCapabilities: z.array(z.enum(PLUGIN_CAPABILITY_KINDS))
|
|
})
|
|
|
|
export const pluginWorkerInvokeParamsSchema = z.object({
|
|
commandId: z.string().min(1),
|
|
args: z.unknown().optional()
|
|
})
|
|
|
|
export const pluginWorkerDeliverParamsSchema = z.object({
|
|
event: z.enum(PLUGIN_EVENT_NAMES),
|
|
payload: z.unknown()
|
|
})
|
|
|
|
export const pluginWorkerReadySchema = z.object({
|
|
/** Command ids the worker registered handlers for (⊆ manifest commands). */
|
|
commands: z.array(pluginCommandIdSchema).max(PLUGIN_COMMAND_LIMIT)
|
|
})
|
|
|
|
export const pluginWorkerLogSchema = z.object({
|
|
level: z.enum(['info', 'warn', 'error']),
|
|
message: z.string().max(8192)
|
|
})
|
|
|
|
export const pluginWorkerFatalSchema = z.object({ error: z.string().max(8192) })
|
|
|
|
export type PluginWorkerInit = z.infer<typeof pluginWorkerInitSchema>
|
|
|
|
const pluginWorkerChildEventSchema = z.discriminatedUnion('event', [
|
|
z.object({ event: z.literal(WORKER_EVENTS.ready), payload: pluginWorkerReadySchema }),
|
|
z.object({ event: z.literal(WORKER_EVENTS.log), payload: pluginWorkerLogSchema }),
|
|
z.object({ event: z.literal(WORKER_EVENTS.fatal), payload: pluginWorkerFatalSchema }),
|
|
z.object({ event: z.literal(WORKER_EVENTS.panelPost), payload: pluginWorkerPanelPostSchema })
|
|
])
|
|
|
|
/** A worker → host lifecycle event, or null for anything else. */
|
|
export function readPluginWorkerEvent(
|
|
message: PluginEvent
|
|
): z.infer<typeof pluginWorkerChildEventSchema> | null {
|
|
const parsed = pluginWorkerChildEventSchema.safeParse(message)
|
|
return parsed.success ? parsed.data : null
|
|
}
|
|
|
|
export function pluginWorkerLog(level: 'info' | 'warn' | 'error', message: string): PluginEvent {
|
|
return pluginEvent(WORKER_EVENTS.log, { level, message: message.slice(0, 8192) })
|
|
}
|
|
|
|
export function pluginWorkerFatal(error: string): PluginEvent {
|
|
return pluginEvent(WORKER_EVENTS.fatal, { error: error.slice(0, 8192) })
|
|
}
|
|
|
|
export const PLUGIN_WORKER_READY_TIMEOUT_MS = 10_000
|
|
export const PLUGIN_WORKER_INVOKE_TIMEOUT_MS = 30_000
|
|
/** Dispose idle workers and refork on their next trigger. */
|
|
export const PLUGIN_WORKER_IDLE_REAP_MS = 5 * 60_000
|
|
/** How often the main process sweeps for idle workers. */
|
|
export const PLUGIN_WORKER_REAP_INTERVAL_MS = 60_000
|
|
/** Default cap on concurrently-active workers; excess activations queue. */
|
|
export const PLUGIN_WORKER_MAX_ACTIVE_DEFAULT = 5
|
|
/** Soft cap on always-running hook workers, which hold no slot of the cap above. */
|
|
export const PLUGIN_WORKER_MAX_KEEP_WARM_DEFAULT = 16
|