@zhin.js/runtime 1.0.12 → 1.0.14

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.
@@ -1,5 +1,7 @@
1
1
  import type { PluginId } from '@zhin.js/plugin-runtime';
2
2
  import type { ProjectGraph } from './project-graph.js';
3
+ /** Host configuration keys consumed by Runtime composition and Host installers. */
4
+ export declare const HOST_CONFIG_KEYS: readonly string[];
3
5
  export type JsonSchema = Readonly<Record<string, unknown>>;
4
6
  export type RuntimeConfigDocument = Readonly<Record<string, unknown>>;
5
7
  export interface ComposedConfig {
@@ -1,6 +1,18 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import Ajv2020 from 'ajv/dist/2020.js';
4
+ import hostConfigSchema from './host-config-schema.json' with { type: 'json' };
5
+ function deepFreeze(value) {
6
+ if (value && typeof value === 'object' && !Object.isFrozen(value)) {
7
+ for (const nested of Object.values(value))
8
+ deepFreeze(nested);
9
+ Object.freeze(value);
10
+ }
11
+ return value;
12
+ }
13
+ const HOST_CONFIG_SCHEMA = deepFreeze(hostConfigSchema);
14
+ /** Host configuration keys consumed by Runtime composition and Host installers. */
15
+ export const HOST_CONFIG_KEYS = Object.freeze(Object.keys(HOST_CONFIG_SCHEMA.properties));
4
16
  export class ConfigSchemaCollisionError extends Error {
5
17
  plugin;
6
18
  instanceKey;
@@ -43,6 +55,7 @@ export class ConfigComposer {
43
55
  type: 'object',
44
56
  additionalProperties: false,
45
57
  properties: {
58
+ ...HOST_CONFIG_SCHEMA.properties,
46
59
  plugin: withDefault(rootOwn),
47
60
  plugins: {
48
61
  type: 'object',
@@ -50,45 +63,10 @@ export class ConfigComposer {
50
63
  default: {},
51
64
  properties: Object.fromEntries(childSchemas.map(([key, schema]) => [key, withDefault(schema)])),
52
65
  },
53
- http: Object.freeze({
54
- type: 'object',
55
- additionalProperties: true,
56
- }),
57
- database: Object.freeze({
58
- type: 'object',
59
- additionalProperties: true,
60
- }),
61
- ai: Object.freeze({
62
- type: 'object',
63
- additionalProperties: true,
64
- }),
65
- mcp: Object.freeze({
66
- type: 'object',
67
- additionalProperties: true,
68
- }),
69
- a2a: Object.freeze({
70
- type: 'object',
71
- additionalProperties: true,
72
- }),
73
- speech: Object.freeze({
74
- type: 'object',
75
- additionalProperties: true,
76
- }),
77
- htmlRenderer: Object.freeze({
78
- type: 'object',
79
- additionalProperties: true,
80
- }),
81
- assistant: Object.freeze({
82
- type: 'object',
83
- additionalProperties: true,
84
- }),
85
- log_level: Object.freeze({
86
- type: ['string', 'number'],
87
- }),
88
66
  },
89
67
  });
90
68
  const document = structuredClone(input);
91
- const validate = new Ajv2020({
69
+ const ajv = new Ajv2020({
92
70
  allErrors: true,
93
71
  useDefaults: true,
94
72
  strict: true,
@@ -96,7 +74,9 @@ export class ConfigComposer {
96
74
  // 不触发 allowUnionTypes);保留此项是面向未来插件 schema 可能出现的
97
75
  // anyOf/oneOf 标量 union,避免届时 Ajv strict 模式直接报错。
98
76
  allowUnionTypes: true,
99
- }).compile(effectiveSchema);
77
+ });
78
+ ajv.addKeyword({ keyword: 'x-descriptionZh', schemaType: 'string' });
79
+ const validate = ajv.compile(effectiveSchema);
100
80
  if (!validate(document)) {
101
81
  throw new ConfigValidationError(formatErrors(validate.errors ?? []), source);
102
82
  }
@@ -20,6 +20,6 @@ export declare class HmrCoordinator {
20
20
  private readonly options;
21
21
  constructor(options: HmrCoordinatorOptions);
22
22
  start(): Dispose;
23
- stop(): void;
23
+ stop(): Promise<void>;
24
24
  enqueue(source: string): Promise<void>;
25
25
  }
@@ -5,12 +5,19 @@ export class HmrCoordinator {
5
5
  #waiters = [];
6
6
  #draining;
7
7
  #unwatch;
8
+ #closing = false;
9
+ #restartRequired = false;
10
+ #stopResult;
8
11
  constructor(options) {
9
12
  this.options = options;
10
13
  }
11
14
  start() {
12
15
  if (this.#unwatch)
13
16
  throw new Error('HmrCoordinator is already started');
17
+ if (this.#closing)
18
+ throw new Error('HmrCoordinator has been stopped');
19
+ if (this.#restartRequired)
20
+ throw new Error('HmrCoordinator requires a process restart');
14
21
  if (!this.options.modules.watch) {
15
22
  throw new Error('ModuleRuntime does not provide a file watcher');
16
23
  }
@@ -21,10 +28,23 @@ export class HmrCoordinator {
21
28
  return () => this.stop();
22
29
  }
23
30
  stop() {
31
+ if (this.#stopResult)
32
+ return this.#stopResult;
33
+ this.#closing = true;
24
34
  this.#unwatch?.();
25
35
  this.#unwatch = undefined;
36
+ this.#stopResult = (async () => {
37
+ await this.#draining;
38
+ })();
39
+ return this.#stopResult;
26
40
  }
27
41
  enqueue(source) {
42
+ if (this.#closing) {
43
+ return Promise.reject(new Error('HMR coordinator is stopping'));
44
+ }
45
+ if (this.#restartRequired) {
46
+ return Promise.reject(new Error('HMR coordinator requires a process restart'));
47
+ }
28
48
  this.#pending.add(source);
29
49
  const completed = new Promise((resolve, reject) => {
30
50
  this.#waiters.push({ resolve, reject });
@@ -50,14 +70,16 @@ export class HmrCoordinator {
50
70
  this.#pending.clear();
51
71
  const forcedRestart = changed.filter((source) => this.options.modules.requiresProcessRestart?.(source));
52
72
  if (forcedRestart.length > 0) {
53
- await this.options.onRestartRequired(Object.freeze({
73
+ this.#restartRequired = true;
74
+ this.#pending.clear();
75
+ this.#notifyRestart(Object.freeze({
54
76
  kind: 'process',
55
77
  changed: Object.freeze(changed),
56
78
  reasons: Object.freeze([
57
79
  `Module loader cannot safely invalidate: ${forcedRestart.join(', ')}`,
58
80
  ]),
59
81
  }));
60
- continue;
82
+ break;
61
83
  }
62
84
  const dependencyPort = this.options.modules.affectedSources
63
85
  ? {
@@ -65,11 +87,14 @@ export class HmrCoordinator {
65
87
  }
66
88
  : undefined;
67
89
  const plan = new InvalidationPlanner(this.options.ownership(), dependencyPort).plan(changed);
68
- await this.options.onPlan?.(plan);
69
90
  if (plan.kind === 'process') {
70
- await this.options.onRestartRequired(plan);
71
- continue;
91
+ this.#restartRequired = true;
92
+ this.#pending.clear();
93
+ this.#notifyPlan(plan);
94
+ this.#notifyRestart(plan);
95
+ break;
72
96
  }
97
+ this.#notifyPlan(plan);
73
98
  if (plan.kind === 'none')
74
99
  continue;
75
100
  const startedAt = performance.now();
@@ -77,15 +102,30 @@ export class HmrCoordinator {
77
102
  await this.options.modules.invalidate?.(source);
78
103
  }
79
104
  const restart = await this.options.runtime.reload(plan);
80
- if (restart)
81
- await this.options.onRestartRequired(restart);
105
+ if (restart) {
106
+ this.#restartRequired = true;
107
+ this.#pending.clear();
108
+ this.#notifyRestart(restart);
109
+ break;
110
+ }
82
111
  else {
83
112
  // reload resolves only after RootController has committed the new
84
113
  // generation. Read ownership now so failed transactions never make
85
114
  // newly discovered workspace packages observable to the watcher.
86
115
  this.#syncWatchRoots();
87
116
  const durationMs = Number((performance.now() - startedAt).toFixed(1));
88
- await this.options.onReload?.(plan, durationMs);
117
+ try {
118
+ await this.options.onReload?.(plan, durationMs);
119
+ }
120
+ catch (error) {
121
+ // A projection/observer cannot change an already committed reload.
122
+ try {
123
+ await this.options.onError(error);
124
+ }
125
+ catch {
126
+ // Diagnostic reporting is deliberately outside the outcome.
127
+ }
128
+ }
89
129
  }
90
130
  }
91
131
  this.#resolveWaiters();
@@ -105,7 +145,7 @@ export class HmrCoordinator {
105
145
  this.#draining = undefined;
106
146
  // A source may arrive after the loop observed an empty queue but before
107
147
  // this promise settled. Keep its waiter attached to a fresh transaction.
108
- if (this.#pending.size > 0)
148
+ if (!this.#closing && !this.#restartRequired && this.#pending.size > 0)
109
149
  this.#ensureDrain();
110
150
  }
111
151
  }
@@ -120,4 +160,37 @@ export class HmrCoordinator {
120
160
  #syncWatchRoots() {
121
161
  this.options.modules.updateWatchRoots?.(this.options.ownership().watchRoots());
122
162
  }
163
+ #notifyRestart(plan) {
164
+ // Restart is a committed control outcome. Invoke the observer without
165
+ // awaiting it so a Process Host may await RootHost.stop() without waiting
166
+ // on the HMR drain that is currently delivering this notification.
167
+ try {
168
+ void Promise.resolve(this.options.onRestartRequired(plan)).catch((error) => {
169
+ this.#reportDiagnostic(error);
170
+ });
171
+ }
172
+ catch (error) {
173
+ this.#reportDiagnostic(error);
174
+ }
175
+ }
176
+ #notifyPlan(plan) {
177
+ if (!this.options.onPlan)
178
+ return;
179
+ try {
180
+ void Promise.resolve(this.options.onPlan(plan)).catch((error) => {
181
+ this.#reportDiagnostic(error);
182
+ });
183
+ }
184
+ catch (error) {
185
+ this.#reportDiagnostic(error);
186
+ }
187
+ }
188
+ #reportDiagnostic(error) {
189
+ try {
190
+ void Promise.resolve(this.options.onError(error)).catch(() => undefined);
191
+ }
192
+ catch {
193
+ // Diagnostic reporting cannot change an invalidation outcome.
194
+ }
195
+ }
123
196
  }
@@ -0,0 +1,123 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "additionalProperties": false,
5
+ "properties": {
6
+ "http": { "type": "object", "additionalProperties": true, "description": "HTTP, Console, REST/RPC/SSE, and Webhook Host.", "x-descriptionZh": "HTTP、Console、REST/RPC/SSE 与 Webhook Host。" },
7
+ "database": { "type": "object", "additionalProperties": true, "description": "Database Host and dialect connection options.", "x-descriptionZh": "Database Host 与方言连接参数。" },
8
+ "ai": {
9
+ "type": "object",
10
+ "additionalProperties": true,
11
+ "description": "Providers, Agents, sessions, memory, tools, and execution security.",
12
+ "x-descriptionZh": "Provider、Agent、会话、记忆、工具与执行安全策略。",
13
+ "properties": {
14
+ "agent": {
15
+ "type": "object",
16
+ "additionalProperties": true,
17
+ "description": "Agent execution, queueing, tool, and model policies.",
18
+ "x-descriptionZh": "Agent 执行、排队、工具与模型策略。",
19
+ "properties": {
20
+ "inboundQueue": {
21
+ "type": "object",
22
+ "additionalProperties": true,
23
+ "description": "Inbound turn queue policy.",
24
+ "x-descriptionZh": "入站回合排队策略。",
25
+ "properties": {
26
+ "groupMode": {
27
+ "type": "string",
28
+ "enum": ["supersede", "fifo"],
29
+ "description": "Replace an older queued group turn, or process all turns in arrival order.",
30
+ "x-descriptionZh": "覆盖较早的群聊排队回合,或按到达顺序处理全部回合。"
31
+ }
32
+ }
33
+ },
34
+ "execSecurity": {
35
+ "type": "string",
36
+ "enum": ["deny", "allowlist", "full"],
37
+ "description": "Shell command security boundary.",
38
+ "x-descriptionZh": "Shell 命令安全边界。"
39
+ },
40
+ "execPreset": {
41
+ "type": "string",
42
+ "enum": ["readonly", "network", "development", "custom"],
43
+ "description": "Command allowlist preset used outside full mode.",
44
+ "x-descriptionZh": "非 full 模式使用的命令白名单预设。"
45
+ },
46
+ "execApprovalMode": { "type": "string", "enum": ["ask", "allow", "deny"], "description": "Approval policy for main Agent commands.", "x-descriptionZh": "主 Agent 命令的审批策略。" },
47
+ "subagentExecApprovalMode": { "type": "string", "enum": ["ask", "allow", "deny"], "description": "Approval policy for sub-Agent commands.", "x-descriptionZh": "子 Agent 命令的审批策略。" },
48
+ "workerExecApprovalMode": { "type": "string", "enum": ["ask", "allow", "deny"], "description": "Approval policy for worker commands.", "x-descriptionZh": "Worker 命令的审批策略。" },
49
+ "taskExecApprovalMode": { "type": "string", "enum": ["ask", "allow", "deny"], "description": "Approval policy for task commands.", "x-descriptionZh": "Task 命令的审批策略。" },
50
+ "toolExecution": {
51
+ "type": "string",
52
+ "enum": ["parallel", "sequential", "tiered"],
53
+ "description": "How tool calls in one model step are scheduled.",
54
+ "x-descriptionZh": "同一模型步骤中的工具调用调度方式。"
55
+ },
56
+ "modelSizeHint": {
57
+ "type": "string",
58
+ "enum": ["", "small", "medium", "large"],
59
+ "description": "Optional model-size hint; an empty string clears the hint.",
60
+ "x-descriptionZh": "可选模型尺寸提示;空字符串表示清除提示。"
61
+ },
62
+ "promptCacheRetention": {
63
+ "type": "string",
64
+ "enum": ["in_memory", "24h"],
65
+ "description": "Provider prompt-cache retention policy.",
66
+ "x-descriptionZh": "Provider Prompt Cache 保留策略。"
67
+ },
68
+ "steeringMode": {
69
+ "type": "string",
70
+ "enum": ["one-at-a-time", "all"],
71
+ "description": "Process steering messages one at a time or drain all pending messages together.",
72
+ "x-descriptionZh": "逐条处理 Steering 消息,或一次取出全部待处理消息。"
73
+ },
74
+ "followUpMode": {
75
+ "type": "string",
76
+ "enum": ["one-at-a-time", "all"],
77
+ "description": "Process follow-up messages one at a time or drain all pending messages together.",
78
+ "x-descriptionZh": "逐条处理 Follow-up 消息,或一次取出全部待处理消息。"
79
+ },
80
+ "outputSchema": {
81
+ "anyOf": [
82
+ { "type": "boolean" },
83
+ { "type": "string", "enum": ["segments"] },
84
+ { "type": "object" }
85
+ ],
86
+ "description": "Structured final-output mode: false for text, true or segments for canonical message segments, or a custom JSON Schema object.",
87
+ "x-descriptionZh": "结构化最终输出:false 表示文本,true 或 segments 表示规范消息段,也可传入自定义 JSON Schema 对象。"
88
+ },
89
+ "schedule": {
90
+ "type": "object",
91
+ "additionalProperties": true,
92
+ "description": "Unattended Schedule execution policy.",
93
+ "x-descriptionZh": "无人值守 Schedule 执行策略。",
94
+ "properties": {
95
+ "security": {
96
+ "type": "object",
97
+ "additionalProperties": true,
98
+ "description": "Schedule command security policy.",
99
+ "x-descriptionZh": "Schedule 命令安全策略。",
100
+ "properties": {
101
+ "execPreset": {
102
+ "type": "string",
103
+ "enum": ["readonly", "network"],
104
+ "description": "Schedule Jobs may use only the read-only or network preset.",
105
+ "x-descriptionZh": "Schedule Job 只能使用只读或网络预设。"
106
+ }
107
+ }
108
+ }
109
+ }
110
+ }
111
+ }
112
+ }
113
+ }
114
+ },
115
+ "mcp": { "type": "object", "additionalProperties": true, "description": "Expose Bot tools through an MCP Server.", "x-descriptionZh": "把 Bot 工具公开为 MCP Server。" },
116
+ "a2a": { "type": "object", "additionalProperties": true, "description": "A2A Agent Card, remote execution, and Workroom callbacks.", "x-descriptionZh": "A2A Agent Card、远程执行与 Workroom 回调。" },
117
+ "speech": { "type": "object", "additionalProperties": true, "description": "Speech-to-text and text-to-speech Host.", "x-descriptionZh": "语音识别与语音合成 Host。" },
118
+ "htmlRenderer": { "type": "object", "additionalProperties": true, "description": "HTML and image rendering options.", "x-descriptionZh": "HTML/图片渲染参数。" },
119
+ "assistant": { "type": "object", "additionalProperties": true, "description": "Scheduled jobs, event ingress, and failure notifications.", "x-descriptionZh": "调度任务、事件入口和失败通知。" },
120
+ "log_level": { "type": ["string", "number"], "description": "Runtime log level.", "x-descriptionZh": "Runtime 日志级别。" },
121
+ "plugin": { "type": "object", "description": "Root Plugin configuration; replaced by its project schema during composition.", "x-descriptionZh": "Root Plugin 配置;组合时由项目 Schema 替换。" }
122
+ }
123
+ }
@@ -8,6 +8,8 @@ import { type PrimaryConfig } from './primary-config.js';
8
8
  import type { RuntimeConfigDocument } from './config-composer.js';
9
9
  export type PluginConfigResolver = (node: PluginGraphNode) => unknown;
10
10
  export interface RootResourceContext {
11
+ /** Exact shadow generation being assembled; never inferred from a latest snapshot. */
12
+ readonly generation: number;
11
13
  readonly signal: AbortSignal;
12
14
  readonly resources: Scope;
13
15
  readonly lifecycle: DisposeStack;
@@ -36,13 +38,14 @@ export declare class PluginScopeAssembler {
36
38
  private readonly configResolver;
37
39
  private readonly environment;
38
40
  private readonly primaryConfigDocument;
41
+ private readonly generation;
39
42
  private readonly installResources?;
40
43
  private readonly isolation?;
41
44
  readonly scopes: Map<PluginId, Scope>;
42
45
  readonly tree: Map<PluginId, PluginNodeSnapshot>;
43
46
  readonly config: Map<PluginId, unknown>;
44
47
  readonly resources: Map<PluginId, ReadonlyMap<TokenId, unknown>>;
45
- constructor(modules: ModuleRuntime, configResolver: PluginConfigResolver, environment: RuntimeEnvironment, primaryConfigDocument: RuntimeConfigDocument, installResources?: RootResourceInstaller | undefined, environmentLayers?: EnvironmentLayers, seed?: PluginAssemblySeed, isolation?: IsolatedPluginRuntimePort | undefined);
48
+ constructor(modules: ModuleRuntime, configResolver: PluginConfigResolver, environment: RuntimeEnvironment, primaryConfigDocument: RuntimeConfigDocument, generation: number, installResources?: RootResourceInstaller | undefined, environmentLayers?: EnvironmentLayers, seed?: PluginAssemblySeed, isolation?: IsolatedPluginRuntimePort | undefined);
46
49
  removeSubtrees(roots: readonly PluginId[]): void;
47
50
  installSetupFeatureAliases(aliases: ReadonlyMap<string, FeatureId>): void;
48
51
  setupTree(node: PluginGraphNode, signal: AbortSignal): Promise<void>;
@@ -9,6 +9,7 @@ export class PluginScopeAssembler {
9
9
  configResolver;
10
10
  environment;
11
11
  primaryConfigDocument;
12
+ generation;
12
13
  installResources;
13
14
  isolation;
14
15
  scopes;
@@ -21,11 +22,12 @@ export class PluginScopeAssembler {
21
22
  #admission = createGenerationAdmissionGate();
22
23
  #envStores;
23
24
  #setupFeatureAliases = new Map();
24
- constructor(modules, configResolver, environment, primaryConfigDocument, installResources, environmentLayers = {}, seed, isolation) {
25
+ constructor(modules, configResolver, environment, primaryConfigDocument, generation, installResources, environmentLayers = {}, seed, isolation) {
25
26
  this.modules = modules;
26
27
  this.configResolver = configResolver;
27
28
  this.environment = environment;
28
29
  this.primaryConfigDocument = primaryConfigDocument;
30
+ this.generation = generation;
29
31
  this.installResources = installResources;
30
32
  this.isolation = isolation;
31
33
  this.#envStores = new EnvStoreFactory(environment, environmentLayers);
@@ -68,6 +70,7 @@ export class PluginScopeAssembler {
68
70
  const config = createPrimaryConfig(this.primaryConfigDocument, environment);
69
71
  scope.provide(primaryConfigToken, config);
70
72
  await this.installResources?.({
73
+ generation: this.generation,
71
74
  signal,
72
75
  resources: scope,
73
76
  lifecycle: scope.disposers,
@@ -8,6 +8,7 @@ import type { IsolatedPluginRuntimePort } from './isolation.js';
8
8
  import type { ModuleRuntime } from './module-runtime.js';
9
9
  import { type PluginConfigResolver, type RootResourceInstaller } from './plugin-scope-assembler.js';
10
10
  import { HmrCoordinator, type HmrCoordinatorOptions } from './hmr-coordinator.js';
11
+ import { type RuntimeGenerationState } from './runtime-generation.js';
11
12
  import { RootProcessRestartExecutor, type ProcessRestartAdapter } from './process-restart.js';
12
13
  import { SourceOwnershipIndex } from './source-ownership.js';
13
14
  export type { PluginConfigResolver, RootResourceContext, RootResourceInstaller, } from './plugin-scope-assembler.js';
@@ -27,7 +28,7 @@ export declare class RootRuntime {
27
28
  constructor(options: RootRuntimeOptions);
28
29
  get snapshot(): RuntimeSnapshot;
29
30
  get snapshots(): SnapshotReader;
30
- onGenerationCommit(listener: GenerationCommitListener): () => void;
31
+ onGenerationCommit(listener: GenerationCommitListener<RuntimeGenerationState>): () => void;
31
32
  get sourceOwnership(): SourceOwnershipIndex;
32
33
  start(): Promise<RuntimeSnapshot>;
33
34
  reload(target?: PluginId | string): Promise<RuntimeSnapshot>;
@@ -14,6 +14,7 @@ import { NodePackageResolver } from './package-resolver.js';
14
14
  import { PluginScopeAssembler, } from './plugin-scope-assembler.js';
15
15
  import { ProjectGraphService, } from './project-graph.js';
16
16
  import { HmrCoordinator } from './hmr-coordinator.js';
17
+ import { prepareRuntimeGeneration, } from './runtime-generation.js';
17
18
  import { RootProcessRestartExecutor, } from './process-restart.js';
18
19
  import { SlotGenerationPreparer } from './slot-generation-preparer.js';
19
20
  import { capabilityDeltaFromSlots, capabilityDeltaIds, ConventionCapabilityDeltaResolver, filterCapabilityDelta, mergeCapabilityDeltas, } from './convention-capability-delta.js';
@@ -33,8 +34,6 @@ export class RootRuntime {
33
34
  #configDocument;
34
35
  #installResources;
35
36
  #isolation;
36
- #ownership = SourceOwnershipIndex.empty();
37
- #model;
38
37
  #configPatchTail = Promise.resolve();
39
38
  #stopResult;
40
39
  #controller;
@@ -51,7 +50,7 @@ export class RootRuntime {
51
50
  this.#configDocument = structuredClone(options.config ?? {});
52
51
  this.#installResources = options.installResources;
53
52
  this.#isolation = options.isolation;
54
- this.#controller = new RootController(emptyState(), options.onControlError);
53
+ this.#controller = new RootController(emptyState(), options.onControlError, Object.freeze({ ownership: SourceOwnershipIndex.empty() }));
55
54
  }
56
55
  get snapshot() {
57
56
  return this.#controller.snapshot;
@@ -63,7 +62,7 @@ export class RootRuntime {
63
62
  return this.#controller.onGenerationCommit(listener);
64
63
  }
65
64
  get sourceOwnership() {
66
- return this.#ownership;
65
+ return this.#controller.committed.state.ownership;
67
66
  }
68
67
  async start() {
69
68
  if (this.#configPort) {
@@ -71,21 +70,15 @@ export class RootRuntime {
71
70
  this.#configSnapshot = snapshot;
72
71
  this.#configDocument = structuredClone(snapshot.document);
73
72
  }
74
- let prepared;
75
73
  const snapshot = await this.#controller.start(async (current, signal) => {
76
- prepared = await this.#prepare(current, signal);
77
- return prepared.generation;
74
+ return (await this.#prepare(current, signal)).generation;
78
75
  });
79
- this.#accept(requirePrepared(prepared));
80
76
  return snapshot;
81
77
  }
82
78
  async reload(target = rootPluginId()) {
83
- let prepared;
84
79
  const snapshot = await this.#controller.reload(target, async (current, signal) => {
85
- prepared = await this.#prepare(current, signal);
86
- return prepared.generation;
80
+ return (await this.#prepare(current, signal)).generation;
87
81
  });
88
- this.#accept(requirePrepared(prepared));
89
82
  return snapshot;
90
83
  }
91
84
  patchConfig(patches) {
@@ -98,7 +91,7 @@ export class RootRuntime {
98
91
  return new HmrCoordinator({
99
92
  ...options,
100
93
  modules: this.#modules,
101
- ownership: () => this.#ownership,
94
+ ownership: () => this.sourceOwnership,
102
95
  runtime: {
103
96
  reload: async (plan) => {
104
97
  const result = await this.#reloadPlan(plan);
@@ -125,9 +118,9 @@ export class RootRuntime {
125
118
  return result;
126
119
  }
127
120
  async #reloadPlan(plan) {
128
- let prepared;
129
121
  let restart;
130
122
  const snapshot = await this.#controller.reload(plan.subtrees[0] ?? plan.slots[0] ?? rootPluginId(), async (current, signal) => {
123
+ let prepared;
131
124
  const resolved = await this.#resolveCapabilityDelta(current, plan);
132
125
  const effective = resolved.plan;
133
126
  if (this.#model && effective.manifestSources.length > 0) {
@@ -154,13 +147,10 @@ export class RootRuntime {
154
147
  });
155
148
  if (restart)
156
149
  return restart;
157
- if (prepared)
158
- this.#accept(prepared);
159
150
  return snapshot;
160
151
  }
161
- #accept(prepared) {
162
- this.#ownership = prepared.ownership;
163
- this.#model = prepared.model;
152
+ get #model() {
153
+ return this.#controller.committed.state.model;
164
154
  }
165
155
  async #resolveCapabilityDelta(current, plan) {
166
156
  const known = capabilityDeltaFromSlots(current, plan.slots);
@@ -185,7 +175,7 @@ export class RootRuntime {
185
175
  if (plan.subtrees.length === 0 || plan.subtrees.includes(rootPluginId()))
186
176
  return false;
187
177
  return plan.changed.every((source) => {
188
- const records = this.#ownership.recordsFor(source);
178
+ const records = this.sourceOwnership.recordsFor(source);
189
179
  return records.length > 0 && records.every((record) => record.role === 'plugin' || record.role === 'schema');
190
180
  });
191
181
  }
@@ -247,10 +237,10 @@ export class RootRuntime {
247
237
  await this.#refreshConfigDocument();
248
238
  const currentDocument = requireConfigDocument(this.#configDocument);
249
239
  let plan;
250
- let prepared;
251
240
  let documentTransaction;
252
241
  let committedDocument;
253
242
  const snapshot = await this.#controller.reload(rootPluginId(), async (current, signal) => {
243
+ let prepared;
254
244
  signal.throwIfAborted();
255
245
  const resolver = await NodePackageResolver.create(this.#projectRoot);
256
246
  const graph = await new ProjectGraphService(resolver).inspect(this.#projectRoot);
@@ -305,8 +295,6 @@ export class RootRuntime {
305
295
  }
306
296
  });
307
297
  const completed = requireConfigPatchPlan(plan);
308
- if (prepared)
309
- this.#accept(prepared);
310
298
  this.#configDocument = completed.candidate;
311
299
  if (committedDocument)
312
300
  this.#configSnapshot = committedDocument;
@@ -406,7 +394,7 @@ class GenerationAssembler {
406
394
  this.environmentLayers = environmentLayers;
407
395
  this.isolation = isolation;
408
396
  this.#host = new NodeDiscoveryHost(modules);
409
- this.#plugins = new PluginScopeAssembler(modules, configResolver, environment, primaryConfigDocument, installResources, environmentLayers, undefined, isolation);
397
+ this.#plugins = new PluginScopeAssembler(modules, configResolver, environment, primaryConfigDocument, generation, installResources, environmentLayers, undefined, isolation);
410
398
  }
411
399
  async prepare(signal) {
412
400
  signal.throwIfAborted();
@@ -426,25 +414,21 @@ class GenerationAssembler {
426
414
  const snapshot = createSnapshotView(this.generation, state);
427
415
  const ownership = SourceOwnershipIndex.fromGeneration(this.graph, snapshot, this.#featureIdsByPackageRoot);
428
416
  const assets = GenerationAssets.create(this.#plugins.createdScopeDisposers(), this.#projectionDisposers);
429
- return {
430
- generation: {
431
- snapshot: state,
432
- dispose: () => assets.dispose(),
433
- handoff: composeGenerationHandoffs(this.#plugins.generationHandoff(), projected.handoff),
434
- },
435
- ownership,
436
- model: {
437
- graph: this.graph,
438
- providers: new Map(this.#catalog.values().map((provider) => [provider.id, provider])),
439
- rootsByFeature: new Map([...this.#rootsByFeature].map(([feature, roots]) => [
440
- feature,
441
- Object.freeze([...roots]),
442
- ])),
443
- featureIdsByPackageRoot: new Map(this.#featureIdsByPackageRoot),
444
- scopes: new Map(this.#plugins.scopes),
445
- assets,
446
- },
447
- };
417
+ return prepareRuntimeGeneration({
418
+ snapshot: state,
419
+ dispose: () => assets.dispose(),
420
+ handoff: composeGenerationHandoffs(this.#plugins.generationHandoff(), projected.handoff),
421
+ }, ownership, {
422
+ graph: this.graph,
423
+ providers: new Map(this.#catalog.values().map((provider) => [provider.id, provider])),
424
+ rootsByFeature: new Map([...this.#rootsByFeature].map(([feature, roots]) => [
425
+ feature,
426
+ Object.freeze([...roots]),
427
+ ])),
428
+ featureIdsByPackageRoot: new Map(this.#featureIdsByPackageRoot),
429
+ scopes: new Map(this.#plugins.scopes),
430
+ assets,
431
+ });
448
432
  }
449
433
  catch (error) {
450
434
  await disposePreparedParts(this.#plugins.createdScopeDisposers().map(([, dispose]) => dispose), [...this.#projectionDisposers.values()], error);
@@ -507,11 +491,6 @@ function emptyState() {
507
491
  projections: new Map(),
508
492
  };
509
493
  }
510
- function requirePrepared(prepared) {
511
- if (!prepared)
512
- throw new Error('RootController committed without a prepared generation');
513
- return prepared;
514
- }
515
494
  function isProcessPlan(value) {
516
495
  return 'kind' in value && value.kind === 'process';
517
496
  }
@@ -11,8 +11,12 @@ export interface RuntimeGenerationModel {
11
11
  readonly scopes: ReadonlyMap<PluginId, Scope>;
12
12
  readonly assets: GenerationAssets;
13
13
  }
14
- export interface PreparedRuntimeGeneration {
15
- readonly generation: PreparedGeneration;
14
+ /** Sidecar state that must never be observed from a different generation. */
15
+ export interface RuntimeGenerationState {
16
16
  readonly ownership: SourceOwnershipIndex;
17
- readonly model: RuntimeGenerationModel;
17
+ readonly model?: RuntimeGenerationModel;
18
+ }
19
+ export interface PreparedRuntimeGeneration {
20
+ readonly generation: PreparedGeneration<RuntimeGenerationState>;
18
21
  }
22
+ export declare function prepareRuntimeGeneration(generation: PreparedGeneration, ownership: SourceOwnershipIndex, model: RuntimeGenerationModel): PreparedRuntimeGeneration;
@@ -1 +1,8 @@
1
- export {};
1
+ export function prepareRuntimeGeneration(generation, ownership, model) {
2
+ return Object.freeze({
3
+ generation: Object.freeze({
4
+ ...generation,
5
+ state: Object.freeze({ ownership, model }),
6
+ }),
7
+ });
8
+ }
@@ -1,6 +1,6 @@
1
1
  import { type CapabilityId, type RuntimeSnapshot } from '@zhin.js/plugin-runtime';
2
2
  import type { ModuleRuntime } from './module-runtime.js';
3
- import type { PreparedRuntimeGeneration, RuntimeGenerationModel } from './runtime-generation.js';
3
+ import { type PreparedRuntimeGeneration, type RuntimeGenerationModel } from './runtime-generation.js';
4
4
  import { type CapabilityDelta } from './convention-capability-delta.js';
5
5
  export declare class SlotGenerationPreparer {
6
6
  private readonly modules;
@@ -2,6 +2,7 @@ import { DisposeStack, createSnapshotView, } from '@zhin.js/plugin-runtime';
2
2
  import { FeatureDiscovery } from '@zhin.js/feature-kit';
3
3
  import { FeatureProjector, composeGenerationHandoffs } from './feature-projector.js';
4
4
  import { NodeDiscoveryHost } from './node-discovery-host.js';
5
+ import { prepareRuntimeGeneration, } from './runtime-generation.js';
5
6
  import { SourceOwnershipIndex } from './source-ownership.js';
6
7
  import { capabilityDeltaFromSlots, } from './convention-capability-delta.js';
7
8
  export class SlotGenerationPreparer {
@@ -48,15 +49,11 @@ export class SlotGenerationPreparer {
48
49
  const snapshot = createSnapshotView(current.generation + 1, projected.state);
49
50
  const ownership = SourceOwnershipIndex.fromGeneration(this.model.graph, snapshot, this.model.featureIdsByPackageRoot);
50
51
  const assets = this.model.assets.replaceProjections(selectedByFeature.keys(), projected.disposers);
51
- return {
52
- generation: {
53
- snapshot: projected.state,
54
- dispose: () => assets.dispose(),
55
- handoff: composeGenerationHandoffs(projected.handoff),
56
- },
57
- ownership,
58
- model: { ...this.model, assets },
59
- };
52
+ return prepareRuntimeGeneration({
53
+ snapshot: projected.state,
54
+ dispose: () => assets.dispose(),
55
+ handoff: composeGenerationHandoffs(projected.handoff),
56
+ }, ownership, { ...this.model, assets });
60
57
  }
61
58
  catch (error) {
62
59
  await disposeProjections(projected.disposers.values(), error);
@@ -6,7 +6,7 @@ import type { IsolatedPluginRuntimePort } from './isolation.js';
6
6
  import type { ModuleRuntime } from './module-runtime.js';
7
7
  import { type PluginConfigResolver, type RootResourceInstaller } from './plugin-scope-assembler.js';
8
8
  import type { ProjectGraph } from './project-graph.js';
9
- import type { PreparedRuntimeGeneration, RuntimeGenerationModel } from './runtime-generation.js';
9
+ import { type PreparedRuntimeGeneration, type RuntimeGenerationModel } from './runtime-generation.js';
10
10
  export declare class SubtreeTopologyChangedError extends Error {
11
11
  constructor(message: string);
12
12
  }
@@ -3,6 +3,7 @@ import { FeatureDiscovery } from '@zhin.js/feature-kit';
3
3
  import { FeatureProjector, composeGenerationHandoffs } from './feature-projector.js';
4
4
  import { NodeDiscoveryHost } from './node-discovery-host.js';
5
5
  import { PluginScopeAssembler, } from './plugin-scope-assembler.js';
6
+ import { prepareRuntimeGeneration, } from './runtime-generation.js';
6
7
  import { SourceOwnershipIndex } from './source-ownership.js';
7
8
  import { addCapabilitySlot, featureSetupAliases, mergeSetupCapabilities, } from './setup-capabilities.js';
8
9
  export class SubtreeTopologyChangedError extends Error {
@@ -37,7 +38,7 @@ export class SubtreeGenerationPreparer {
37
38
  signal.throwIfAborted();
38
39
  const nodes = indexGraph(this.graph);
39
40
  assertCompatibleTopology(indexGraph(this.model.graph), nodes, roots);
40
- const plugins = new PluginScopeAssembler(this.modules, this.configResolver, this.environment, this.primaryConfigDocument, this.installResources, this.environmentLayers, {
41
+ const plugins = new PluginScopeAssembler(this.modules, this.configResolver, this.environment, this.primaryConfigDocument, current.generation + 1, this.installResources, this.environmentLayers, {
41
42
  scopes: this.model.scopes,
42
43
  tree: current.tree,
43
44
  config: current.config,
@@ -86,20 +87,16 @@ export class SubtreeGenerationPreparer {
86
87
  // Local prepare still commits a complete immutable generation. The
87
88
  // replacement map controls lifetime ownership, not snapshot granularity.
88
89
  const assets = this.model.assets.replaceScopes([...nodes.keys()], replacements, projectionDisposers);
89
- return {
90
- generation: {
91
- snapshot: projected.state,
92
- dispose: () => assets.dispose(),
93
- handoff: composeGenerationHandoffs(plugins.generationHandoff(), projected.handoff),
94
- },
95
- ownership,
96
- model: {
97
- ...this.model,
98
- graph: this.graph,
99
- scopes: new Map(plugins.scopes),
100
- assets,
101
- },
102
- };
90
+ return prepareRuntimeGeneration({
91
+ snapshot: projected.state,
92
+ dispose: () => assets.dispose(),
93
+ handoff: composeGenerationHandoffs(plugins.generationHandoff(), projected.handoff),
94
+ }, ownership, {
95
+ ...this.model,
96
+ graph: this.graph,
97
+ scopes: new Map(plugins.scopes),
98
+ assets,
99
+ });
103
100
  }
104
101
  catch (error) {
105
102
  await rollback(plugins.createdScopeDisposers().map(([, dispose]) => dispose), [...projectionDisposers.values()], error);
@@ -6,7 +6,7 @@ import type { IsolatedPluginRuntimePort } from './isolation.js';
6
6
  import type { ModuleRuntime } from './module-runtime.js';
7
7
  import { type PluginConfigResolver, type RootResourceInstaller } from './plugin-scope-assembler.js';
8
8
  import type { ProjectGraph } from './project-graph.js';
9
- import type { PreparedRuntimeGeneration, RuntimeGenerationModel } from './runtime-generation.js';
9
+ import { type PreparedRuntimeGeneration, type RuntimeGenerationModel } from './runtime-generation.js';
10
10
  import { type CapabilityDelta } from './convention-capability-delta.js';
11
11
  /** Runtime-local invalidation to commit alongside an ABI-safe manifest change. */
12
12
  export interface TopologyRuntimeDelta {
@@ -5,6 +5,7 @@ import { FeatureCatalog, FeatureDiscovery, } from '@zhin.js/feature-kit';
5
5
  import { FeatureProjector, composeGenerationHandoffs } from './feature-projector.js';
6
6
  import { NodeDiscoveryHost } from './node-discovery-host.js';
7
7
  import { PluginScopeAssembler, } from './plugin-scope-assembler.js';
8
+ import { prepareRuntimeGeneration, } from './runtime-generation.js';
8
9
  import { SourceOwnershipIndex } from './source-ownership.js';
9
10
  import { capabilityDeltaFromSlots, capabilityDeltaIds, } from './convention-capability-delta.js';
10
11
  import { addCapabilitySlot, featureSetupAliases, mergeSetupCapabilities, } from './setup-capabilities.js';
@@ -46,7 +47,7 @@ export class TopologyGenerationPreparer {
46
47
  if (!plan.changed && subtreeRoots.length === 0 && selected.size === 0)
47
48
  return undefined;
48
49
  const featureTopology = await this.#loadFeatureTopology(plan);
49
- const plugins = new PluginScopeAssembler(this.modules, this.configResolver, this.environment, this.primaryConfigDocument, this.installResources, this.environmentLayers, {
50
+ const plugins = new PluginScopeAssembler(this.modules, this.configResolver, this.environment, this.primaryConfigDocument, current.generation + 1, this.installResources, this.environmentLayers, {
50
51
  scopes: this.model.scopes,
51
52
  tree: current.tree,
52
53
  config: current.config,
@@ -91,22 +92,18 @@ export class TopologyGenerationPreparer {
91
92
  const ownership = SourceOwnershipIndex.fromGeneration(this.graph, snapshot, featureTopology.featureIdsByPackageRoot);
92
93
  const replacements = new Map(plugins.createdScopeDisposers());
93
94
  const assets = this.model.assets.replaceScopes(graphOrder(this.graph), replacements, projectionDisposers);
94
- return {
95
- generation: {
96
- snapshot: projected.state,
97
- dispose: () => assets.dispose(),
98
- handoff: composeGenerationHandoffs(plugins.generationHandoff(), projected.handoff),
99
- },
100
- ownership,
101
- model: {
102
- graph: this.graph,
103
- providers: featureTopology.providers,
104
- rootsByFeature: featureTopology.rootsByFeature,
105
- featureIdsByPackageRoot: featureTopology.featureIdsByPackageRoot,
106
- scopes: new Map(plugins.scopes),
107
- assets,
108
- },
109
- };
95
+ return prepareRuntimeGeneration({
96
+ snapshot: projected.state,
97
+ dispose: () => assets.dispose(),
98
+ handoff: composeGenerationHandoffs(plugins.generationHandoff(), projected.handoff),
99
+ }, ownership, {
100
+ graph: this.graph,
101
+ providers: featureTopology.providers,
102
+ rootsByFeature: featureTopology.rootsByFeature,
103
+ featureIdsByPackageRoot: featureTopology.featureIdsByPackageRoot,
104
+ scopes: new Map(plugins.scopes),
105
+ assets,
106
+ });
110
107
  }
111
108
  catch (error) {
112
109
  await rollback(plugins.createdScopeDisposers().map(([, dispose]) => dispose), [...projectionDisposers.values()], error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/runtime",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "description": "Static Plugin graph, generation transaction and HMR Root runtime for Zhin.js",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -18,22 +18,23 @@
18
18
  "dependencies": {
19
19
  "ajv": "8.18.0",
20
20
  "semver": "7.8.5",
21
- "@zhin.js/feature-kit": "1.0.11",
22
- "@zhin.js/plugin-runtime": "1.1.6"
21
+ "@zhin.js/plugin-runtime": "1.1.8",
22
+ "@zhin.js/feature-kit": "1.0.13"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/node": "^26.1.2",
26
26
  "typescript": "^6.0.3",
27
- "@zhin.js/adapter": "1.1.10",
28
- "@zhin.js/agent-feature": "1.0.11",
29
- "@zhin.js/command": "1.0.14",
30
- "@zhin.js/component": "1.0.11",
31
- "@zhin.js/layout": "1.0.11",
32
- "@zhin.js/mcp-feature": "1.0.11",
33
- "@zhin.js/middleware": "1.0.11",
34
- "@zhin.js/page": "1.0.11",
35
- "@zhin.js/skill": "1.0.11",
36
- "@zhin.js/tool": "1.0.11"
27
+ "@zhin.js/agent-feature": "1.0.13",
28
+ "@zhin.js/adapter": "1.2.1",
29
+ "@zhin.js/command": "1.0.16",
30
+ "@zhin.js/component": "1.0.13",
31
+ "@zhin.js/layout": "1.0.13",
32
+ "@zhin.js/mcp-feature": "1.0.13",
33
+ "@zhin.js/middleware": "1.0.13",
34
+ "@zhin.js/prompt-section": "0.0.1",
35
+ "@zhin.js/skill": "1.0.13",
36
+ "@zhin.js/page": "1.0.13",
37
+ "@zhin.js/tool": "1.0.13"
37
38
  },
38
39
  "repository": {
39
40
  "type": "git",