@zhin.js/runtime 1.1.3 → 1.1.5

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,6 +1,637 @@
1
- import type { RuntimeConfigDocument } from '@zhin.js/plugin-runtime';
2
- import type { PluginId } from '@zhin.js/plugin-runtime';
1
+ import type { RuntimeConfigDocument, PluginId } from '@zhin.js/plugin-runtime';
3
2
  import type { ProjectGraph } from './project-graph.js';
3
+ export declare const HOST_CONFIG_SCHEMA: {
4
+ $schema: string;
5
+ type: string;
6
+ additionalProperties: boolean;
7
+ properties: {
8
+ http: {
9
+ type: string;
10
+ additionalProperties: boolean;
11
+ description: string;
12
+ "x-descriptionZh": string;
13
+ properties: {
14
+ readiness: {
15
+ type: string;
16
+ additionalProperties: boolean;
17
+ description: string;
18
+ "x-descriptionZh": string;
19
+ properties: {
20
+ database: {
21
+ type: string;
22
+ default: boolean;
23
+ description: string;
24
+ "x-descriptionZh": string;
25
+ };
26
+ endpoints: {
27
+ type: string;
28
+ uniqueItems: boolean;
29
+ description: string;
30
+ "x-descriptionZh": string;
31
+ items: {
32
+ type: string;
33
+ additionalProperties: boolean;
34
+ required: string[];
35
+ properties: {
36
+ owner: {
37
+ type: string;
38
+ pattern: string;
39
+ };
40
+ name: {
41
+ type: string;
42
+ minLength: number;
43
+ pattern: string;
44
+ };
45
+ };
46
+ };
47
+ };
48
+ agents: {
49
+ type: string;
50
+ uniqueItems: boolean;
51
+ items: {
52
+ type: string;
53
+ minLength: number;
54
+ };
55
+ description: string;
56
+ "x-descriptionZh": string;
57
+ };
58
+ };
59
+ };
60
+ };
61
+ };
62
+ database: {
63
+ type: string;
64
+ additionalProperties: boolean;
65
+ description: string;
66
+ "x-descriptionZh": string;
67
+ };
68
+ ai: {
69
+ type: string;
70
+ additionalProperties: boolean;
71
+ description: string;
72
+ "x-descriptionZh": string;
73
+ properties: {
74
+ decisions: {
75
+ type: string;
76
+ additionalProperties: boolean;
77
+ required: string[];
78
+ description: string;
79
+ "x-descriptionZh": string;
80
+ properties: {
81
+ provider: {
82
+ type: string;
83
+ pattern: string;
84
+ description: string;
85
+ "x-descriptionZh": string;
86
+ };
87
+ skills: {
88
+ $ref: string;
89
+ };
90
+ tools: {
91
+ $ref: string;
92
+ };
93
+ memory: {
94
+ $ref: string;
95
+ };
96
+ agents: {
97
+ $ref: string;
98
+ };
99
+ approval: {
100
+ $ref: string;
101
+ };
102
+ };
103
+ };
104
+ workroom: {
105
+ type: string;
106
+ additionalProperties: boolean;
107
+ description: string;
108
+ "x-descriptionZh": string;
109
+ properties: {
110
+ trustedPackPublishers: {
111
+ type: string;
112
+ items: {
113
+ type: string;
114
+ minLength: number;
115
+ };
116
+ uniqueItems: boolean;
117
+ description: string;
118
+ "x-descriptionZh": string;
119
+ };
120
+ disclosure: {
121
+ type: string;
122
+ additionalProperties: boolean;
123
+ description: string;
124
+ "x-descriptionZh": string;
125
+ properties: {
126
+ tenantId: {
127
+ type: string;
128
+ minLength: number;
129
+ };
130
+ modelProviders: {
131
+ type: string;
132
+ additionalProperties: {
133
+ type: string;
134
+ additionalProperties: boolean;
135
+ required: string[];
136
+ properties: {
137
+ endpoint: {
138
+ type: string;
139
+ minLength: number;
140
+ };
141
+ owner: {
142
+ type: string;
143
+ minLength: number;
144
+ };
145
+ trustDomain: {
146
+ type: string;
147
+ minLength: number;
148
+ };
149
+ processingRegions: {
150
+ type: string;
151
+ minItems: number;
152
+ uniqueItems: boolean;
153
+ items: {
154
+ type: string;
155
+ minLength: number;
156
+ };
157
+ };
158
+ maxConfidentiality: {
159
+ type: string;
160
+ enum: string[];
161
+ };
162
+ external: {
163
+ type: string;
164
+ };
165
+ noTraining: {
166
+ type: string;
167
+ };
168
+ loggingMode: {
169
+ type: string;
170
+ enum: string[];
171
+ };
172
+ maximumRetentionSeconds: {
173
+ type: string;
174
+ minimum: number;
175
+ };
176
+ allowsRedisclosure: {
177
+ type: string;
178
+ };
179
+ supportsDeletion: {
180
+ type: string;
181
+ };
182
+ };
183
+ };
184
+ };
185
+ };
186
+ };
187
+ };
188
+ };
189
+ agent: {
190
+ type: string;
191
+ additionalProperties: boolean;
192
+ description: string;
193
+ "x-descriptionZh": string;
194
+ properties: {
195
+ inboundQueue: {
196
+ type: string;
197
+ additionalProperties: boolean;
198
+ description: string;
199
+ "x-descriptionZh": string;
200
+ properties: {
201
+ groupMode: {
202
+ type: string;
203
+ enum: string[];
204
+ description: string;
205
+ "x-descriptionZh": string;
206
+ };
207
+ };
208
+ };
209
+ execSecurity: {
210
+ type: string;
211
+ enum: string[];
212
+ description: string;
213
+ "x-descriptionZh": string;
214
+ };
215
+ execPreset: {
216
+ type: string;
217
+ enum: string[];
218
+ description: string;
219
+ "x-descriptionZh": string;
220
+ };
221
+ execApprovalMode: {
222
+ type: string;
223
+ enum: string[];
224
+ description: string;
225
+ "x-descriptionZh": string;
226
+ };
227
+ subagentExecApprovalMode: {
228
+ type: string;
229
+ enum: string[];
230
+ description: string;
231
+ "x-descriptionZh": string;
232
+ };
233
+ workerExecApprovalMode: {
234
+ type: string;
235
+ enum: string[];
236
+ description: string;
237
+ "x-descriptionZh": string;
238
+ };
239
+ taskExecApprovalMode: {
240
+ type: string;
241
+ enum: string[];
242
+ description: string;
243
+ "x-descriptionZh": string;
244
+ };
245
+ toolExecution: {
246
+ type: string;
247
+ enum: string[];
248
+ description: string;
249
+ "x-descriptionZh": string;
250
+ };
251
+ modelSizeHint: {
252
+ type: string;
253
+ enum: string[];
254
+ description: string;
255
+ "x-descriptionZh": string;
256
+ };
257
+ promptCacheRetention: {
258
+ type: string;
259
+ enum: string[];
260
+ description: string;
261
+ "x-descriptionZh": string;
262
+ };
263
+ steeringMode: {
264
+ type: string;
265
+ enum: string[];
266
+ description: string;
267
+ "x-descriptionZh": string;
268
+ };
269
+ followUpMode: {
270
+ type: string;
271
+ enum: string[];
272
+ description: string;
273
+ "x-descriptionZh": string;
274
+ };
275
+ outputSchema: {
276
+ anyOf: ({
277
+ type: string;
278
+ enum?: undefined;
279
+ } | {
280
+ type: string;
281
+ enum: string[];
282
+ })[];
283
+ description: string;
284
+ "x-descriptionZh": string;
285
+ };
286
+ schedule: {
287
+ type: string;
288
+ additionalProperties: boolean;
289
+ description: string;
290
+ "x-descriptionZh": string;
291
+ properties: {
292
+ security: {
293
+ type: string;
294
+ additionalProperties: boolean;
295
+ description: string;
296
+ "x-descriptionZh": string;
297
+ properties: {
298
+ execPreset: {
299
+ type: string;
300
+ enum: string[];
301
+ description: string;
302
+ "x-descriptionZh": string;
303
+ };
304
+ };
305
+ };
306
+ };
307
+ };
308
+ };
309
+ };
310
+ };
311
+ };
312
+ mcp: {
313
+ type: string;
314
+ additionalProperties: boolean;
315
+ description: string;
316
+ "x-descriptionZh": string;
317
+ };
318
+ a2a: {
319
+ type: string;
320
+ additionalProperties: boolean;
321
+ description: string;
322
+ "x-descriptionZh": string;
323
+ };
324
+ speech: {
325
+ type: string;
326
+ additionalProperties: boolean;
327
+ description: string;
328
+ "x-descriptionZh": string;
329
+ };
330
+ htmlRenderer: {
331
+ type: string;
332
+ additionalProperties: boolean;
333
+ description: string;
334
+ "x-descriptionZh": string;
335
+ properties: {
336
+ width: {
337
+ type: string;
338
+ minimum: number;
339
+ maximum: number;
340
+ description: string;
341
+ "x-descriptionZh": string;
342
+ };
343
+ defaultWidth: {
344
+ type: string;
345
+ minimum: number;
346
+ maximum: number;
347
+ description: string;
348
+ "x-descriptionZh": string;
349
+ };
350
+ viewport: {
351
+ type: string;
352
+ additionalProperties: boolean;
353
+ description: string;
354
+ "x-descriptionZh": string;
355
+ properties: {
356
+ width: {
357
+ type: string;
358
+ minimum: number;
359
+ maximum: number;
360
+ description: string;
361
+ "x-descriptionZh": string;
362
+ };
363
+ height: {
364
+ type: string;
365
+ minimum: number;
366
+ maximum: number;
367
+ description: string;
368
+ "x-descriptionZh": string;
369
+ };
370
+ };
371
+ };
372
+ backgroundColor: {
373
+ type: string;
374
+ description: string;
375
+ "x-descriptionZh": string;
376
+ };
377
+ defaultBackgroundColor: {
378
+ type: string;
379
+ description: string;
380
+ "x-descriptionZh": string;
381
+ };
382
+ scale: {
383
+ type: string;
384
+ minimum: number;
385
+ maximum: number;
386
+ description: string;
387
+ "x-descriptionZh": string;
388
+ };
389
+ type: {
390
+ type: string;
391
+ enum: string[];
392
+ description: string;
393
+ "x-descriptionZh": string;
394
+ };
395
+ quality: {
396
+ type: string;
397
+ minimum: number;
398
+ maximum: number;
399
+ description: string;
400
+ "x-descriptionZh": string;
401
+ };
402
+ timeout: {
403
+ type: string;
404
+ minimum: number;
405
+ maximum: number;
406
+ description: string;
407
+ "x-descriptionZh": string;
408
+ };
409
+ waitUntil: {
410
+ type: string;
411
+ enum: string[];
412
+ description: string;
413
+ "x-descriptionZh": string;
414
+ };
415
+ fontFamily: {
416
+ type: string;
417
+ description: string;
418
+ "x-descriptionZh": string;
419
+ };
420
+ maxImageHeight: {
421
+ type: string;
422
+ minimum: number;
423
+ maximum: number;
424
+ description: string;
425
+ "x-descriptionZh": string;
426
+ };
427
+ sliceCompression: {
428
+ type: string;
429
+ minimum: number;
430
+ maximum: number;
431
+ description: string;
432
+ "x-descriptionZh": string;
433
+ };
434
+ allowFileAccess: {
435
+ type: string;
436
+ description: string;
437
+ "x-descriptionZh": string;
438
+ };
439
+ takeOverHtmlSegments: {
440
+ type: string;
441
+ description: string;
442
+ "x-descriptionZh": string;
443
+ };
444
+ cacheDir: {
445
+ type: string;
446
+ description: string;
447
+ "x-descriptionZh": string;
448
+ };
449
+ cacheMaxBytes: {
450
+ type: string;
451
+ minimum: number;
452
+ description: string;
453
+ "x-descriptionZh": string;
454
+ };
455
+ userAgent: {
456
+ type: string;
457
+ description: string;
458
+ "x-descriptionZh": string;
459
+ };
460
+ idleTimeoutMs: {
461
+ type: string;
462
+ minimum: number;
463
+ maximum: number;
464
+ description: string;
465
+ "x-descriptionZh": string;
466
+ };
467
+ logStats: {
468
+ type: string;
469
+ description: string;
470
+ "x-descriptionZh": string;
471
+ };
472
+ mode: {
473
+ type: string;
474
+ enum: string[];
475
+ description: string;
476
+ "x-descriptionZh": string;
477
+ };
478
+ aiTextAsImage: {
479
+ anyOf: ({
480
+ type: string;
481
+ additionalProperties?: undefined;
482
+ description?: undefined;
483
+ "x-descriptionZh"?: undefined;
484
+ properties?: undefined;
485
+ } | {
486
+ type: string;
487
+ additionalProperties: boolean;
488
+ description: string;
489
+ "x-descriptionZh": string;
490
+ properties: {
491
+ enabled: {
492
+ type: string;
493
+ description: string;
494
+ "x-descriptionZh": string;
495
+ };
496
+ onlyAdapters: {
497
+ type: string;
498
+ items: {
499
+ type: string;
500
+ minLength: number;
501
+ };
502
+ uniqueItems: boolean;
503
+ description: string;
504
+ "x-descriptionZh": string;
505
+ };
506
+ minLength: {
507
+ type: string;
508
+ minimum: number;
509
+ description: string;
510
+ "x-descriptionZh": string;
511
+ };
512
+ maxLength: {
513
+ type: string;
514
+ minimum: number;
515
+ description: string;
516
+ "x-descriptionZh": string;
517
+ };
518
+ skipIfRich: {
519
+ type: string;
520
+ description: string;
521
+ "x-descriptionZh": string;
522
+ };
523
+ width: {
524
+ type: string;
525
+ minimum: number;
526
+ maximum: number;
527
+ description: string;
528
+ "x-descriptionZh": string;
529
+ };
530
+ height: {
531
+ type: string;
532
+ minimum: number;
533
+ maximum: number;
534
+ description: string;
535
+ "x-descriptionZh": string;
536
+ };
537
+ backgroundColor: {
538
+ type: string;
539
+ description: string;
540
+ "x-descriptionZh": string;
541
+ };
542
+ fontSize: {
543
+ type: string;
544
+ minimum: number;
545
+ description: string;
546
+ "x-descriptionZh": string;
547
+ };
548
+ color: {
549
+ type: string;
550
+ description: string;
551
+ "x-descriptionZh": string;
552
+ };
553
+ padding: {
554
+ type: string;
555
+ minimum: number;
556
+ description: string;
557
+ "x-descriptionZh": string;
558
+ };
559
+ scale: {
560
+ type: string;
561
+ minimum: number;
562
+ maximum: number;
563
+ description: string;
564
+ "x-descriptionZh": string;
565
+ };
566
+ fileName: {
567
+ type: string;
568
+ minLength: number;
569
+ description: string;
570
+ "x-descriptionZh": string;
571
+ };
572
+ };
573
+ })[];
574
+ description: string;
575
+ "x-descriptionZh": string;
576
+ };
577
+ };
578
+ };
579
+ assistant: {
580
+ type: string;
581
+ additionalProperties: boolean;
582
+ description: string;
583
+ "x-descriptionZh": string;
584
+ };
585
+ log_level: {
586
+ type: string[];
587
+ description: string;
588
+ "x-descriptionZh": string;
589
+ };
590
+ plugin: {
591
+ type: string;
592
+ description: string;
593
+ "x-descriptionZh": string;
594
+ };
595
+ };
596
+ $defs: {
597
+ decisionTask: {
598
+ type: string;
599
+ additionalProperties: boolean;
600
+ required: string[];
601
+ properties: {
602
+ mode: {
603
+ type: string;
604
+ enum: string[];
605
+ };
606
+ timeoutMs: {
607
+ type: string;
608
+ minimum: number;
609
+ maximum: number;
610
+ };
611
+ minConfidence: {
612
+ type: string;
613
+ minimum: number;
614
+ maximum: number;
615
+ };
616
+ topK: {
617
+ type: string;
618
+ minimum: number;
619
+ maximum: number;
620
+ };
621
+ maxCandidates: {
622
+ type: string;
623
+ minimum: number;
624
+ maximum: number;
625
+ };
626
+ maxSelections: {
627
+ type: string;
628
+ minimum: number;
629
+ maximum: number;
630
+ };
631
+ };
632
+ };
633
+ };
634
+ };
4
635
  /** Host configuration keys consumed by Runtime composition and Host installers. */
5
636
  export declare const HOST_CONFIG_KEYS: readonly string[];
6
637
  export type JsonSchema = Readonly<Record<string, unknown>>;
@@ -10,7 +10,7 @@ function deepFreeze(value) {
10
10
  }
11
11
  return value;
12
12
  }
13
- const HOST_CONFIG_SCHEMA = deepFreeze(hostConfigSchema);
13
+ export const HOST_CONFIG_SCHEMA = deepFreeze(hostConfigSchema);
14
14
  /** Host configuration keys consumed by Runtime composition and Host installers. */
15
15
  export const HOST_CONFIG_KEYS = Object.freeze(Object.keys(HOST_CONFIG_SCHEMA.properties));
16
16
  export class ConfigSchemaCollisionError extends Error {
@@ -57,6 +57,7 @@ export class ConfigComposer {
57
57
  // start-command, not Plugin ConfigViews.
58
58
  const effectiveSchema = Object.freeze({
59
59
  type: 'object',
60
+ $defs: HOST_CONFIG_SCHEMA.$defs,
60
61
  additionalProperties: false,
61
62
  properties: {
62
63
  ...HOST_CONFIG_SCHEMA.properties,
@@ -1,3 +1,4 @@
1
+ import { normalizeSourcePath } from './source-path.js';
1
2
  import { InvalidationPlanner, } from './invalidation-planner.js';
2
3
  export class HmrCoordinator {
3
4
  options;
@@ -31,11 +32,28 @@ export class HmrCoordinator {
31
32
  if (this.#stopResult)
32
33
  return this.#stopResult;
33
34
  this.#closing = true;
34
- this.#unwatch?.();
35
+ const unwatch = this.#unwatch;
35
36
  this.#unwatch = undefined;
36
- this.#stopResult = (async () => {
37
- await this.#draining;
38
- })();
37
+ this.#stopResult = Promise.resolve().then(async () => {
38
+ const errors = [];
39
+ try {
40
+ await unwatch?.();
41
+ }
42
+ catch (error) {
43
+ errors.push(error);
44
+ }
45
+ // Watcher disposal failure must not bypass already admitted reloads.
46
+ try {
47
+ await this.#draining;
48
+ }
49
+ catch (error) {
50
+ errors.push(error);
51
+ }
52
+ if (errors.length === 1)
53
+ throw errors[0];
54
+ if (errors.length > 1)
55
+ throw new AggregateError(errors, 'HMR stop failed');
56
+ });
39
57
  return this.#stopResult;
40
58
  }
41
59
  enqueue(source) {
@@ -45,7 +63,7 @@ export class HmrCoordinator {
45
63
  if (this.#restartRequired) {
46
64
  return Promise.reject(new Error('HMR coordinator requires a process restart'));
47
65
  }
48
- this.#pending.add(source);
66
+ this.#pending.add(normalizeSourcePath(source));
49
67
  const completed = new Promise((resolve, reject) => {
50
68
  this.#waiters.push({ resolve, reject });
51
69
  });
@@ -46,6 +46,21 @@
46
46
  "description": "Providers, Agents, sessions, memory, tools, and execution security.",
47
47
  "x-descriptionZh": "Provider、Agent、会话、记忆、工具与执行安全策略。",
48
48
  "properties": {
49
+ "decisions": {
50
+ "type": "object",
51
+ "additionalProperties": false,
52
+ "required": ["provider"],
53
+ "description": "Optional structured-decision service. Missing tasks are off; use shadow before active.",
54
+ "x-descriptionZh": "可选的结构化决策服务。未配置的任务关闭;建议先用 shadow 验证。",
55
+ "properties": {
56
+ "provider": { "type": "string", "pattern": "^root(/[a-z0-9][a-z0-9-]*)+$", "description": "Exact enabled DecisionProvider Plugin owner, such as root/typesafe.", "x-descriptionZh": "提供已启用 DecisionProvider 的精确插件身份,例如 root/typesafe。" },
57
+ "skills": { "$ref": "#/$defs/decisionTask" },
58
+ "tools": { "$ref": "#/$defs/decisionTask" },
59
+ "memory": { "$ref": "#/$defs/decisionTask" },
60
+ "agents": { "$ref": "#/$defs/decisionTask" },
61
+ "approval": { "$ref": "#/$defs/decisionTask" }
62
+ }
63
+ },
49
64
  "workroom": {
50
65
  "type": "object",
51
66
  "additionalProperties": true,
@@ -264,5 +279,20 @@
264
279
  "assistant": { "type": "object", "additionalProperties": true, "description": "Scheduled jobs, event ingress, and failure notifications.", "x-descriptionZh": "调度任务、事件入口和失败通知。" },
265
280
  "log_level": { "type": ["string", "number"], "description": "Runtime log level.", "x-descriptionZh": "Runtime 日志级别。" },
266
281
  "plugin": { "type": "object", "description": "Root Plugin configuration; replaced by its project schema during composition.", "x-descriptionZh": "Root Plugin 配置;组合时由项目 Schema 替换。" }
282
+ },
283
+ "$defs": {
284
+ "decisionTask": {
285
+ "type": "object",
286
+ "additionalProperties": false,
287
+ "required": ["mode"],
288
+ "properties": {
289
+ "mode": { "type": "string", "enum": ["off", "shadow", "active"] },
290
+ "timeoutMs": { "type": "integer", "minimum": 1, "maximum": 2147483647 },
291
+ "minConfidence": { "type": "number", "minimum": 0, "maximum": 1 },
292
+ "topK": { "type": "integer", "minimum": 1, "maximum": 9007199254740991 },
293
+ "maxCandidates": { "type": "integer", "minimum": 1, "maximum": 9007199254740991 },
294
+ "maxSelections": { "type": "integer", "minimum": 1, "maximum": 9007199254740991 }
295
+ }
296
+ }
267
297
  }
268
298
  }
@@ -1,4 +1,5 @@
1
- import { basename, resolve } from 'node:path';
1
+ import { normalizeSourcePath } from './source-path.js';
2
+ import { basename } from 'node:path';
2
3
  import { rootPluginId } from '@zhin.js/plugin-runtime';
3
4
  const processFiles = new Set([
4
5
  'pnpm-lock.yaml',
@@ -14,7 +15,7 @@ export class InvalidationPlanner {
14
15
  this.dependencies = dependencies;
15
16
  }
16
17
  plan(sources) {
17
- const changed = unique(sources.map((source) => resolve(source)));
18
+ const changed = unique(sources.map((source) => normalizeSourcePath(source)));
18
19
  if (changed.some((source) => processFiles.has(basename(source)))) {
19
20
  return Object.freeze({
20
21
  kind: 'process',
@@ -32,7 +33,7 @@ export class InvalidationPlanner {
32
33
  if (basename(source) === 'package.json') {
33
34
  addManifestSource(manifestSources, reasons, source);
34
35
  }
35
- const affected = unique([source, ...(this.dependencies?.affectedSources(source) ?? [])].map((item) => resolve(item)));
36
+ const affected = unique([source, ...(this.dependencies?.affectedSources(source) ?? [])].map((item) => normalizeSourcePath(item)));
36
37
  let matched = false;
37
38
  for (const item of affected) {
38
39
  const records = this.ownership.recordsFor(item);
@@ -95,7 +96,7 @@ export class InvalidationPlanner {
95
96
  }
96
97
  }
97
98
  function addManifestSource(manifestSources, reasons, source) {
98
- const manifest = resolve(source);
99
+ const manifest = normalizeSourcePath(source);
99
100
  manifestSources.add(manifest);
100
101
  reasons.add(`Manifest source changed: ${manifest}`);
101
102
  }
@@ -1,3 +1,4 @@
1
+ import { normalizeSourcePath } from './source-path.js';
1
2
  import { access, readFile } from 'node:fs/promises';
2
3
  import { basename, dirname, extname, isAbsolute, join, relative, resolve } from 'node:path';
3
4
  import { fileURLToPath, pathToFileURL } from 'node:url';
@@ -21,7 +22,7 @@ export class ModuleDependencyIndex {
21
22
  #commonJsByDirectory = new Map();
22
23
  #dirtyEntries = new Set();
23
24
  async analyze(entry) {
24
- const normalized = resolve(entry);
25
+ const normalized = normalizeSourcePath(entry);
25
26
  const cached = this.#dependenciesByEntry.get(normalized);
26
27
  if (cached && !this.#dirtyEntries.has(normalized)) {
27
28
  return analysis(cached, this.#commonJsSourcesByEntry.get(normalized) ?? new Set());
@@ -34,7 +35,7 @@ export class ModuleDependencyIndex {
34
35
  return analysis(dependencies, commonJsSources);
35
36
  }
36
37
  commit(entry, result) {
37
- const normalized = resolve(entry);
38
+ const normalized = normalizeSourcePath(entry);
38
39
  this.#remove(normalized);
39
40
  const dependencies = new Set(result.dependencies);
40
41
  const commonJsSources = new Set(result.commonJsSources);
@@ -50,7 +51,7 @@ export class ModuleDependencyIndex {
50
51
  }
51
52
  /** Marks the changed entry and every entry importing it for lazy re-analysis. */
52
53
  invalidate(source) {
53
- const normalized = resolve(source);
54
+ const normalized = normalizeSourcePath(source);
54
55
  if (basename(normalized) === 'package.json') {
55
56
  this.#commonJsByDirectory.clear();
56
57
  const packageRoot = dirname(normalized);
@@ -69,7 +70,7 @@ export class ModuleDependencyIndex {
69
70
  }
70
71
  /** Drops entries that no longer belong to the successfully committed generation. */
71
72
  retain(entries) {
72
- const active = new Set(entries.map((entry) => resolve(entry)));
73
+ const active = new Set(entries.map((entry) => normalizeSourcePath(entry)));
73
74
  for (const entry of this.#dependenciesByEntry.keys()) {
74
75
  if (!active.has(entry))
75
76
  this.#remove(entry);
@@ -84,7 +85,7 @@ export class ModuleDependencyIndex {
84
85
  this.#dirtyEntries.clear();
85
86
  }
86
87
  hasCommonJsImpact(source) {
87
- const normalized = resolve(source);
88
+ const normalized = normalizeSourcePath(source);
88
89
  if (isExplicitCommonJsSource(normalized))
89
90
  return true;
90
91
  if (this.#entriesByCommonJsSource.has(normalized))
@@ -92,7 +93,7 @@ export class ModuleDependencyIndex {
92
93
  return [...(this.#entriesByDependency.get(normalized) ?? [])].some((entry) => this.#commonJsSourcesByEntry.get(entry)?.has(entry) ?? false);
93
94
  }
94
95
  affectedSources(source) {
95
- const normalized = resolve(source);
96
+ const normalized = normalizeSourcePath(source);
96
97
  const entries = [...(this.#entriesByDependency.get(normalized) ?? [])].sort();
97
98
  return Object.freeze([normalized, ...entries.filter((entry) => entry !== normalized)]);
98
99
  }
@@ -363,7 +364,7 @@ async function resolveLocalImport(importer, specifier) {
363
364
  }
364
365
  for (const candidate of importCandidates(requested)) {
365
366
  if (await exists(candidate))
366
- return resolve(candidate);
367
+ return normalizeSourcePath(candidate);
367
368
  }
368
369
  return undefined;
369
370
  }
@@ -1,3 +1,4 @@
1
+ import { normalizeSourcePath } from './source-path.js';
1
2
  import { readdirSync, statSync, watch as watchDirectory, } from 'node:fs';
2
3
  import { basename, extname, isAbsolute, relative, resolve, sep } from 'node:path';
3
4
  import { pathToFileURL } from 'node:url';
@@ -29,14 +30,14 @@ export class NativeDevelopmentModuleRuntime {
29
30
  #closed = false;
30
31
  constructor(options) {
31
32
  ensureTypeScriptSpecifierRemap();
32
- this.#projectRoot = resolve(options.projectRoot);
33
+ this.#projectRoot = normalizeSourcePath(options.projectRoot);
33
34
  this.#watchEnabled = options.watch ?? true;
34
35
  this.#tsxLoader = options.tsxLoader;
35
36
  this.#watchRoots = normalizeWatchRoots([this.#projectRoot]);
36
37
  }
37
38
  async load(source) {
38
39
  this.#assertOpen();
39
- const normalized = resolve(source);
40
+ const normalized = normalizeSourcePath(source);
40
41
  if (isNativeTypeScriptSource(normalized))
41
42
  assertNativeTypeScriptSupport();
42
43
  const analysis = this.#pendingDependencies?.get(normalized)
@@ -67,7 +68,7 @@ export class NativeDevelopmentModuleRuntime {
67
68
  return this.#dependencies.affectedSources(source);
68
69
  }
69
70
  requiresProcessRestart(source) {
70
- const normalized = resolve(source);
71
+ const normalized = normalizeSourcePath(source);
71
72
  const packageRoot = nearestWatchRoot(this.#watchRoots, normalized);
72
73
  // Installed packages and external paths are intentionally not watched.
73
74
  // The HMR coordinator turns this into a visible process restart reason.
@@ -353,7 +354,7 @@ function isNodeModulesSource(root, source) {
353
354
  return relative(root, source).split(sep).some((part) => part === 'node_modules');
354
355
  }
355
356
  function normalizeWatchRoots(roots) {
356
- const sorted = [...new Set(roots.map((root) => resolve(root)))].sort((left, right) => left.length - right.length);
357
+ const sorted = [...new Set(roots.map((root) => normalizeSourcePath(root)))].sort((left, right) => left.length - right.length);
357
358
  return Object.freeze(sorted.filter((root, index) => !sorted.slice(0, index).some((parent) => isWithin(parent, root))));
358
359
  }
359
360
  function sameRoots(left, right) {
@@ -1,4 +1,4 @@
1
- import { DisposeStack, Scope, type Dispose, type FeatureId, type GenerationHandoff, type GenerationAdmissionGate, type GenerationHandoffRegistry, type PluginId, type PluginNodeSnapshot, type SetupCapabilityRegistration, type TokenId } from '@zhin.js/plugin-runtime';
1
+ import { DisposeStack, Scope, type Dispose, type FeatureId, type GenerationHandoff, type GenerationAdmissionGate, type GenerationHandoffRegistry, type RuntimeSnapshot, type PluginId, type PluginNodeSnapshot, type SetupCapabilityRegistration, type TokenId } from '@zhin.js/plugin-runtime';
2
2
  import { type RuntimeEnvironment } from './environment.js';
3
3
  import { type EnvironmentLayers } from './environment-store.js';
4
4
  import type { ModuleRuntime } from './module-runtime.js';
@@ -10,6 +10,8 @@ export type PluginConfigResolver = (node: PluginGraphNode) => unknown;
10
10
  export interface RootResourceContext {
11
11
  /** Exact shadow generation being assembled; never inferred from a latest snapshot. */
12
12
  readonly generation: number;
13
+ /** Exact projected candidate, available during handoff activation, never the committed previous generation. */
14
+ readonly readCandidateSnapshot?: () => RuntimeSnapshot;
13
15
  readonly signal: AbortSignal;
14
16
  readonly resources: Scope;
15
17
  readonly lifecycle: DisposeStack;
@@ -51,6 +53,7 @@ export declare class PluginScopeAssembler {
51
53
  setupTree(node: PluginGraphNode, signal: AbortSignal): Promise<void>;
52
54
  synchronizeTree(node: PluginGraphNode): void;
53
55
  createdScopeDisposers(): readonly (readonly [PluginId, Dispose])[];
56
+ bindCandidateSnapshot(snapshot: RuntimeSnapshot): void;
54
57
  generationHandoff(): GenerationHandoff | undefined;
55
58
  setupCapabilities(): readonly Readonly<SetupCapabilityRegistration>[];
56
59
  /** Root resources are process-owned; every Plugin Scope receives an owner-bound facade. */
@@ -21,6 +21,7 @@ export class PluginScopeAssembler {
21
21
  #handoffs = new GenerationHandoffStack();
22
22
  #admission = createGenerationAdmissionGate();
23
23
  #envStores;
24
+ #candidateSnapshot;
24
25
  #setupFeatureAliases = new Map();
25
26
  constructor(modules, configResolver, environment, primaryConfigDocument, generation, installResources, environmentLayers = {}, seed, isolation) {
26
27
  this.modules = modules;
@@ -71,6 +72,11 @@ export class PluginScopeAssembler {
71
72
  scope.provide(primaryConfigToken, config);
72
73
  await this.installResources?.({
73
74
  generation: this.generation,
75
+ readCandidateSnapshot: () => {
76
+ if (!this.#candidateSnapshot)
77
+ throw new Error('Candidate snapshot is not projected yet');
78
+ return this.#candidateSnapshot;
79
+ },
74
80
  signal,
75
81
  resources: scope,
76
82
  lifecycle: scope.disposers,
@@ -206,6 +212,12 @@ export class PluginScopeAssembler {
206
212
  return [owner, () => scope.disposers.dispose()];
207
213
  });
208
214
  }
215
+ bindCandidateSnapshot(snapshot) {
216
+ if (snapshot.generation !== this.generation || this.#candidateSnapshot) {
217
+ throw new Error('Candidate snapshot does not match this Plugin assembly');
218
+ }
219
+ this.#candidateSnapshot = snapshot;
220
+ }
209
221
  generationHandoff() {
210
222
  return this.#handoffs.seal();
211
223
  }
@@ -1,3 +1,4 @@
1
+ import { normalizeSourcePath } from './source-path.js';
1
2
  import { resolve } from 'node:path';
2
3
  import { isDeepStrictEqual } from 'node:util';
3
4
  import { DisposeStack, GenerationHandoffStack, RootController, ConfigDocumentDivergenceError, createSnapshotView, rootPluginId, } from '@zhin.js/plugin-runtime';
@@ -41,7 +42,7 @@ export class RootRuntime {
41
42
  #controller;
42
43
  #disabledPluginInstanceKeys;
43
44
  constructor(options) {
44
- this.#projectRoot = resolve(options.projectRoot);
45
+ this.#projectRoot = normalizeSourcePath(options.projectRoot);
45
46
  this.#modules = options.modules;
46
47
  this.#environment = defineRuntimeEnvironment(options.environment);
47
48
  this.#environmentLayers = defineEnvironmentLayers(options.environmentVariables);
@@ -55,7 +56,7 @@ export class RootRuntime {
55
56
  this.#runtimeInputSources = new Set([
56
57
  ...(this.#configPort?.sources ?? []),
57
58
  ...(this.#environmentSource?.sources ?? []),
58
- ].map((source) => resolve(source)));
59
+ ].map((source) => normalizeSourcePath(source)));
59
60
  this.#installResources = options.installResources;
60
61
  this.#isolation = options.isolation;
61
62
  this.#disabledPluginInstanceKeys = Object.freeze([...(options.disabledPluginInstanceKeys ?? [])]);
@@ -105,7 +106,7 @@ export class RootRuntime {
105
106
  modules: this.#modules,
106
107
  ownership: () => this.sourceOwnership,
107
108
  runtime: {
108
- handlesSource: (source) => this.#runtimeInputSources.has(resolve(source)),
109
+ handlesSource: (source) => this.#runtimeInputSources.has(normalizeSourcePath(source)),
109
110
  reload: async (plan) => {
110
111
  const result = await this.#reloadPlan(plan);
111
112
  return isProcessPlan(result) ? result : undefined;
@@ -139,7 +140,7 @@ export class RootRuntime {
139
140
  ? Object.freeze({
140
141
  ...plan,
141
142
  subtrees: collapseInvalidationSubtrees([...plan.subtrees, ...inputs.roots]),
142
- fallbacks: Object.freeze(plan.fallbacks.filter((fallback) => !inputs.inputSources.has(resolve(fallback.source)))),
143
+ fallbacks: Object.freeze(plan.fallbacks.filter((fallback) => !inputs.inputSources.has(normalizeSourcePath(fallback.source)))),
143
144
  })
144
145
  : plan;
145
146
  let restart;
@@ -189,11 +190,11 @@ export class RootRuntime {
189
190
  return this.#controller.committed.state.model;
190
191
  }
191
192
  async #planRuntimeInputs(changed) {
192
- const inputSources = new Set(changed.map((source) => resolve(source)).filter((source) => this.#runtimeInputSources.has(source)));
193
+ const inputSources = new Set(changed.map((source) => normalizeSourcePath(source)).filter((source) => this.#runtimeInputSources.has(source)));
193
194
  if (inputSources.size === 0 || !this.#model)
194
195
  return undefined;
195
- const configChanged = this.#configPort?.sources?.some((source) => inputSources.has(resolve(source))) ?? false;
196
- const environmentChanged = this.#environmentSource?.sources.some((source) => inputSources.has(resolve(source))) ?? false;
196
+ const configChanged = this.#configPort?.sources?.some((source) => inputSources.has(normalizeSourcePath(source))) ?? false;
197
+ const environmentChanged = this.#environmentSource?.sources.some((source) => inputSources.has(normalizeSourcePath(source))) ?? false;
197
198
  const configSnapshot = configChanged ? await this.#configPort?.read() : undefined;
198
199
  const document = configSnapshot?.document ?? requireConfigDocument(this.#configDocument);
199
200
  const environmentLayers = environmentChanged && this.#environmentSource
@@ -210,7 +211,7 @@ export class RootRuntime {
210
211
  if (inputPlan.hostKeys.length > 0) {
211
212
  return Object.freeze({
212
213
  kind: 'process',
213
- changed: Object.freeze([...changed].map((source) => resolve(source))),
214
+ changed: Object.freeze([...changed].map((source) => normalizeSourcePath(source))),
214
215
  reasons: Object.freeze(inputPlan.hostKeys.map((key) => `Host configuration changed: ${key}`)),
215
216
  });
216
217
  }
@@ -263,7 +264,7 @@ export class RootRuntime {
263
264
  if (plan.subtrees.length === 0 || plan.subtrees.includes(rootPluginId()))
264
265
  return false;
265
266
  return plan.changed.every((source) => {
266
- if (this.#runtimeInputSources.has(resolve(source)))
267
+ if (this.#runtimeInputSources.has(normalizeSourcePath(source)))
267
268
  return true;
268
269
  const records = this.sourceOwnership.recordsFor(source);
269
270
  return records.length > 0 && records.every((record) => record.role === 'plugin' || record.role === 'schema');
@@ -519,6 +520,7 @@ class GenerationAssembler {
519
520
  }
520
521
  const state = projected.state;
521
522
  const snapshot = createSnapshotView(this.generation, state);
523
+ this.#plugins.bindCandidateSnapshot(snapshot);
522
524
  const ownership = SourceOwnershipIndex.fromGeneration(this.graph, snapshot, this.#featureIdsByPackageRoot);
523
525
  const assets = GenerationAssets.create(this.#plugins.createdScopeDisposers(), this.#projectionDisposers);
524
526
  return prepareRuntimeGeneration({
@@ -1,3 +1,4 @@
1
+ import { normalizeSourcePath } from './source-path.js';
1
2
  import { isAbsolute, relative, resolve } from 'node:path';
2
3
  export class SourceOwnershipIndex {
3
4
  #records = new Map();
@@ -102,7 +103,7 @@ export class SourceOwnershipIndex {
102
103
  return Object.freeze([...this.#watchRoots.values()]);
103
104
  }
104
105
  addWatchRoot(root) {
105
- const normalized = resolve(root.root);
106
+ const normalized = normalizeSourcePath(root.root);
106
107
  this.#watchRoots.set(normalized, Object.freeze({ ...root, root: normalized }));
107
108
  }
108
109
  }
@@ -112,7 +113,7 @@ function visitPlugin(node, visit) {
112
113
  visitPlugin(child, visit);
113
114
  }
114
115
  function normalizeSource(source) {
115
- return resolve(source);
116
+ return normalizeSourcePath(source);
116
117
  }
117
118
  function contains(packageRoot, source) {
118
119
  const path = relative(packageRoot, source);
@@ -0,0 +1,4 @@
1
+ /** Keep file events and scanned sources in the same physical path namespace.
2
+ * Resolve the nearest existing parent for unlink events and newly added files.
3
+ */
4
+ export declare function normalizeSourcePath(source: string): string;
@@ -0,0 +1,25 @@
1
+ import { realpathSync } from 'node:fs';
2
+ import { basename, dirname, join, resolve } from 'node:path';
3
+ /** Keep file events and scanned sources in the same physical path namespace.
4
+ * Resolve the nearest existing parent for unlink events and newly added files.
5
+ */
6
+ export function normalizeSourcePath(source) {
7
+ const absolute = resolve(source);
8
+ let parent = absolute;
9
+ const suffix = [];
10
+ for (;;) {
11
+ try {
12
+ return join(realpathSync(parent), ...suffix);
13
+ }
14
+ catch (error) {
15
+ const code = error.code;
16
+ if (code !== 'ENOENT' && code !== 'ENOTDIR')
17
+ throw error;
18
+ const next = dirname(parent);
19
+ if (next === parent)
20
+ return absolute;
21
+ suffix.unshift(basename(parent));
22
+ parent = next;
23
+ }
24
+ }
25
+ }
@@ -82,6 +82,7 @@ export class SubtreeGenerationPreparer {
82
82
  projectionDisposers.set(feature, dispose);
83
83
  }
84
84
  const snapshot = createSnapshotView(current.generation + 1, projected.state);
85
+ plugins.bindCandidateSnapshot(snapshot);
85
86
  const ownership = SourceOwnershipIndex.fromGeneration(this.graph, snapshot, this.model.featureIdsByPackageRoot);
86
87
  const replacements = new Map(plugins.createdScopeDisposers());
87
88
  // Local prepare still commits a complete immutable generation. The
@@ -89,6 +89,7 @@ export class TopologyGenerationPreparer {
89
89
  projectionDisposers.set(feature, dispose);
90
90
  }
91
91
  const snapshot = createSnapshotView(current.generation + 1, projected.state);
92
+ plugins.bindCandidateSnapshot(snapshot);
92
93
  const ownership = SourceOwnershipIndex.fromGeneration(this.graph, snapshot, featureTopology.featureIdsByPackageRoot);
93
94
  const replacements = new Map(plugins.createdScopeDisposers());
94
95
  const assets = this.model.assets.replaceScopes(graphOrder(this.graph), replacements, projectionDisposers);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/runtime",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
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",
@@ -20,24 +20,24 @@
20
20
  "es-module-lexer": "2.3.1",
21
21
  "import-meta-resolve": "4.2.0",
22
22
  "semver": "7.8.5",
23
- "@zhin.js/feature-kit": "1.1.2",
24
- "@zhin.js/plugin-runtime": "1.1.11"
23
+ "@zhin.js/plugin-runtime": "1.1.12",
24
+ "@zhin.js/feature-kit": "1.1.3"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^26.1.2",
28
28
  "tsx": "^4.23.1",
29
29
  "typescript": "^6.0.3",
30
- "@zhin.js/adapter": "1.1.15",
31
- "@zhin.js/agent-feature": "1.1.2",
32
- "@zhin.js/command": "1.1.3",
33
- "@zhin.js/component": "1.1.2",
34
- "@zhin.js/layout": "1.1.3",
35
- "@zhin.js/mcp-feature": "1.1.2",
36
- "@zhin.js/middleware": "1.1.2",
37
- "@zhin.js/prompt-section": "1.1.2",
38
- "@zhin.js/page": "1.1.3",
39
- "@zhin.js/tool": "1.1.2",
40
- "@zhin.js/skill": "1.1.2"
30
+ "@zhin.js/adapter": "1.1.17",
31
+ "@zhin.js/agent-feature": "1.1.4",
32
+ "@zhin.js/command": "1.1.5",
33
+ "@zhin.js/component": "1.1.4",
34
+ "@zhin.js/layout": "1.1.4",
35
+ "@zhin.js/mcp-feature": "1.1.4",
36
+ "@zhin.js/middleware": "1.1.4",
37
+ "@zhin.js/page": "1.1.4",
38
+ "@zhin.js/prompt-section": "1.1.3",
39
+ "@zhin.js/skill": "1.1.4",
40
+ "@zhin.js/tool": "1.1.4"
41
41
  },
42
42
  "repository": {
43
43
  "type": "git",