@openspecui/server 6.0.0 → 6.1.0

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.
package/dist/index.mjs CHANGED
@@ -26,12 +26,16 @@ import { promisify } from "node:util";
26
26
  import { NotificationGroupKeySchema, NotificationPublishInputSchema as NotificationPublishInputSchema$1, getNotificationGroupKey } from "@openspecui/core/notifications";
27
27
  import { match } from "ts-pattern";
28
28
  import { buildSpecCatalog, specIdentityKey, specRoutePath } from "@openspecui/core/spec-catalog";
29
+ import { DiagConsoleLogger, DiagLogLevel, SpanStatusCode, diag, trace } from "@opentelemetry/api";
29
30
  import { EventEmitter } from "node:events";
30
31
  import { ProjectSearchDocumentSchema, ProjectSearchHitSchema, ProjectSearchQuerySchema, parseProjectSearchHits } from "@openspecui/search";
31
32
  import { NodeWorkerSearchProvider } from "@openspecui/search/node";
32
33
  import * as pty from "@lydell/node-pty";
33
34
  import { EventEmitter as EventEmitter$1 } from "events";
34
- import { HostedCliProjectionNoticeSchema, HostedRootContextProjectionStateSchema, HostedRootContextResolvedStateSchema, HostedStoreDoctorEnvelopeSchema, HostedStoreDoctorProjectionStateSchema, HostedStoreListEnvelopeSchema, HostedStoreListProjectionStateSchema } from "@openspecui/core/hosted-contract";
35
+ import { HostedCliProjectionNoticeSchema, HostedRootContextProjectionStateSchema, HostedRootContextResolvedStateSchema, HostedStoreContentChangesProjectionStateSchema, HostedStoreContentSpecsProjectionStateSchema, HostedStoreDoctorEnvelopeSchema, HostedStoreDoctorProjectionStateSchema, HostedStoreListEnvelopeSchema, HostedStoreListProjectionStateSchema } from "@openspecui/core/hosted-contract";
36
+ import { StoreContentChangeListSchema, StoreContentSpecListSchema } from "@openspecui/core/store-content-projection";
37
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
38
+ import { NodeSDK } from "@opentelemetry/sdk-node";
35
39
  import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
36
40
 
37
41
  //#region src/access-gate.ts
@@ -2867,6 +2871,32 @@ function formatBytes$4(value) {
2867
2871
  //#endregion
2868
2872
  //#region src/port-utils.ts
2869
2873
  /**
2874
+ * Orthogonal intents (created 2026-07-31 Asia/Shanghai):
2875
+ * 1. Resolve an actual bindable port for explicit ranges and OS-assigned ephemeral requests.
2876
+ *
2877
+ * Owner-reported defect (2026-07-31): Managed directory launch published `http://localhost:0`
2878
+ * even though the backend was listening on an OS-assigned port.
2879
+ */
2880
+ function findEphemeralPort() {
2881
+ return new Promise((resolve$1, reject) => {
2882
+ const server = createServer$1();
2883
+ server.once("error", reject);
2884
+ server.once("listening", () => {
2885
+ const address = server.address();
2886
+ if (!address || typeof address === "string") {
2887
+ server.close();
2888
+ reject(/* @__PURE__ */ new Error("OS-assigned TCP port did not expose a numeric address."));
2889
+ return;
2890
+ }
2891
+ server.close((error) => {
2892
+ if (error) reject(error);
2893
+ else resolve$1(address.port);
2894
+ });
2895
+ });
2896
+ server.listen(0);
2897
+ });
2898
+ }
2899
+ /**
2870
2900
  * Check if a port is available by trying to listen on it.
2871
2901
  * Uses default binding (both IPv4 and IPv6) to detect conflicts.
2872
2902
  */
@@ -2892,6 +2922,7 @@ function isPortAvailable(port) {
2892
2922
  * @throws Error if no available port is found in the range
2893
2923
  */
2894
2924
  async function findAvailablePort(startPort, maxAttempts = 10) {
2925
+ if (startPort === 0) return findEphemeralPort();
2895
2926
  for (let i = 0; i < maxAttempts; i++) {
2896
2927
  const port = startPort + i;
2897
2928
  if (await isPortAvailable(port)) return port;
@@ -3001,6 +3032,7 @@ var ProjectionWorkScheduler = class {
3001
3032
  work.started = true;
3002
3033
  const queue$1 = this.queues[work.resourceClass];
3003
3034
  queue$1.running.add(work);
3035
+ request.onStarted?.();
3004
3036
  request.run(controller.signal).then((value) => {
3005
3037
  settle(() => resolve$1(value));
3006
3038
  queue$1.running.delete(work);
@@ -3020,6 +3052,7 @@ var ProjectionWorkScheduler = class {
3020
3052
  queue.foreground.push(work);
3021
3053
  this.cancelOneBackgroundWork(request.resourceClass);
3022
3054
  } else queue.background.push(work);
3055
+ request.onQueued?.();
3023
3056
  this.drain(request.resourceClass);
3024
3057
  });
3025
3058
  }
@@ -3073,6 +3106,22 @@ var ProjectionWorkScheduler = class {
3073
3106
 
3074
3107
  //#endregion
3075
3108
  //#region src/projection-work/types.ts
3109
+ /** Observable lifecycle phases. These describe actual work, never inferred page copy. */
3110
+ const projectionWorkPhases = [
3111
+ "request",
3112
+ "transport-start",
3113
+ "root-ready",
3114
+ "cache-hit",
3115
+ "join",
3116
+ "start",
3117
+ "queue-enter",
3118
+ "resource-admitted",
3119
+ "leaf-settled",
3120
+ "first-stable-payload",
3121
+ "complete",
3122
+ "error",
3123
+ "cancel"
3124
+ ];
3076
3125
  /** Produce a deterministic, collision-resistant-for-fields registry key without relying on object ordering. */
3077
3126
  function projectionWorkIdentityKey(identity) {
3078
3127
  return JSON.stringify([
@@ -3091,15 +3140,16 @@ function projectionWorkIdentityKey(identity) {
3091
3140
  //#endregion
3092
3141
  //#region src/projection-work/registry.ts
3093
3142
  /**
3094
- * Orthogonal intents (created 2026-07-23 Asia/Shanghai):
3143
+ * Orthogonal intents (updated 2026-07-31 Asia/Shanghai):
3095
3144
  * 1. Share same-identity in-flight projection work across Server subscribers.
3096
3145
  * 2. Retain bounded display snapshots while preserving freshness and provenance.
3097
3146
  * 3. Retire generations so late A work cannot publish into current B state.
3098
- * 4. Expose explicit invalidation, cancellation, batches, and typed failures.
3147
+ * 4. Expose explicit invalidation, cancellation, batches, typed failures, and resource-admission trace boundaries.
3099
3148
  * 5. Expose immediate cached lifecycle reads and data-free lifecycle notices.
3100
3149
  *
3101
3150
  * Original request (2026-07-23): "现在页面数据的加载数据非常慢(比如dashboard页面、changes页面都要等待非常久,页面刷新后,似乎后台没有缓存一样,也要加载很久。"
3102
3151
  * Original request (2026-07-26): "界面上仍然可以读到缓存,但它也能知道这个缓存现在正在被更新中。"
3152
+ * Original request (2026-07-31): "检查它的工作到底做了什么,为什么需要那么多的时间"
3103
3153
  */
3104
3154
  var ProjectionWorkCapacityError = class extends Error {
3105
3155
  constructor() {
@@ -3319,6 +3369,16 @@ var ProjectionWork = class {
3319
3369
  resourceClass: this.request.resourceClass,
3320
3370
  priority: this.request.priority,
3321
3371
  signal: controller.signal,
3372
+ onQueued: () => {
3373
+ if (!this.isCurrentRun(generation, controller)) return;
3374
+ this.record("queue-enter", generation);
3375
+ this.publishStage("queue-enter", generation);
3376
+ },
3377
+ onStarted: () => {
3378
+ if (!this.isCurrentRun(generation, controller)) return;
3379
+ this.record("resource-admitted", generation);
3380
+ this.publishStage("resource-admitted", generation);
3381
+ },
3322
3382
  run: (scheduleSignal) => context.runOnce(() => this.request.load({
3323
3383
  signal: scheduleSignal,
3324
3384
  workGeneration: generation,
@@ -3607,7 +3667,7 @@ const serverProjectionWorkCacheBudget = {
3607
3667
  maxBytes: 2 * 1024 * 1024,
3608
3668
  maxWorkEntries: 64,
3609
3669
  traceCapacity: 256,
3610
- maxRegistryCount: 9
3670
+ maxRegistryCount: 10
3611
3671
  };
3612
3672
  var ProjectionWorkRuntimeCapacityError = class extends Error {
3613
3673
  constructor() {
@@ -3654,7 +3714,7 @@ function createServerProjectionWorkRuntime() {
3654
3714
  function createChangesProjectionWorkOwner(runtime) {
3655
3715
  return { rows: runtime.createRegistry() };
3656
3716
  }
3657
- function estimateSnapshotBytes$5(data) {
3717
+ function estimateSnapshotBytes$6(data) {
3658
3718
  return Buffer$1.byteLength(JSON.stringify(data) ?? "", "utf8");
3659
3719
  }
3660
3720
  function normalizeRowError(changeId, cause) {
@@ -3733,7 +3793,7 @@ var ChangesProjectionService = class {
3733
3793
  identity: this.identity(),
3734
3794
  resourceClass: "filesystem",
3735
3795
  priority: "foreground",
3736
- estimateSnapshotBytes: estimateSnapshotBytes$5,
3796
+ estimateSnapshotBytes: estimateSnapshotBytes$6,
3737
3797
  load: async (context) => {
3738
3798
  context.reportStage("root-ready");
3739
3799
  const ids = await this.options.adapter.listChanges();
@@ -5479,7 +5539,7 @@ var DashboardProjectionWorkOwner = class {
5479
5539
  function createDashboardProjectionWorkOwner(runtime) {
5480
5540
  return new DashboardProjectionWorkOwner(runtime);
5481
5541
  }
5482
- function estimateSnapshotBytes$4(data) {
5542
+ function estimateSnapshotBytes$5(data) {
5483
5543
  return Buffer$1.byteLength(JSON.stringify(data) ?? "", "utf8");
5484
5544
  }
5485
5545
  /** Root-scoped facade over Server-owned regional Projection Work registries. */
@@ -5533,7 +5593,7 @@ var DashboardProjectionService = class {
5533
5593
  getGit() {
5534
5594
  return this.getCurrent((listener) => this.subscribeGit(listener));
5535
5595
  }
5536
- /** Retire only the current Code Git projection after an explicit Git refresh mutation succeeds. */
5596
+ /** Retire only the current Code Git projection after an explicit readonly Git refresh succeeds. */
5537
5597
  invalidateGit() {
5538
5598
  if (this.disposed) return;
5539
5599
  this.options.workOwner.git.invalidate(this.identity("git"));
@@ -5567,7 +5627,7 @@ var DashboardProjectionService = class {
5567
5627
  identity: this.identity("summary"),
5568
5628
  resourceClass: "filesystem",
5569
5629
  priority: "foreground",
5570
- estimateSnapshotBytes: estimateSnapshotBytes$4,
5630
+ estimateSnapshotBytes: estimateSnapshotBytes$5,
5571
5631
  load: async (context) => {
5572
5632
  context.reportStage("root-ready");
5573
5633
  const data = await this.options.loaders.loadSummary();
@@ -5606,7 +5666,7 @@ var DashboardProjectionService = class {
5606
5666
  identity: this.identity("trends"),
5607
5667
  resourceClass: "filesystem",
5608
5668
  priority: "foreground",
5609
- estimateSnapshotBytes: estimateSnapshotBytes$4,
5669
+ estimateSnapshotBytes: estimateSnapshotBytes$5,
5610
5670
  load: async (context) => {
5611
5671
  context.reportStage("root-ready");
5612
5672
  const data = await this.options.loaders.loadTrends();
@@ -5620,10 +5680,10 @@ var DashboardProjectionService = class {
5620
5680
  identity: this.identity("git"),
5621
5681
  resourceClass: "git",
5622
5682
  priority: "foreground",
5623
- estimateSnapshotBytes: estimateSnapshotBytes$4,
5683
+ estimateSnapshotBytes: estimateSnapshotBytes$5,
5624
5684
  load: async (context) => {
5625
5685
  context.reportStage("root-ready");
5626
- const data = await this.options.loaders.loadGit();
5686
+ const data = await this.options.loaders.loadGit(context.signal);
5627
5687
  context.reportStage("leaf-settled");
5628
5688
  return data;
5629
5689
  }
@@ -5899,7 +5959,7 @@ function createEnvironmentGlobalProjectionWorkOwner(runtime) {
5899
5959
  fileRegistry: runtime.createRegistry()
5900
5960
  };
5901
5961
  }
5902
- function estimateSnapshotBytes$3(data) {
5962
+ function estimateSnapshotBytes$4(data) {
5903
5963
  return Buffer$1.byteLength(JSON.stringify(data) ?? "", "utf8");
5904
5964
  }
5905
5965
  /** CLI-backed Pull/Push owner for environment-global config, profile, and drift evidence. */
@@ -6027,7 +6087,7 @@ var EnvironmentGlobalProjectionService = class {
6027
6087
  identity: this.identity(),
6028
6088
  resourceClass: "cli",
6029
6089
  priority: "foreground",
6030
- estimateSnapshotBytes: estimateSnapshotBytes$3,
6090
+ estimateSnapshotBytes: estimateSnapshotBytes$4,
6031
6091
  load: async ({ reportStage, signal, workGeneration }) => {
6032
6092
  reportStage("root-ready");
6033
6093
  const projection = await readEnvironmentGlobalConfig({
@@ -6142,13 +6202,15 @@ var EnvironmentGlobalProjectionService = class {
6142
6202
  //#endregion
6143
6203
  //#region src/git-shared.ts
6144
6204
  /**
6145
- * Orthogonal intents (updated 2026-07-19 Asia/Shanghai):
6205
+ * Orthogonal intents (updated 2026-07-31 Asia/Shanghai):
6146
6206
  * 1. Execute Git commands and retain structured stdout/stderr/exit evidence.
6147
6207
  * 2. Distinguish a valid non-repository path from an identity-command failure.
6148
6208
  * 3. Canonicalize repository paths for binding identity and stale-intent checks.
6209
+ * 4. Propagate cooperative cancellation into Git subprocess ownership.
6149
6210
  *
6150
6211
  * Original request (2026-07-19): "代码已经提交,开始review。如果有问题,那么可更新change。"
6151
6212
  * Derived requirement (2026-07-19): Checkpoint 6.11 preserves objective Planning Git failures.
6213
+ * Original request (2026-07-31): "Code Git Snapshot,它非常慢,有时候甚至要十几秒"
6152
6214
  */
6153
6215
  const execFileAsync$2 = promisify(execFile);
6154
6216
  const EMPTY_DIFF = {
@@ -6156,18 +6218,20 @@ const EMPTY_DIFF = {
6156
6218
  insertions: 0,
6157
6219
  deletions: 0
6158
6220
  };
6159
- async function defaultRunGit(cwd, args) {
6221
+ async function defaultRunGit(cwd, args, signal) {
6160
6222
  try {
6161
6223
  const { stdout } = await execFileAsync$2("git", args, {
6162
6224
  cwd,
6163
6225
  encoding: "utf8",
6164
- maxBuffer: 8 * 1024 * 1024
6226
+ maxBuffer: 8 * 1024 * 1024,
6227
+ signal
6165
6228
  });
6166
6229
  return {
6167
6230
  ok: true,
6168
6231
  stdout
6169
6232
  };
6170
6233
  } catch (error) {
6234
+ if (signal?.aborted) throw error;
6171
6235
  const stderrField = readErrorField(error, "stderr");
6172
6236
  const stderr = typeof stderrField === "string" ? stderrField : void 0;
6173
6237
  return {
@@ -8089,13 +8153,15 @@ var NotificationService = class {
8089
8153
  //#endregion
8090
8154
  //#region src/spec-catalog-service.ts
8091
8155
  /**
8092
- * Orthogonal intents (updated 2026-07-27 Asia/Shanghai):
8156
+ * Orthogonal intents (updated 2026-07-31 Asia/Shanghai):
8093
8157
  * 1. Build CLI-owned Spec membership from the active planning root and direct References.
8094
8158
  * 2. Read owned documents from planning-root services and referenced documents from CLI JSON.
8095
8159
  * 3. Preserve live referenced command provenance and evidence without synthesizing document fields.
8096
8160
  * 4. Reject mismatched Root/Store/spec provenance before publishing a Catalog or document.
8161
+ * 5. Submit owned and referenced CLI enumeration lazily so one Catalog cannot monopolize CLI admission.
8097
8162
  *
8098
8163
  * Original request (2026-07-15): "Live and static modes share one source-aware Spec Catalog."
8164
+ * Original request (2026-07-31): "系统性地进行修复,因为List页面也有类似的问题。所有可能其它页面都有类似的问题。"
8099
8165
  */
8100
8166
  /** Exact compound identity was absent from the current direct project Catalog. */
8101
8167
  var SpecCatalogIdentityNotFoundError = class extends Error {
@@ -8108,7 +8174,9 @@ var SpecCatalogIdentityNotFoundError = class extends Error {
8108
8174
  /** Build one collision-safe Catalog from exact typed CLI lists. */
8109
8175
  async function readSpecCatalog(source, options = {}) {
8110
8176
  const rootSelector = source.rootContext.storeId !== null ? { store: source.rootContext.storeId } : {};
8111
- const [ownedList, references] = await Promise.all([source.contracts.listSpecs(rootSelector), Promise.all(source.rootContext.references.map((reference) => enumerateReference(source, reference)))]);
8177
+ const ownedList = await source.contracts.listSpecs(rootSelector);
8178
+ const references = [];
8179
+ for (const reference of source.rootContext.references) references.push(await enumerateReference(source, reference));
8112
8180
  const ownedError = ownedListContractError(source.rootContext, ownedList);
8113
8181
  if (!ownedList.success || !ownedList.data || ownedError) throw new CliProjectionCommandError(ownedError || ownedList.contractError || ownedList.stderr.trim() || "OpenSpec returned no owned Spec list.", ownedError ? {
8114
8182
  ...ownedList,
@@ -8255,7 +8323,7 @@ var PlanningCliRootContextState = class {
8255
8323
  function createPlanningCliProjectionWorkOwner(runtime) {
8256
8324
  return { registry: runtime.createRegistry() };
8257
8325
  }
8258
- function estimateSnapshotBytes$2(data) {
8326
+ function estimateSnapshotBytes$3(data) {
8259
8327
  return Buffer$1.byteLength(JSON.stringify(data) ?? "", "utf8");
8260
8328
  }
8261
8329
  /** Planning-root owner for all normal-path CLI-backed OPSX and Spec live projections. */
@@ -8359,7 +8427,7 @@ var PlanningCliProjectionService = class {
8359
8427
  identity: this.identity(selector),
8360
8428
  resourceClass: "cli",
8361
8429
  priority: "foreground",
8362
- estimateSnapshotBytes: estimateSnapshotBytes$2,
8430
+ estimateSnapshotBytes: estimateSnapshotBytes$3,
8363
8431
  load: async ({ reportStage }) => {
8364
8432
  const rootContext = this.currentRootContext();
8365
8433
  this.trackDependencies(selector);
@@ -8570,25 +8638,57 @@ async function listRecentGitEntries(options) {
8570
8638
  //#region src/dashboard-git-snapshot.ts
8571
8639
  async function collectCommitEntries(options) {
8572
8640
  const { worktreePath, defaultBranch, maxCommitEntries, runGit, readPathTimestampMs } = options;
8573
- return listRecentGitEntries({
8641
+ const entries = await listRecentGitEntries({
8574
8642
  worktreePath,
8575
8643
  defaultBranch,
8576
8644
  maxCommitEntries,
8577
8645
  runGit,
8578
8646
  readPathTimestampMs
8579
8647
  });
8648
+ const uncommitted = entries.find((entry) => entry.type === "uncommitted");
8649
+ const commits = entries.filter((entry) => entry.type === "commit");
8650
+ return uncommitted?.type === "uncommitted" && (uncommitted.diff.files > 0 || uncommitted.diff.insertions > 0 || uncommitted.diff.deletions > 0) ? [uncommitted, ...commits.slice(0, Math.max(0, maxCommitEntries - 1))] : commits.slice(0, maxCommitEntries);
8580
8651
  }
8581
8652
  async function collectWorktree(options) {
8582
- const { projectDir, worktree, defaultBranch, runGit, maxCommitEntries, readPathTimestampMs } = options;
8653
+ const { projectDir, worktree, defaultBranch, runGit, maxCommitEntries, readPathTimestampMs, pathAvailable } = options;
8583
8654
  const worktreePath = resolve(worktree.path);
8584
8655
  const resolvedProjectDir = resolve(projectDir);
8585
8656
  const isCurrent = await sameGitPath(worktreePath, resolvedProjectDir);
8586
- const pathAvailable = await pathExists(worktreePath);
8587
- const aheadBehindResult = await runGit(worktreePath, [
8588
- "rev-list",
8589
- "--left-right",
8590
- "--count",
8591
- `${defaultBranch}...HEAD`
8657
+ const isPathAvailable = await pathAvailable(worktreePath);
8658
+ const baseWorktree = {
8659
+ path: worktreePath,
8660
+ relativePath: relativePath(resolvedProjectDir, worktreePath),
8661
+ pathAvailable: isPathAvailable,
8662
+ branchName: parseBranchName(worktree.branchRef, worktree.detached),
8663
+ detached: worktree.detached,
8664
+ isCurrent
8665
+ };
8666
+ if (!isPathAvailable || worktree.detached) return {
8667
+ ...baseWorktree,
8668
+ ahead: 0,
8669
+ behind: 0,
8670
+ diff: EMPTY_DIFF,
8671
+ entries: []
8672
+ };
8673
+ const [aheadBehindResult, diffResult, entries] = await Promise.all([
8674
+ runGit(worktreePath, [
8675
+ "rev-list",
8676
+ "--left-right",
8677
+ "--count",
8678
+ `${defaultBranch}...HEAD`
8679
+ ]),
8680
+ runGit(worktreePath, [
8681
+ "diff",
8682
+ "--shortstat",
8683
+ `${defaultBranch}...HEAD`
8684
+ ]),
8685
+ isCurrent ? collectCommitEntries({
8686
+ worktreePath,
8687
+ defaultBranch,
8688
+ maxCommitEntries,
8689
+ runGit,
8690
+ readPathTimestampMs
8691
+ }) : Promise.resolve([])
8592
8692
  ]);
8593
8693
  let ahead = 0;
8594
8694
  let behind = 0;
@@ -8597,26 +8697,9 @@ async function collectWorktree(options) {
8597
8697
  ahead = Number(aheadRaw) || 0;
8598
8698
  behind = Number(behindRaw) || 0;
8599
8699
  }
8600
- const diffResult = await runGit(worktreePath, [
8601
- "diff",
8602
- "--shortstat",
8603
- `${defaultBranch}...HEAD`
8604
- ]);
8605
8700
  const diff = diffResult.ok ? parseShortStat(diffResult.stdout) : EMPTY_DIFF;
8606
- const entries = await collectCommitEntries({
8607
- worktreePath,
8608
- defaultBranch,
8609
- maxCommitEntries,
8610
- runGit,
8611
- readPathTimestampMs
8612
- });
8613
8701
  return {
8614
- path: worktreePath,
8615
- relativePath: relativePath(resolvedProjectDir, worktreePath),
8616
- pathAvailable,
8617
- branchName: parseBranchName(worktree.branchRef, worktree.detached),
8618
- detached: worktree.detached,
8619
- isCurrent,
8702
+ ...baseWorktree,
8620
8703
  ahead,
8621
8704
  behind,
8622
8705
  diff,
@@ -8645,18 +8728,21 @@ async function removeDetachedDashboardGitWorktree(options) {
8645
8728
  }
8646
8729
  async function buildDashboardGitSnapshot(options) {
8647
8730
  const runGit = options.runGit ?? defaultRunGit;
8648
- const maxCommitEntries = options.maxCommitEntries ?? 8;
8731
+ const maxCommitEntries = options.maxCommitEntries ?? 5;
8649
8732
  const readPathTimestampMs = options.readPathTimestampMs ?? defaultReadPathTimestampMs;
8733
+ const readPathAvailable = options.pathAvailable ?? pathExists;
8734
+ const runSnapshotGit = (cwd, args) => runGit(cwd, args, options.signal);
8650
8735
  const resolvedProjectDir = resolve(options.projectDir);
8651
- const defaultBranch = await resolveDefaultBranch(resolvedProjectDir, runGit);
8652
- const baseWorktrees = await listGitWorktrees(resolvedProjectDir, runGit);
8736
+ const defaultBranch = await resolveDefaultBranch(resolvedProjectDir, runSnapshotGit);
8737
+ const baseWorktrees = await listGitWorktrees(resolvedProjectDir, runSnapshotGit);
8653
8738
  const worktrees = await Promise.all(baseWorktrees.map((worktree) => collectWorktree({
8654
8739
  projectDir: resolvedProjectDir,
8655
8740
  worktree,
8656
8741
  defaultBranch,
8657
- runGit,
8742
+ runGit: runSnapshotGit,
8658
8743
  maxCommitEntries,
8659
- readPathTimestampMs
8744
+ readPathTimestampMs,
8745
+ pathAvailable: readPathAvailable
8660
8746
  })));
8661
8747
  worktrees.sort((a$1, b) => {
8662
8748
  if (a$1.isCurrent !== b.isCurrent) return a$1.isCurrent ? -1 : 1;
@@ -8672,13 +8758,15 @@ async function buildDashboardGitSnapshot(options) {
8672
8758
  //#endregion
8673
8759
  //#region src/dashboard-git-projection.ts
8674
8760
  /**
8675
- * Orthogonal intents (updated 2026-07-25 Asia/Shanghai):
8761
+ * Orthogonal intents (updated 2026-07-31 Asia/Shanghai):
8676
8762
  * 1. Load Dashboard Code Git snapshots independently from planning Summary and trends.
8677
8763
  * 2. Preserve backend-issued Code binding provenance and observable Git task lifecycle.
8678
8764
  * 3. Trigger explicit Git snapshot invalidation through a reactive stamp without broad Dashboard reloads.
8765
+ * 4. Propagate Projection Work cancellation into the Git subprocess tree.
8679
8766
  *
8680
8767
  * Original request (2026-07-23): "现在页面数据的加载数据非常慢(比如dashboard页面、changes页面都要等待非常久,页面刷新后,似乎后台没有缓存一样,也要加载很久。"
8681
8768
  * Derived requirement (2026-07-25): P4.3 settles a cached refresh stamp before Dashboard Git refresh returns.
8769
+ * Original request (2026-07-31): "Code Git Snapshot,它非常慢,有时候甚至要十几秒"
8682
8770
  */
8683
8771
  const execFileAsync$1 = promisify(execFile);
8684
8772
  const DASHBOARD_GIT_REFRESH_STAMP_NAME = "openspecui-dashboard-git-refresh.stamp";
@@ -8748,14 +8836,15 @@ async function touchDashboardGitRefreshStamp(projectDir, reason) {
8748
8836
  return { skipped: false };
8749
8837
  }
8750
8838
  /** Load one Code Git snapshot and retain any failure as Git-region evidence only. */
8751
- async function loadDashboardGitProjection(ctx, reason = "dashboard-git") {
8839
+ async function loadDashboardGitProjection(ctx, reason = "dashboard-git", signal) {
8752
8840
  beginDashboardGitTask(reason);
8753
8841
  try {
8754
8842
  const gitMetadataDir = await resolveGitMetadataDir(ctx.projectDir);
8755
8843
  if (gitMetadataDir) await reactiveReadFile(join(gitMetadataDir, DASHBOARD_GIT_REFRESH_STAMP_NAME));
8756
8844
  const snapshot = await buildDashboardGitSnapshot({
8757
8845
  projectDir: ctx.projectDir,
8758
- bindingToken: ctx.codeBindingToken
8846
+ bindingToken: ctx.codeBindingToken,
8847
+ signal
8759
8848
  });
8760
8849
  endDashboardGitTask(null);
8761
8850
  return snapshot;
@@ -9204,6 +9293,19 @@ async function loadDashboardOverview(ctx, reason = "dashboard-refresh") {
9204
9293
 
9205
9294
  //#endregion
9206
9295
  //#region src/entity-read-options.ts
9296
+ /**
9297
+ * Orthogonal intents (updated 2026-07-31 Asia/Shanghai):
9298
+ * 1. Resolve the OPSX Schema detail and diagnostic metadata for one change/archive entity.
9299
+ * 2. Delegate schema resolution through the Kernel ensure contract while tolerating missing schemas.
9300
+ * 3. Never block on Kernel warmup: schema diagnostics are optional display enrichment, so return
9301
+ * cached/empty data immediately and let reactive invalidation fill them after warmup settles.
9302
+ *
9303
+ * Original request (2026-07-15): "为不同命令建立强类型适配器,不实现平行解析规则。"
9304
+ * Performance correction (2026-07-23): Status must not block on Kernel warmup; schema diagnostics are optional.
9305
+ * Hotpath correction (2026-07-31): `buildEntityReadOptions` removed `waitForWarmup()` blocking because
9306
+ * `fetchSchemas` (~1.2s) dominated detail-page latency. Schema data is now read opportunistically
9307
+ * from whatever the Kernel has already cached; reactive invalidation fills the rest.
9308
+ */
9207
9309
  async function readEntityMetadata(ctx, stage, id) {
9208
9310
  return (stage === "change" ? await ctx.adapter.readChangeFiles(id) : await ctx.adapter.readArchivedChangeFiles(id)).find((file) => file.type === "file" && file.path === ".openspec.yaml")?.content ?? null;
9209
9311
  }
@@ -9211,14 +9313,12 @@ async function buildEntityReadOptions(ctx, stage, id) {
9211
9313
  const schemaName = parseOpsxEntityMetadata(await readEntityMetadata(ctx, stage, id)).schemaName;
9212
9314
  if (!schemaName) return {};
9213
9315
  try {
9214
- await ctx.kernel.waitForWarmup();
9215
- await ctx.kernel.ensureSchemaDetail(schemaName);
9216
- await ctx.kernel.ensureSchemaYaml(schemaName);
9217
- const schemaYaml = ctx.kernel.getSchemaYaml(schemaName);
9316
+ const schemaDetail = ctx.kernel.tryGetSchemaDetail(schemaName);
9317
+ const schemaYaml = ctx.kernel.tryGetSchemaYaml(schemaName);
9218
9318
  const diagnostics = schemaYaml ? parseOpsxSchemaDetail(schemaYaml, schemaName, { path: `schema:${schemaName}` }).diagnostics : [];
9219
9319
  return {
9220
- schemas: { [schemaName]: ctx.kernel.getSchemaDetail(schemaName) },
9221
- schemaDiagnostics: diagnostics.length > 0 ? { [schemaName]: diagnostics } : void 0
9320
+ ...schemaDetail ? { schemas: { [schemaName]: schemaDetail } } : {},
9321
+ ...diagnostics.length > 0 ? { schemaDiagnostics: { [schemaName]: diagnostics } } : {}
9222
9322
  };
9223
9323
  } catch {
9224
9324
  return { schemas: {} };
@@ -9402,6 +9502,197 @@ var FilePreviewService = class {
9402
9502
  }
9403
9503
  };
9404
9504
 
9505
+ //#endregion
9506
+ //#region src/read-write-lock.ts
9507
+ /**
9508
+ * Orthogonal intents (created 2026-07-31 Asia/Shanghai):
9509
+ * 1. Provide a writer-preference async read-write lock for PlanningRootServiceManager.
9510
+ * 2. Multiple readers can hold the lock concurrently; writers get exclusive access.
9511
+ * 3. Writer preference prevents write starvation: when a writer is waiting, new readers queue.
9512
+ * 4. Export queue-to-release OTel evidence with explicit source stacks and reader blockers.
9513
+ *
9514
+ * Original request (2026-07-31): "使用读写锁的概念进行改进:Read 并行,Write 独占"
9515
+ * Original request (2026-07-31): "把所有write-lock打印上时间日志和来源堆栈,使用otel来进行trace。"
9516
+ *
9517
+ * Design: a single Promise chain resolves readers in batches and writers one-at-a-time.
9518
+ * Writers wait for all active readers to finish; readers arriving while a writer is pending
9519
+ * (or active) wait behind that writer. This guarantees writers are not starved by a continuous
9520
+ * stream of readers, at the cost of slightly higher reader latency when a write is pending.
9521
+ */
9522
+ /** Convert an application Tracer into the narrow lock instrumentation boundary. */
9523
+ function createLockTracer(tracer) {
9524
+ return { startActiveSpan: (name, options, callback) => tracer.startActiveSpan(name, options, (span) => callback(span)) };
9525
+ }
9526
+ function timestampAttributes(timestamp) {
9527
+ return {
9528
+ "lock.timestamp": new Date(timestamp).toISOString(),
9529
+ "lock.timestamp_unix_ms": timestamp
9530
+ };
9531
+ }
9532
+ var AsyncReadWriteLock = class {
9533
+ name;
9534
+ tracer;
9535
+ nextOperationId = 1;
9536
+ activeReaders = 0;
9537
+ activeWriter = null;
9538
+ waitingReaders = [];
9539
+ waitingWriters = [];
9540
+ constructor(options = { name: "async-read-write-lock" }) {
9541
+ this.name = options.name;
9542
+ this.tracer = options.tracer ?? createLockTracer(trace.getTracer("openspecui-server", "lock-instrumentation"));
9543
+ }
9544
+ /**
9545
+ * Acquire a shared (read) lock and run `fn`. Multiple read locks can be held simultaneously.
9546
+ * If a writer is waiting or active, this queues behind it (writer preference).
9547
+ */
9548
+ async withReadLock(source, fn) {
9549
+ const operation = this.createOperation(source);
9550
+ return this.tracer.startActiveSpan("lock.read", {
9551
+ attributes: this.baseAttributes("read", operation),
9552
+ startTime: operation.queuedAt
9553
+ }, async (span) => {
9554
+ const blockers = await this.acquireRead();
9555
+ const acquiredAt = Date.now();
9556
+ const waitMs = acquiredAt - operation.queuedAt;
9557
+ span.setAttributes({
9558
+ "lock.blocked": blockers.length > 0,
9559
+ "lock.wait_ms": waitMs
9560
+ });
9561
+ if (blockers.length > 0) {
9562
+ const firstBlocker = blockers[0];
9563
+ if (firstBlocker) span.setAttributes({
9564
+ "lock.blocked_by.source": firstBlocker.source,
9565
+ "lock.blocked_by.stack": firstBlocker.stack,
9566
+ "lock.blocked_by.wait_ms": waitMs,
9567
+ "lock.blocked_by.sources": blockers.map((blocker) => blocker.source),
9568
+ "lock.blocked_by.stacks": blockers.map((blocker) => blocker.stack)
9569
+ });
9570
+ }
9571
+ span.addEvent("read-lock.acquired", timestampAttributes(acquiredAt), acquiredAt);
9572
+ try {
9573
+ return await fn();
9574
+ } catch (error) {
9575
+ this.recordError(span, error);
9576
+ throw error;
9577
+ } finally {
9578
+ this.releaseRead();
9579
+ const releasedAt = Date.now();
9580
+ span.setAttribute("lock.hold_ms", releasedAt - acquiredAt);
9581
+ span.addEvent("read-lock.released", timestampAttributes(releasedAt), releasedAt);
9582
+ span.end(releasedAt);
9583
+ }
9584
+ });
9585
+ }
9586
+ /**
9587
+ * Acquire an exclusive (write) lock and run `fn`. Waits for all active readers/writers to finish.
9588
+ * While active, new readers and writers queue behind this writer.
9589
+ */
9590
+ async withWriteLock(source, fn) {
9591
+ const operation = this.createOperation(source);
9592
+ return this.tracer.startActiveSpan("lock.write", {
9593
+ attributes: this.baseAttributes("write", operation),
9594
+ startTime: operation.queuedAt
9595
+ }, async (span) => {
9596
+ span.addEvent("write-lock.queued", {
9597
+ ...timestampAttributes(operation.queuedAt),
9598
+ "lock.active_readers": this.activeReaders,
9599
+ "lock.waiting_readers": this.waitingReaders.length,
9600
+ "lock.waiting_writers": this.waitingWriters.length
9601
+ }, operation.queuedAt);
9602
+ await this.acquireWrite(operation);
9603
+ const acquiredAt = Date.now();
9604
+ span.setAttribute("lock.wait_ms", acquiredAt - operation.queuedAt);
9605
+ span.addEvent("write-lock.acquired", timestampAttributes(acquiredAt), acquiredAt);
9606
+ try {
9607
+ return await fn();
9608
+ } catch (error) {
9609
+ this.recordError(span, error);
9610
+ throw error;
9611
+ } finally {
9612
+ this.releaseWrite();
9613
+ const releasedAt = Date.now();
9614
+ span.setAttribute("lock.hold_ms", releasedAt - acquiredAt);
9615
+ span.addEvent("write-lock.released", timestampAttributes(releasedAt), releasedAt);
9616
+ span.end(releasedAt);
9617
+ }
9618
+ });
9619
+ }
9620
+ createOperation(source) {
9621
+ return {
9622
+ ...source,
9623
+ id: this.nextOperationId++,
9624
+ queuedAt: Date.now()
9625
+ };
9626
+ }
9627
+ baseAttributes(mode, operation) {
9628
+ return {
9629
+ "lock.name": this.name,
9630
+ "lock.mode": mode,
9631
+ "lock.source": operation.source,
9632
+ "lock.stack": operation.stack,
9633
+ "lock.queue_started_at": new Date(operation.queuedAt).toISOString(),
9634
+ "lock.queue_started_at_unix_ms": operation.queuedAt
9635
+ };
9636
+ }
9637
+ recordError(span, error) {
9638
+ const message = error instanceof Error ? error.message : String(error);
9639
+ span.recordException(error instanceof Error ? error : message);
9640
+ span.setStatus({
9641
+ code: SpanStatusCode.ERROR,
9642
+ message
9643
+ });
9644
+ }
9645
+ acquireRead() {
9646
+ if (this.activeWriter === null && this.waitingWriters.length === 0) {
9647
+ this.activeReaders += 1;
9648
+ return Promise.resolve([]);
9649
+ }
9650
+ const blockers = [...this.activeWriter ? [this.activeWriter] : [], ...this.waitingWriters.map((writer) => writer.operation)];
9651
+ return new Promise((resolve$1) => {
9652
+ this.waitingReaders.push({
9653
+ resolve: () => resolve$1(blockers),
9654
+ blockers
9655
+ });
9656
+ });
9657
+ }
9658
+ releaseRead() {
9659
+ this.activeReaders -= 1;
9660
+ if (this.activeReaders === 0 && this.waitingWriters.length > 0) this.drainNextWriter();
9661
+ }
9662
+ acquireWrite(operation) {
9663
+ if (this.activeWriter === null && this.activeReaders === 0 && this.waitingWriters.length === 0) {
9664
+ this.activeWriter = operation;
9665
+ return Promise.resolve();
9666
+ }
9667
+ for (const reader of this.waitingReaders) reader.blockers.push(operation);
9668
+ return new Promise((resolve$1) => {
9669
+ this.waitingWriters.push({
9670
+ resolve: resolve$1,
9671
+ operation
9672
+ });
9673
+ });
9674
+ }
9675
+ releaseWrite() {
9676
+ this.activeWriter = null;
9677
+ if (this.waitingWriters.length > 0) {
9678
+ this.drainNextWriter();
9679
+ return;
9680
+ }
9681
+ this.drainWaitingReaders();
9682
+ }
9683
+ drainNextWriter() {
9684
+ const writer = this.waitingWriters.shift();
9685
+ if (!writer) return;
9686
+ this.activeWriter = writer.operation;
9687
+ writer.resolve();
9688
+ }
9689
+ drainWaitingReaders() {
9690
+ const readers = this.waitingReaders.splice(0);
9691
+ this.activeReaders += readers.length;
9692
+ for (const reader of readers) reader.resolve();
9693
+ }
9694
+ };
9695
+
9405
9696
  //#endregion
9406
9697
  //#region src/root-context-service.ts
9407
9698
  /**
@@ -10195,9 +10486,9 @@ var WorkflowInvocationService = class {
10195
10486
  //#endregion
10196
10487
  //#region src/planning-root-service.ts
10197
10488
  /**
10198
- * Orthogonal intents (updated 2026-07-27 Asia/Shanghai):
10489
+ * Orthogonal intents (updated 2026-07-31 Asia/Shanghai):
10199
10490
  * 1. Own every root-scoped operation, CLI projection, and project-Schema mutation for the selected Planning root.
10200
- * 2. Serialize replacement and issue fresh record/generation provenance without reconstructing root selection.
10491
+ * 2. Single-flight Root resolution outside traced locks, then serialize short generation-checked record commits.
10201
10492
  * 3. Acquire and retire observation/invalidation leases with each active root.
10202
10493
  * 4. Keep reactive subscriptions and current-generation snapshots bound to Root Context dependencies and selection.
10203
10494
  * 5. Revoke leased service capabilities, cancel retiring streams outside blocked transitions, and
@@ -10210,6 +10501,10 @@ var WorkflowInvocationService = class {
10210
10501
  * Original request (2026-07-23): "现在页面数据的加载数据非常慢(比如dashboard页面、changes页面都要等待非常久,页面刷新后,似乎后台没有缓存一样,也要加载很久。"
10211
10502
  * Original request (2026-07-26): "展开全面的接口升级和内核升级和测试升级。"
10212
10503
  * Owner architecture clarification (2026-07-26): "将这些变更信息收集起来作为触发器,更新底层幂等计算的缓存结果。"
10504
+ * Built-runtime correction (2026-07-30): retiring a Planning root must retire its buffered CLI children.
10505
+ * Original request (2026-07-31): "把所有write-lock打印上时间日志和来源堆栈,使用otel来进行trace。"
10506
+ * Owner diagnosis (2026-07-31): readonly cache misses must merge before lock admission, and slow async work must remain outside write locks.
10507
+ * Full-gate correction (2026-07-31): imperative snapshots cannot become reactive replay authority before dependency-tracked validation.
10213
10508
  */
10214
10509
  /** Root resolution failure that retains the complete failed Root Context state. */
10215
10510
  var PlanningRootUnavailableError = class extends Error {
@@ -10225,7 +10520,9 @@ var PlanningRootServiceManager = class {
10225
10520
  activeRecord = null;
10226
10521
  currentRootContextSnapshot = null;
10227
10522
  retiringRecords = /* @__PURE__ */ new Set();
10228
- transitionTail = Promise.resolve();
10523
+ rootTransitionFlights = /* @__PURE__ */ new Map();
10524
+ rootTransitionCommitTail = Promise.resolve();
10525
+ transitionLock;
10229
10526
  disposePromise = null;
10230
10527
  disposed = false;
10231
10528
  dashboardProjectionWorkOwner;
@@ -10234,6 +10531,10 @@ var PlanningRootServiceManager = class {
10234
10531
  ownedProjectionWorkRuntime;
10235
10532
  constructor(options) {
10236
10533
  this.options = options;
10534
+ this.transitionLock = new AsyncReadWriteLock({
10535
+ name: "planning-root-transition",
10536
+ tracer: createLockTracer(options.tracer ?? trace.getTracer("openspecui-server"))
10537
+ });
10237
10538
  if (options.dashboardProjectionWorkOwner && options.changesProjectionWorkOwner && options.planningCliProjectionWorkOwner) {
10238
10539
  this.dashboardProjectionWorkOwner = options.dashboardProjectionWorkOwner;
10239
10540
  this.changesProjectionWorkOwner = options.changesProjectionWorkOwner;
@@ -10295,10 +10596,10 @@ var PlanningRootServiceManager = class {
10295
10596
  adapter,
10296
10597
  configManager: this.options.configManager
10297
10598
  }),
10298
- loadGit: () => loadDashboardGitProjection({
10599
+ loadGit: (signal) => loadDashboardGitProjection({
10299
10600
  projectDir: this.options.launchProjectDir,
10300
10601
  codeBindingToken: this.options.codeBinding.bindingToken
10301
- }, "projection-work")
10602
+ }, "projection-work", signal)
10302
10603
  }
10303
10604
  });
10304
10605
  const changesProjectionService = new ChangesProjectionService({
@@ -10340,6 +10641,7 @@ var PlanningRootServiceManager = class {
10340
10641
  });
10341
10642
  return {
10342
10643
  identity: this.rootIdentity(rootContext),
10644
+ rootCliExecutor,
10343
10645
  gitBindingToken,
10344
10646
  schemaMutationService,
10345
10647
  rootContext,
@@ -10363,10 +10665,23 @@ var PlanningRootServiceManager = class {
10363
10665
  disposePromise: null
10364
10666
  };
10365
10667
  }
10366
- runTransition(task) {
10367
- const result = this.transitionTail.then(task, task);
10368
- this.transitionTail = result.then(() => void 0, () => void 0);
10369
- return result;
10668
+ /** Run one short state commit under the exclusive transition lock. */
10669
+ runWriteTransition(source, task) {
10670
+ return this.transitionLock.withWriteLock({
10671
+ source,
10672
+ stack: (/* @__PURE__ */ new Error(`Write lock source: ${source}`)).stack ?? source
10673
+ }, task);
10674
+ }
10675
+ /**
10676
+ * Run `task` under a shared (read) lock. Used for cache-hit fast paths where only
10677
+ * `activeRecord` and `currentRootContextSnapshot` are read. Multiple read transitions
10678
+ * run concurrently; a pending writer blocks new readers.
10679
+ */
10680
+ runReadTransition(source, task) {
10681
+ return this.transitionLock.withReadLock({
10682
+ source,
10683
+ stack: (/* @__PURE__ */ new Error(`Read lock source: ${source}`)).stack ?? source
10684
+ }, task);
10370
10685
  }
10371
10686
  async disposeRecord(record) {
10372
10687
  if (record.disposePromise) return record.disposePromise;
@@ -10379,6 +10694,7 @@ var PlanningRootServiceManager = class {
10379
10694
  Promise.resolve().then(() => record.dashboardProjectionService.dispose()),
10380
10695
  Promise.resolve().then(() => record.changesProjectionService.dispose()),
10381
10696
  Promise.resolve().then(() => record.planningCliProjectionService.dispose()),
10697
+ Promise.resolve().then(() => record.rootCliExecutor.dispose()),
10382
10698
  record.hookRuntime.dispose(),
10383
10699
  record.searchService.dispose(),
10384
10700
  record.observationRelease.then((releaseObservationRoot) => releaseObservationRoot?.())
@@ -10491,96 +10807,290 @@ var PlanningRootServiceManager = class {
10491
10807
  observedAt: attempt.observedAt
10492
10808
  };
10493
10809
  }
10494
- async activate(state) {
10810
+ traceRootContextCacheHit() {
10811
+ (this.options.tracer ?? trace.getTracer("openspecui-server")).startActiveSpan("planningRoot.resolveRootContext", (span) => {
10812
+ span.setAttribute("rootContext.cacheHit", true);
10813
+ span.end();
10814
+ });
10815
+ }
10816
+ resolvedStateForRecord(state, record) {
10817
+ return {
10818
+ ...state,
10819
+ data: record.rootContext
10820
+ };
10821
+ }
10822
+ enqueueRootTransitionCommit(task) {
10823
+ const result = this.rootTransitionCommitTail.then(task, task);
10824
+ this.rootTransitionCommitTail = result.then(() => void 0, () => void 0);
10825
+ return result;
10826
+ }
10827
+ getRootTransition(invalidationKey) {
10828
+ const existing = this.rootTransitionFlights.get(invalidationKey);
10829
+ if (existing) return {
10830
+ transition: existing,
10831
+ joined: true
10832
+ };
10833
+ const transition = this.resolveAndCommitRootTransition(invalidationKey);
10834
+ this.rootTransitionFlights.set(invalidationKey, transition);
10835
+ const clear = () => {
10836
+ if (this.rootTransitionFlights.get(invalidationKey) === transition) this.rootTransitionFlights.delete(invalidationKey);
10837
+ };
10838
+ transition.then(clear, clear);
10839
+ return {
10840
+ transition,
10841
+ joined: false
10842
+ };
10843
+ }
10844
+ awaitRootTransition(invalidationKey) {
10845
+ return (this.options.tracer ?? trace.getTracer("openspecui-server")).startActiveSpan("planningRoot.transition.wait", async (span) => {
10846
+ const { transition, joined } = this.getRootTransition(invalidationKey);
10847
+ span.setAttribute("rootContext.invalidationKey", invalidationKey);
10848
+ span.setAttribute("rootContext.singleFlight.joined", joined);
10849
+ try {
10850
+ const result = await transition;
10851
+ span.setAttribute("rootContext.transition.status", result.status);
10852
+ return result;
10853
+ } finally {
10854
+ span.end();
10855
+ }
10856
+ });
10857
+ }
10858
+ async resolveAndCommitRootTransition(invalidationKey) {
10859
+ if (this.disposed) throw new Error("Planning-root service manager is disposed.");
10860
+ const state = await (this.options.tracer ?? trace.getTracer("openspecui-server")).startActiveSpan("planningRoot.resolveRootContext", async (span) => {
10861
+ span.setAttribute("rootContext.cacheHit", false);
10862
+ span.setAttribute("rootContext.invalidationKey", invalidationKey);
10863
+ try {
10864
+ return await resolveServerRootContext({
10865
+ projectDir: this.options.launchProjectDir,
10866
+ cliExecutor: this.options.cliExecutor
10867
+ });
10868
+ } finally {
10869
+ span.end();
10870
+ }
10871
+ });
10872
+ try {
10873
+ return await this.enqueueRootTransitionCommit(() => this.commitRootTransition(invalidationKey, state));
10874
+ } catch (error) {
10875
+ return {
10876
+ status: "current",
10877
+ state: this.lifecycleErrorState(state, error),
10878
+ services: null
10879
+ };
10880
+ }
10881
+ }
10882
+ async commitRootTransition(invalidationKey, state) {
10883
+ if (this.disposed) throw new Error("Planning-root service manager is disposed.");
10884
+ if (this.currentRootContextInvalidationKey() !== invalidationKey) return { status: "stale" };
10495
10885
  if (state.state === "error") {
10496
- const previous = this.activeRecord;
10497
- this.activeRecord = null;
10498
- if (previous) await this.retireRecord(previous);
10499
- return null;
10886
+ const detached = await this.runWriteTransition("root-transition.commit", () => {
10887
+ if (this.disposed) throw new Error("Planning-root service manager is disposed.");
10888
+ if (this.currentRootContextInvalidationKey() !== invalidationKey) return {
10889
+ status: "stale",
10890
+ record: null
10891
+ };
10892
+ const record = this.activeRecord;
10893
+ this.activeRecord = null;
10894
+ this.currentRootContextSnapshot = null;
10895
+ if (record) this.retiringRecords.add(record);
10896
+ return {
10897
+ status: "current",
10898
+ record
10899
+ };
10900
+ });
10901
+ if (detached.status === "stale") return detached;
10902
+ if (detached.record) await this.retireRecord(detached.record);
10903
+ if (this.currentRootContextInvalidationKey() !== invalidationKey) return { status: "stale" };
10904
+ return {
10905
+ status: "current",
10906
+ state,
10907
+ services: null
10908
+ };
10500
10909
  }
10501
- if (!state.data.planningRoot) throw new Error("Root Context reached ready state without a planning root.");
10910
+ if (!state.data.planningRoot) return {
10911
+ status: "current",
10912
+ state: this.lifecycleErrorState(state, "Root Context reached ready state without a planning root."),
10913
+ services: null
10914
+ };
10915
+ const identity = this.rootIdentity(state.data);
10502
10916
  const active = this.activeRecord;
10503
- if (active?.identity === this.rootIdentity(state.data)) {
10917
+ if (active?.identity === identity) return this.runWriteTransition("root-transition.commit", () => {
10918
+ if (this.disposed) throw new Error("Planning-root service manager is disposed.");
10919
+ if (this.currentRootContextInvalidationKey() !== invalidationKey) return { status: "stale" };
10920
+ if (this.activeRecord !== active) return { status: "stale" };
10504
10921
  const currentRootContext = {
10505
10922
  ...state.data,
10506
10923
  generation: active.gitBindingToken
10507
10924
  };
10508
10925
  active.rootContext = currentRootContext;
10509
10926
  active.rootContextValue.set(currentRootContext);
10510
- return active;
10511
- }
10512
- this.activeRecord = null;
10513
- if (active) await this.retireRecord(active);
10514
- if (this.disposed) throw new Error("Planning-root service manager is disposed.");
10515
- const created = this.createRecord({
10516
- ...state.data,
10517
- generation: state.data.generation ?? randomUUID()
10518
- });
10519
- this.activeRecord = created;
10520
- return created;
10521
- }
10522
- async resolveActiveTransition(reactive, useRuntimeInvalidationCache = true) {
10523
- if (this.disposed) throw new Error("Planning-root service manager is disposed.");
10524
- const invalidationKey = useRuntimeInvalidationCache ? this.currentRootContextInvalidationKey() : null;
10525
- const cached = this.currentRootContextSnapshot;
10526
- if (reactive && invalidationKey !== null && cached?.invalidationKey === invalidationKey && cached.services === this.activeRecord) {
10527
- if (reactive) await trackRootContextDependencies({ projectDir: this.options.launchProjectDir }, cached.state);
10927
+ const resolvedState = this.resolvedStateForRecord(state, active);
10928
+ this.currentRootContextSnapshot = {
10929
+ invalidationKey,
10930
+ state: resolvedState,
10931
+ services: active,
10932
+ reactiveReplayEligible: false
10933
+ };
10528
10934
  return {
10529
- state: cached.state,
10530
- services: cached.services
10935
+ status: "current",
10936
+ state: resolvedState,
10937
+ services: active
10531
10938
  };
10532
- }
10533
- this.currentRootContextSnapshot = null;
10534
- const state = await resolveServerRootContext({
10535
- projectDir: this.options.launchProjectDir,
10536
- cliExecutor: this.options.cliExecutor
10537
10939
  });
10538
- if (reactive) await trackRootContextDependencies({ projectDir: this.options.launchProjectDir }, state);
10940
+ if (active) {
10941
+ if (!await this.runWriteTransition("root-transition.detach", () => {
10942
+ if (this.disposed) throw new Error("Planning-root service manager is disposed.");
10943
+ if (this.currentRootContextInvalidationKey() !== invalidationKey) return false;
10944
+ if (this.activeRecord !== active) return false;
10945
+ this.activeRecord = null;
10946
+ this.currentRootContextSnapshot = null;
10947
+ this.retiringRecords.add(active);
10948
+ return true;
10949
+ })) return { status: "stale" };
10950
+ await this.retireRecord(active);
10951
+ if (this.currentRootContextInvalidationKey() !== invalidationKey) return { status: "stale" };
10952
+ }
10953
+ if (this.disposed) throw new Error("Planning-root service manager is disposed.");
10954
+ let created;
10539
10955
  try {
10540
- const services = await this.activate(state);
10541
- if (state.state === "ready" && services) {
10542
- const resolvedState = {
10543
- ...state,
10544
- data: services.rootContext
10545
- };
10546
- if (reactive && invalidationKey !== null && this.currentRootContextInvalidationKey() === invalidationKey) this.currentRootContextSnapshot = {
10547
- invalidationKey,
10548
- state: resolvedState,
10549
- services
10550
- };
10551
- return {
10552
- state: resolvedState,
10553
- services
10554
- };
10555
- }
10556
- return {
10557
- state,
10558
- services
10559
- };
10956
+ created = this.createRecord({
10957
+ ...state.data,
10958
+ generation: state.data.generation ?? randomUUID()
10959
+ });
10560
10960
  } catch (error) {
10561
10961
  return {
10962
+ status: "current",
10562
10963
  state: this.lifecycleErrorState(state, error),
10563
10964
  services: null
10564
10965
  };
10565
10966
  }
10967
+ let committed;
10968
+ try {
10969
+ committed = await this.runWriteTransition("root-transition.commit", () => {
10970
+ if (this.disposed) throw new Error("Planning-root service manager is disposed.");
10971
+ if (this.currentRootContextInvalidationKey() !== invalidationKey) return false;
10972
+ if (this.activeRecord !== null) return false;
10973
+ this.activeRecord = created;
10974
+ this.currentRootContextSnapshot = {
10975
+ invalidationKey,
10976
+ state: this.resolvedStateForRecord(state, created),
10977
+ services: created,
10978
+ reactiveReplayEligible: false
10979
+ };
10980
+ return true;
10981
+ });
10982
+ } catch (error) {
10983
+ await this.retireRecord(created);
10984
+ throw error;
10985
+ }
10986
+ if (!committed) {
10987
+ await this.retireRecord(created);
10988
+ return { status: "stale" };
10989
+ }
10990
+ return {
10991
+ status: "current",
10992
+ state: this.resolvedStateForRecord(state, created),
10993
+ services: created
10994
+ };
10995
+ }
10996
+ readCurrentSnapshot(invalidationKey) {
10997
+ return this.runReadTransition("root-transition.cache-check", () => {
10998
+ if (this.disposed) throw new Error("Planning-root service manager is disposed.");
10999
+ const cached = this.currentRootContextSnapshot;
11000
+ if (cached?.invalidationKey !== invalidationKey || cached.services !== this.activeRecord) return null;
11001
+ return cached;
11002
+ });
11003
+ }
11004
+ async trackReactiveRootContext(state) {
11005
+ await trackRootContextDependencies({ projectDir: this.options.launchProjectDir }, state);
11006
+ }
11007
+ promoteReactiveSnapshot(invalidationKey, services) {
11008
+ const snapshot = this.currentRootContextSnapshot;
11009
+ if (!services || snapshot?.invalidationKey !== invalidationKey || snapshot.services !== services || this.activeRecord !== services) return;
11010
+ snapshot.reactiveReplayEligible = true;
10566
11011
  }
10567
- resolveTransition(reactive, useRuntimeInvalidationCache = true) {
10568
- return this.runTransition(() => this.resolveActiveTransition(reactive, useRuntimeInvalidationCache));
11012
+ async resolveTransition(reactive, useRuntimeInvalidationCache = true, allowStaleCache = false) {
11013
+ const cacheEligible = reactive || allowStaleCache;
11014
+ while (true) {
11015
+ if (this.disposed) throw new Error("Planning-root service manager is disposed.");
11016
+ const invalidationKey = this.currentRootContextInvalidationKey();
11017
+ if (useRuntimeInvalidationCache && cacheEligible) {
11018
+ const cached = await this.readCurrentSnapshot(invalidationKey);
11019
+ if (cached && (!reactive || cached.reactiveReplayEligible)) {
11020
+ this.traceRootContextCacheHit();
11021
+ if (reactive) await this.trackReactiveRootContext(cached.state);
11022
+ return {
11023
+ state: cached.state,
11024
+ services: cached.services
11025
+ };
11026
+ }
11027
+ }
11028
+ const transition = await this.awaitRootTransition(invalidationKey);
11029
+ if (transition.status === "stale") continue;
11030
+ if (reactive) {
11031
+ await this.trackReactiveRootContext(transition.state);
11032
+ this.promoteReactiveSnapshot(invalidationKey, transition.services);
11033
+ }
11034
+ return transition;
11035
+ }
10569
11036
  }
10570
11037
  requireActiveServices(result) {
10571
11038
  if (result.state.state === "error" || !result.services) throw new PlanningRootUnavailableError(result.state.state === "error" ? result.state : this.lifecycleErrorState(result.state, "Planning-root services are unavailable."));
10572
11039
  return result.services;
10573
11040
  }
10574
- acquireOperation(reactive) {
10575
- return this.runTransition(async () => {
10576
- if (this.disposed) throw new Error("Planning-root service manager is disposed.");
10577
- const record = this.requireActiveServices(await this.resolveActiveTransition(reactive));
10578
- if (this.disposed) throw new Error("Planning-root service manager is disposed.");
10579
- return {
10580
- lease: this.createOperationLease(record),
10581
- record
10582
- };
10583
- });
11041
+ async acquireOperation(reactive, allowStaleCache = false) {
11042
+ const cacheEligible = reactive || allowStaleCache;
11043
+ let acceptNewerSnapshot = false;
11044
+ while (true) {
11045
+ const invalidationKey = this.currentRootContextInvalidationKey();
11046
+ const cached = cacheEligible || acceptNewerSnapshot ? await this.runReadTransition("acquire-operation.cache-check", () => {
11047
+ if (this.disposed) throw new Error("Planning-root service manager is disposed.");
11048
+ const snapshot = this.currentRootContextSnapshot;
11049
+ if (snapshot?.invalidationKey !== invalidationKey || snapshot.services !== this.activeRecord || reactive && !snapshot.reactiveReplayEligible) return null;
11050
+ return {
11051
+ state: snapshot.state,
11052
+ lease: this.createOperationLease(snapshot.services),
11053
+ record: snapshot.services
11054
+ };
11055
+ }) : null;
11056
+ if (cached) try {
11057
+ this.traceRootContextCacheHit();
11058
+ if (reactive) await this.trackReactiveRootContext(cached.state);
11059
+ return {
11060
+ lease: cached.lease,
11061
+ record: cached.record
11062
+ };
11063
+ } catch (error) {
11064
+ await cached.lease.release();
11065
+ throw error;
11066
+ }
11067
+ const transition = await this.awaitRootTransition(invalidationKey);
11068
+ if (transition.status === "stale") {
11069
+ acceptNewerSnapshot = true;
11070
+ continue;
11071
+ }
11072
+ const admitted = await this.runReadTransition("acquire-operation.commit", () => {
11073
+ if (this.disposed) throw new Error("Planning-root service manager is disposed.");
11074
+ if (this.currentRootContextInvalidationKey() !== invalidationKey) return null;
11075
+ const record = this.requireActiveServices(transition);
11076
+ if (this.activeRecord !== record) return null;
11077
+ return {
11078
+ lease: this.createOperationLease(record),
11079
+ record
11080
+ };
11081
+ });
11082
+ if (!admitted) continue;
11083
+ try {
11084
+ if (reactive) {
11085
+ await this.trackReactiveRootContext(transition.state);
11086
+ this.promoteReactiveSnapshot(invalidationKey, admitted.record);
11087
+ }
11088
+ return admitted;
11089
+ } catch (error) {
11090
+ await admitted.lease.release();
11091
+ throw error;
11092
+ }
11093
+ }
10584
11094
  }
10585
11095
  /** Resolve Root Context only after the active Planning-root lifecycle has converged. */
10586
11096
  async resolveRootContext() {
@@ -10595,8 +11105,8 @@ var PlanningRootServiceManager = class {
10595
11105
  return (await this.resolveTransition(true, false)).state;
10596
11106
  }
10597
11107
  /** Run one complete operation without exposing a durable service handle. */
10598
- async runOperation(operation) {
10599
- const { lease } = await this.acquireOperation(false);
11108
+ async runOperation(operation, allowStaleCache = false) {
11109
+ const { lease } = await this.acquireOperation(false, allowStaleCache);
10600
11110
  try {
10601
11111
  return await operation(lease.services);
10602
11112
  } finally {
@@ -10613,39 +11123,35 @@ var PlanningRootServiceManager = class {
10613
11123
  }
10614
11124
  }
10615
11125
  /** Retain a revocable operation capability until the child process actually settles. */
10616
- startOperationStream(operation) {
10617
- return this.runTransition(async () => {
10618
- if (this.disposed) throw new Error("Planning-root service manager is disposed.");
10619
- const record = this.requireActiveServices(await this.resolveActiveTransition(false));
11126
+ async startOperationStream(operation) {
11127
+ const { lease, record } = await this.acquireOperation(false);
11128
+ try {
10620
11129
  if (this.disposed) throw new Error("Planning-root service manager is disposed.");
10621
- const lease = this.createOperationLease(record);
10622
- try {
10623
- const stream = operation(lease.services);
10624
- let managedStream;
10625
- const settled = stream.settled.then(async (settlement) => {
10626
- await lease.release();
10627
- record.activeStreams.delete(managedStream);
10628
- return settlement;
10629
- });
10630
- let cancelRequested = false;
10631
- managedStream = {
10632
- settled,
10633
- cancel: () => {
10634
- if (!cancelRequested) {
10635
- cancelRequested = true;
10636
- stream.cancel().catch(() => {});
10637
- }
10638
- return settled;
10639
- }
10640
- };
10641
- record.activeStreams.add(managedStream);
10642
- settled.catch(() => {});
10643
- return managedStream;
10644
- } catch (error) {
11130
+ const stream = operation(lease.services);
11131
+ let managedStream;
11132
+ const settled = stream.settled.then(async (settlement) => {
10645
11133
  await lease.release();
10646
- throw error;
10647
- }
10648
- });
11134
+ record.activeStreams.delete(managedStream);
11135
+ return settlement;
11136
+ });
11137
+ let cancelRequested = false;
11138
+ managedStream = {
11139
+ settled,
11140
+ cancel: () => {
11141
+ if (!cancelRequested) {
11142
+ cancelRequested = true;
11143
+ stream.cancel().catch(() => {});
11144
+ }
11145
+ return settled;
11146
+ }
11147
+ };
11148
+ record.activeStreams.add(managedStream);
11149
+ settled.catch(() => {});
11150
+ return managedStream;
11151
+ } catch (error) {
11152
+ await lease.release();
11153
+ throw error;
11154
+ }
10649
11155
  }
10650
11156
  /** Run one Schema action inside the same operation-lifetime contract as every Router caller. */
10651
11157
  async mutateSchema(action) {
@@ -10670,8 +11176,9 @@ var PlanningRootServiceManager = class {
10670
11176
  }
10671
11177
  const cancellation = this.cancelRetiringStreams();
10672
11178
  cancellation.catch(() => {});
10673
- const teardown = this.runTransition(async () => {
10674
- await cancellation;
11179
+ const teardown = cancellation.then(async () => {
11180
+ await Promise.allSettled(this.rootTransitionFlights.values());
11181
+ await this.rootTransitionCommitTail;
10675
11182
  for (const record of this.retiringRecords) await this.retireRecord(record);
10676
11183
  this.ownedProjectionWorkRuntime?.clear();
10677
11184
  });
@@ -11883,7 +12390,7 @@ function createRootContextNotificationBridge(input) {
11883
12390
  function createRootContextProjectionWorkOwner(runtime) {
11884
12391
  return { rootContext: runtime.createRegistry() };
11885
12392
  }
11886
- function estimateSnapshotBytes$1(data) {
12393
+ function estimateSnapshotBytes$2(data) {
11887
12394
  return Buffer$1.byteLength(JSON.stringify(data) ?? "", "utf8");
11888
12395
  }
11889
12396
  /** CLI-backed Root Context Pull/Push owner for one launch project. */
@@ -11955,7 +12462,7 @@ var RootContextProjectionService = class {
11955
12462
  identity: this.identity(),
11956
12463
  resourceClass: "cli",
11957
12464
  priority: "foreground",
11958
- estimateSnapshotBytes: estimateSnapshotBytes$1,
12465
+ estimateSnapshotBytes: estimateSnapshotBytes$2,
11959
12466
  load: async ({ reportStage }) => {
11960
12467
  await trackRootContextStaticDependencies({
11961
12468
  launchProjectDir: this.options.launchProjectDir,
@@ -13250,12 +13757,58 @@ async function setTrackedTaskCompletion(options) {
13250
13757
  //#endregion
13251
13758
  //#region src/router.ts
13252
13759
  const t = initTRPC.context().create();
13760
+ /**
13761
+ * Diagnostic-only tRPC procedure span. Covers every query/mutation/subscription so per-procedure
13762
+ * transport latency is visible. When `--otel` is off the Context carries a no-op tracer, so this
13763
+ * middleware is effectively free. Subscription establishment (not the data fetch) is measured here;
13764
+ * the actual planning-root data resolution carries its own nested `planningRoot.runOperation` span.
13765
+ */
13766
+ const tracingMiddleware = t.middleware(async ({ ctx, path, type, next }) => {
13767
+ const tracer = ctx.tracer;
13768
+ if (!tracer) return next();
13769
+ return tracer.startActiveSpan(`trpc.${path}`, async (span) => {
13770
+ span.setAttribute("trpc.path", path);
13771
+ span.setAttribute("trpc.type", type);
13772
+ try {
13773
+ const result = await next();
13774
+ span.setAttribute("trpc.ok", !("ok" in result) || result.ok);
13775
+ return result;
13776
+ } catch (err) {
13777
+ span.recordException(err);
13778
+ span.setStatus({
13779
+ code: 2,
13780
+ message: err instanceof Error ? err.message : String(err)
13781
+ });
13782
+ throw err;
13783
+ } finally {
13784
+ span.end();
13785
+ }
13786
+ });
13787
+ });
13253
13788
  /** Typed tRPC router factory bound to the OpenSpecUI Server context. */
13254
13789
  const router = t.router;
13255
13790
  /** Typed public tRPC procedure factory bound to the OpenSpecUI Server context. */
13256
- const publicProcedure = t.procedure;
13791
+ const publicProcedure = t.procedure.use(tracingMiddleware);
13257
13792
  function runPlanningRoot(ctx, task, options = {}) {
13258
- return options.reactive ? ctx.planningRootServices.runReactiveOperation(task) : ctx.planningRootServices.runOperation(task);
13793
+ const tracer = ctx.tracer;
13794
+ const invoke = () => options.reactive ? ctx.planningRootServices.runReactiveOperation(task) : ctx.planningRootServices.runOperation(task, options.allowStaleCache ?? false);
13795
+ if (!tracer) return invoke();
13796
+ return tracer.startActiveSpan("planningRoot.runOperation", async (span) => {
13797
+ span.setAttribute("planningRoot.reactive", !!options.reactive);
13798
+ try {
13799
+ return await invoke();
13800
+ } finally {
13801
+ span.end();
13802
+ }
13803
+ });
13804
+ }
13805
+ /**
13806
+ * Read-only Planning-root query helper. Equivalent to `runPlanningRoot` with `allowStaleCache: true`,
13807
+ * so read-only HTTP queries can reuse the cached Root Context snapshot instead of re-resolving on every
13808
+ * call. Write operations must use `runPlanningRoot` directly (default `allowStaleCache: false`).
13809
+ */
13810
+ function runPlanningRootRead(ctx, task) {
13811
+ return runPlanningRoot(ctx, task, { allowStaleCache: true });
13259
13812
  }
13260
13813
  function createPlanningRootSubscription(ctx, task) {
13261
13814
  return createReactiveSubscription(() => runPlanningRoot(ctx, task, { reactive: true }));
@@ -13284,7 +13837,7 @@ function createPlanningRootProjectionWorkSubscription(ctx, subscribe, parseEvent
13284
13837
  for await (const _ of reactiveContext.stream(() => {
13285
13838
  retireRegionalSubscription();
13286
13839
  const epoch = regionalEpoch;
13287
- return ctx.planningRootServices.runReactiveOperation((services) => {
13840
+ return runPlanningRoot(ctx, (services) => {
13288
13841
  regionalSubscription = subscribe(services, (event) => {
13289
13842
  if (!active || epoch !== regionalEpoch) return;
13290
13843
  try {
@@ -13293,7 +13846,7 @@ function createPlanningRootProjectionWorkSubscription(ctx, subscribe, parseEvent
13293
13846
  emit.error(error instanceof Error ? error : new Error(String(error)));
13294
13847
  }
13295
13848
  });
13296
- });
13849
+ }, { reactive: true });
13297
13850
  }, controller.signal, { onRecomputeStarted: retireRegionalSubscription }));
13298
13851
  } catch (error) {
13299
13852
  if (active && !controller.signal.aborted) emit.error(error instanceof Error ? error : new Error(String(error)));
@@ -13412,6 +13965,7 @@ const projectionWorkIdentitySchema = z.object({
13412
13965
  inputFingerprint: z.string(),
13413
13966
  protocolVersion: z.number().int()
13414
13967
  });
13968
+ const projectionWorkPhaseSchema = z.enum(projectionWorkPhases);
13415
13969
  function dashboardProjectionEventSchema(dataSchema) {
13416
13970
  const snapshotSchema = z.object({
13417
13971
  data: dataSchema,
@@ -13426,19 +13980,7 @@ function dashboardProjectionEventSchema(dataSchema) {
13426
13980
  }),
13427
13981
  z.object({
13428
13982
  type: z.literal("stage"),
13429
- phase: z.enum([
13430
- "request",
13431
- "transport-start",
13432
- "root-ready",
13433
- "cache-hit",
13434
- "join",
13435
- "start",
13436
- "leaf-settled",
13437
- "first-stable-payload",
13438
- "complete",
13439
- "error",
13440
- "cancel"
13441
- ]),
13983
+ phase: projectionWorkPhaseSchema,
13442
13984
  workGeneration: z.number().int()
13443
13985
  }),
13444
13986
  z.object({
@@ -13551,19 +14093,7 @@ function changeProjectionEventSchema() {
13551
14093
  }),
13552
14094
  z.object({
13553
14095
  type: z.literal("stage"),
13554
- phase: z.enum([
13555
- "request",
13556
- "transport-start",
13557
- "root-ready",
13558
- "cache-hit",
13559
- "join",
13560
- "start",
13561
- "leaf-settled",
13562
- "first-stable-payload",
13563
- "complete",
13564
- "error",
13565
- "cancel"
13566
- ]),
14096
+ phase: projectionWorkPhaseSchema,
13567
14097
  workGeneration: z.number().int()
13568
14098
  }),
13569
14099
  z.object({
@@ -13830,7 +14360,7 @@ const localModelsRouter = router({
13830
14360
  })).mutation(({ ctx, input }) => {
13831
14361
  return ctx.localModelAssetService.deleteModel(input.modelId, input.groupId ?? input.selectedGroupId);
13832
14362
  }),
13833
- refreshProfiles: publicProcedure.input(z.object({ modelId: z.string().min(1).optional() })).mutation(async ({ ctx, input }) => {
14363
+ refreshProfiles: publicProcedure.input(z.object({ modelId: z.string().min(1).optional() })).query(async ({ ctx, input }) => {
13834
14364
  const asset = await ctx.localModelAssetService.refreshProfiles(input.modelId);
13835
14365
  return {
13836
14366
  modelId: asset.modelId,
@@ -13839,7 +14369,7 @@ const localModelsRouter = router({
13839
14369
  downloadPlan: asset.plan ?? null
13840
14370
  };
13841
14371
  }),
13842
- refreshArtifacts: publicProcedure.input(z.object({ modelId: z.string().min(1).optional() })).mutation(async ({ ctx, input }) => {
14372
+ refreshArtifacts: publicProcedure.input(z.object({ modelId: z.string().min(1).optional() })).query(async ({ ctx, input }) => {
13843
14373
  const asset = await ctx.localModelAssetService.refreshProfiles(input.modelId);
13844
14374
  return {
13845
14375
  modelId: asset.modelId,
@@ -13938,7 +14468,7 @@ const localCt2ModelsRouter = router({
13938
14468
  })).mutation(({ ctx, input }) => {
13939
14469
  return ctx.localCt2ModelAssetService.deleteModel(input.modelId, input.groupId ?? input.selectedGroupId);
13940
14470
  }),
13941
- refreshArtifacts: publicProcedure.input(z.object({ modelId: z.string().min(1).optional() })).mutation(async ({ ctx, input }) => {
14471
+ refreshArtifacts: publicProcedure.input(z.object({ modelId: z.string().min(1).optional() })).query(async ({ ctx, input }) => {
13942
14472
  const asset = await ctx.localCt2ModelAssetService.refreshArtifacts(input.modelId);
13943
14473
  return {
13944
14474
  modelId: asset.modelId,
@@ -14037,7 +14567,7 @@ const localLlamaModelsRouter = router({
14037
14567
  })).mutation(({ ctx, input }) => {
14038
14568
  return ctx.localLlamaModelAssetService.deleteModel(input.modelId, input.groupId ?? input.selectedGroupId);
14039
14569
  }),
14040
- refreshArtifacts: publicProcedure.input(z.object({ modelId: z.string().min(1).optional() })).mutation(async ({ ctx, input }) => {
14570
+ refreshArtifacts: publicProcedure.input(z.object({ modelId: z.string().min(1).optional() })).query(async ({ ctx, input }) => {
14041
14571
  const asset = await ctx.localLlamaModelAssetService.refreshArtifacts(input.modelId);
14042
14572
  return {
14043
14573
  modelId: asset.modelId,
@@ -14113,13 +14643,13 @@ function buildSystemStatus(ctx) {
14113
14643
  /** Source-aware Spec Catalog and document operations. */
14114
14644
  const specRouter = router({
14115
14645
  catalog: publicProcedure.query(async ({ ctx }) => {
14116
- const data = await runPlanningRoot(ctx, ({ planningCliProjectionService }) => planningCliProjectionService.getCurrent({ kind: "spec-catalog" }));
14646
+ const data = await runPlanningRootRead(ctx, ({ planningCliProjectionService }) => planningCliProjectionService.getCurrent({ kind: "spec-catalog" }));
14117
14647
  if (data.kind !== "spec-catalog") throw new Error("Unexpected Spec Catalog projection.");
14118
14648
  return data.value;
14119
14649
  }),
14120
14650
  document: publicProcedure.input(SpecIdentitySchema).query(async ({ ctx, input }) => {
14121
14651
  try {
14122
- const data = await runPlanningRoot(ctx, ({ planningCliProjectionService }) => planningCliProjectionService.getCurrent({
14652
+ const data = await runPlanningRootRead(ctx, ({ planningCliProjectionService }) => planningCliProjectionService.getCurrent({
14123
14653
  kind: "spec-document",
14124
14654
  identity: input
14125
14655
  }));
@@ -14150,16 +14680,16 @@ const specRouter = router({
14150
14680
  */
14151
14681
  const changeRouter = router({
14152
14682
  list: publicProcedure.query(async ({ ctx }) => {
14153
- return runPlanningRoot(ctx, ({ adapter }) => adapter.listChanges());
14683
+ return runPlanningRootRead(ctx, ({ adapter }) => adapter.listChanges());
14154
14684
  }),
14155
14685
  listWithMeta: publicProcedure.query(async ({ ctx }) => {
14156
- return runPlanningRoot(ctx, ({ adapter }) => adapter.listChangesWithMeta());
14686
+ return runPlanningRootRead(ctx, ({ adapter }) => adapter.listChangesWithMeta());
14157
14687
  }),
14158
14688
  listArchived: publicProcedure.query(async ({ ctx }) => {
14159
- return runPlanningRoot(ctx, ({ adapter }) => adapter.listArchivedChanges());
14689
+ return runPlanningRootRead(ctx, ({ adapter }) => adapter.listArchivedChanges());
14160
14690
  }),
14161
14691
  get: publicProcedure.input(z.object({ id: z.string() })).query(async ({ ctx, input }) => {
14162
- return runPlanningRoot(ctx, ({ documentService }) => documentService.readChange(input.id));
14692
+ return runPlanningRootRead(ctx, ({ documentService }) => documentService.readChange(input.id));
14163
14693
  }),
14164
14694
  getRaw: publicProcedure.input(z.object({ id: z.string() })).query(async ({ ctx, input }) => {
14165
14695
  return runPlanningRoot(ctx, ({ adapter }) => adapter.readChangeRaw(input.id));
@@ -14216,7 +14746,7 @@ const changeRouter = router({
14216
14746
  id: z.string(),
14217
14747
  path: z.string()
14218
14748
  })).query(async ({ ctx, input }) => {
14219
- return runPlanningRoot(ctx, ({ filePreviewService }) => filePreviewService.prepareEntityFilePreview({
14749
+ return runPlanningRootRead(ctx, ({ filePreviewService }) => filePreviewService.prepareEntityFilePreview({
14220
14750
  stage: "change",
14221
14751
  changeId: input.id,
14222
14752
  path: input.path
@@ -14235,16 +14765,16 @@ const initRouter = router({ init: publicProcedure.mutation(async ({ ctx }) => {
14235
14765
  */
14236
14766
  const archiveRouter = router({
14237
14767
  list: publicProcedure.query(async ({ ctx }) => {
14238
- return runPlanningRoot(ctx, ({ adapter }) => adapter.listArchivedChanges());
14768
+ return runPlanningRootRead(ctx, ({ adapter }) => adapter.listArchivedChanges());
14239
14769
  }),
14240
14770
  listWithMeta: publicProcedure.query(async ({ ctx }) => {
14241
- return runPlanningRoot(ctx, ({ adapter }) => adapter.listArchivedChangesWithMeta());
14771
+ return runPlanningRootRead(ctx, ({ adapter }) => adapter.listArchivedChangesWithMeta());
14242
14772
  }),
14243
14773
  get: publicProcedure.input(z.object({ id: z.string() })).query(async ({ ctx, input }) => {
14244
- return runPlanningRoot(ctx, async (services) => services.documentService.readEntityDetail("archive", input.id, "view", "processed", await buildEntityReadOptions(services, "archive", input.id)));
14774
+ return runPlanningRootRead(ctx, async (services) => services.documentService.readEntityDetail("archive", input.id, "view", "processed", await buildEntityReadOptions(services, "archive", input.id)));
14245
14775
  }),
14246
14776
  getRaw: publicProcedure.input(z.object({ id: z.string() })).query(async ({ ctx, input }) => {
14247
- return runPlanningRoot(ctx, async (services) => services.documentService.readEntityDetail("archive", input.id, "view", "source", await buildEntityReadOptions(services, "archive", input.id)));
14777
+ return runPlanningRootRead(ctx, async (services) => services.documentService.readEntityDetail("archive", input.id, "view", "source", await buildEntityReadOptions(services, "archive", input.id)));
14248
14778
  }),
14249
14779
  subscribe: publicProcedure.subscription(({ ctx }) => {
14250
14780
  return createPlanningRootProjectionSubscription(ctx, ({ adapter }) => adapter.listArchivedChangesWithMeta());
@@ -14267,7 +14797,7 @@ const archiveRouter = router({
14267
14797
  id: z.string(),
14268
14798
  path: z.string()
14269
14799
  })).query(async ({ ctx, input }) => {
14270
- return runPlanningRoot(ctx, ({ filePreviewService }) => filePreviewService.prepareEntityFilePreview({
14800
+ return runPlanningRootRead(ctx, ({ filePreviewService }) => filePreviewService.prepareEntityFilePreview({
14271
14801
  stage: "archive",
14272
14802
  changeId: input.id,
14273
14803
  path: input.path
@@ -14371,7 +14901,6 @@ const configRouter = router({
14371
14901
  "system"
14372
14902
  ]).optional(),
14373
14903
  codeEditor: z.object({ theme: CodeEditorThemeSchema.optional() }).optional(),
14374
- appBaseUrl: z.string().optional(),
14375
14904
  opsx: OpsxConfigSchema.partial().optional(),
14376
14905
  terminal: TerminalConfigSchema.omit({ rendererEngine: true }).partial().extend({ rendererEngine: TerminalRendererEngineSchema.optional() }).optional(),
14377
14906
  dashboard: DashboardConfigSchema.partial().optional(),
@@ -14383,10 +14912,9 @@ const configRouter = router({
14383
14912
  const hasCliArgs = input.cli !== void 0 && Object.prototype.hasOwnProperty.call(input.cli, "args");
14384
14913
  if (hasCliCommand && !hasCliArgs) {
14385
14914
  await ctx.configManager.setCliCommand(input.cli?.command ?? "");
14386
- if (input.theme !== void 0 || input.codeEditor !== void 0 || input.appBaseUrl !== void 0 || input.opsx !== void 0 || input.terminal !== void 0 || input.dashboard !== void 0 || input.git !== void 0 || input.notifications !== void 0 || input.translation !== void 0) await ctx.configManager.writeConfig({
14915
+ if (input.theme !== void 0 || input.codeEditor !== void 0 || input.opsx !== void 0 || input.terminal !== void 0 || input.dashboard !== void 0 || input.git !== void 0 || input.notifications !== void 0 || input.translation !== void 0) await ctx.configManager.writeConfig({
14387
14916
  theme: input.theme,
14388
14917
  codeEditor: input.codeEditor,
14389
- appBaseUrl: input.appBaseUrl,
14390
14918
  opsx: input.opsx,
14391
14919
  terminal: input.terminal,
14392
14920
  dashboard: input.dashboard,
@@ -14606,7 +15134,7 @@ const cliRouter = router({
14606
15134
  */
14607
15135
  const planningCliProjectionRouter = router({
14608
15136
  read: publicProcedure.input(PlanningCliProjectionSelectorSchema).query(({ ctx, input }) => runPlanningRoot(ctx, ({ planningCliProjectionService }) => PlanningCliProjectionStateSchema.parse(planningCliProjectionService.read(input)), { reactive: true })),
14609
- refresh: publicProcedure.input(PlanningCliProjectionSelectorSchema).mutation(({ ctx, input }) => runPlanningRoot(ctx, ({ planningCliProjectionService }) => PlanningCliProjectionStateSchema.parse(planningCliProjectionService.refresh(input)), { reactive: true })),
15137
+ refresh: publicProcedure.input(PlanningCliProjectionSelectorSchema).query(({ ctx, input }) => runPlanningRoot(ctx, ({ planningCliProjectionService }) => PlanningCliProjectionStateSchema.parse(planningCliProjectionService.refresh(input)), { reactive: true })),
14610
15138
  subscribe: publicProcedure.input(PlanningCliProjectionSelectorSchema).subscription(({ ctx, input }) => createPlanningRootCliProjectionSubscription(ctx, input))
14611
15139
  });
14612
15140
  const opsxRouter = router({
@@ -14620,7 +15148,7 @@ const opsxRouter = router({
14620
15148
  change: z.string().optional(),
14621
15149
  schema: z.string().optional()
14622
15150
  })).query(async ({ ctx, input }) => {
14623
- const data = await runPlanningRoot(ctx, ({ planningCliProjectionService }) => planningCliProjectionService.getCurrent({
15151
+ const data = await runPlanningRootRead(ctx, ({ planningCliProjectionService }) => planningCliProjectionService.getCurrent({
14624
15152
  kind: "opsx-status",
14625
15153
  change: requireChangeId(input.change),
14626
15154
  schema: input.schema
@@ -14629,7 +15157,7 @@ const opsxRouter = router({
14629
15157
  return data.value;
14630
15158
  }),
14631
15159
  statusList: publicProcedure.query(async ({ ctx }) => {
14632
- const data = await runPlanningRoot(ctx, ({ planningCliProjectionService }) => planningCliProjectionService.getCurrent({ kind: "opsx-status-list" }));
15160
+ const data = await runPlanningRootRead(ctx, ({ planningCliProjectionService }) => planningCliProjectionService.getCurrent({ kind: "opsx-status-list" }));
14633
15161
  if (data.kind !== "opsx-status-list") throw new Error("Unexpected OPSX Status-list projection.");
14634
15162
  return data.value;
14635
15163
  }),
@@ -14638,7 +15166,7 @@ const opsxRouter = router({
14638
15166
  artifact: z.string(),
14639
15167
  schema: z.string().optional()
14640
15168
  })).query(async ({ ctx, input }) => {
14641
- const data = await runPlanningRoot(ctx, ({ planningCliProjectionService }) => planningCliProjectionService.getCurrent({
15169
+ const data = await runPlanningRootRead(ctx, ({ planningCliProjectionService }) => planningCliProjectionService.getCurrent({
14642
15170
  kind: "opsx-instructions",
14643
15171
  change: requireChangeId(input.change),
14644
15172
  artifact: input.artifact,
@@ -14651,7 +15179,7 @@ const opsxRouter = router({
14651
15179
  change: z.string().optional(),
14652
15180
  schema: z.string().optional()
14653
15181
  })).query(async ({ ctx, input }) => {
14654
- const data = await runPlanningRoot(ctx, ({ planningCliProjectionService }) => planningCliProjectionService.getCurrent({
15182
+ const data = await runPlanningRootRead(ctx, ({ planningCliProjectionService }) => planningCliProjectionService.getCurrent({
14655
15183
  kind: "opsx-apply-instructions",
14656
15184
  change: requireChangeId(input.change),
14657
15185
  schema: input.schema
@@ -14660,12 +15188,12 @@ const opsxRouter = router({
14660
15188
  return data.value;
14661
15189
  }),
14662
15190
  configBundle: publicProcedure.query(async ({ ctx }) => {
14663
- const data = await runPlanningRoot(ctx, ({ planningCliProjectionService }) => planningCliProjectionService.getCurrent({ kind: "opsx-config-bundle" }));
15191
+ const data = await runPlanningRootRead(ctx, ({ planningCliProjectionService }) => planningCliProjectionService.getCurrent({ kind: "opsx-config-bundle" }));
14664
15192
  if (data.kind !== "opsx-config-bundle") throw new Error("Unexpected OPSX Config-bundle projection.");
14665
15193
  return data.value;
14666
15194
  }),
14667
15195
  templates: publicProcedure.input(z.object({ schema: z.string().optional() }).optional()).query(async ({ ctx, input }) => {
14668
- const data = await runPlanningRoot(ctx, ({ planningCliProjectionService }) => planningCliProjectionService.getCurrent({
15196
+ const data = await runPlanningRootRead(ctx, ({ planningCliProjectionService }) => planningCliProjectionService.getCurrent({
14669
15197
  kind: "opsx-templates",
14670
15198
  schema: input?.schema
14671
15199
  }));
@@ -14673,7 +15201,7 @@ const opsxRouter = router({
14673
15201
  return data.value;
14674
15202
  }),
14675
15203
  templateContents: publicProcedure.input(z.object({ schema: z.string().optional() }).optional()).query(async ({ ctx, input }) => {
14676
- const data = await runPlanningRoot(ctx, ({ planningCliProjectionService }) => planningCliProjectionService.getCurrent({
15204
+ const data = await runPlanningRootRead(ctx, ({ planningCliProjectionService }) => planningCliProjectionService.getCurrent({
14677
15205
  kind: "opsx-template-contents",
14678
15206
  schema: input?.schema
14679
15207
  }));
@@ -14804,7 +15332,7 @@ const opsxRouter = router({
14804
15332
  return result;
14805
15333
  }),
14806
15334
  listChanges: publicProcedure.query(async ({ ctx }) => {
14807
- const data = await runPlanningRoot(ctx, ({ planningCliProjectionService }) => planningCliProjectionService.getCurrent({ kind: "opsx-change-list" }));
15335
+ const data = await runPlanningRootRead(ctx, ({ planningCliProjectionService }) => planningCliProjectionService.getCurrent({ kind: "opsx-change-list" }));
14808
15336
  if (data.kind !== "opsx-change-list") throw new Error("Unexpected OPSX Change-list projection.");
14809
15337
  return data.value;
14810
15338
  }),
@@ -14896,7 +15424,7 @@ const kvRouter = router({
14896
15424
  */
14897
15425
  const searchRouter = router({
14898
15426
  query: publicProcedure.input(ProjectSearchQuerySchema).output(ProjectSearchHitSchema.array()).query(async ({ ctx, input }) => {
14899
- return runPlanningRoot(ctx, async ({ searchService }) => parseProjectSearchHits(await searchService.query(input), input.scope));
15427
+ return runPlanningRootRead(ctx, async ({ searchService }) => parseProjectSearchHits(await searchService.query(input), input.scope));
14900
15428
  }),
14901
15429
  subscribe: publicProcedure.input(ProjectSearchQuerySchema).subscription(({ ctx, input }) => {
14902
15430
  return createPlanningRootSubscription(ctx, async ({ searchService }) => parseProjectSearchHits(await searchService.queryReactive(input), input.scope));
@@ -14938,17 +15466,17 @@ const systemRouter = router({
14938
15466
  */
14939
15467
  const dashboardRouter = router({
14940
15468
  get: publicProcedure.query(async ({ ctx }) => {
14941
- return runPlanningRoot(ctx, ({ dashboardOverviewService }) => dashboardOverviewService.getCurrent());
15469
+ return runPlanningRootRead(ctx, ({ dashboardOverviewService }) => dashboardOverviewService.getCurrent());
14942
15470
  }),
14943
15471
  subscribe: publicProcedure.subscription(({ ctx }) => {
14944
15472
  return createPlanningRootSubscription(ctx, ({ dashboardOverviewService }) => dashboardOverviewService.getCurrent());
14945
15473
  }),
14946
15474
  getSummary: publicProcedure.query(async ({ ctx }) => DashboardSummaryProjectionStateSchema.parse(await runPlanningRoot(ctx, ({ dashboardProjectionService }) => dashboardProjectionService.getSummary(), { reactive: true }))),
14947
15475
  getTrends: publicProcedure.query(({ ctx }) => {
14948
- return runPlanningRoot(ctx, ({ dashboardProjectionService }) => dashboardProjectionService.getTrends());
15476
+ return runPlanningRootRead(ctx, ({ dashboardProjectionService }) => dashboardProjectionService.getTrends());
14949
15477
  }),
14950
15478
  getGit: publicProcedure.query(({ ctx }) => {
14951
- return runPlanningRoot(ctx, ({ dashboardProjectionService }) => dashboardProjectionService.getGit());
15479
+ return runPlanningRootRead(ctx, ({ dashboardProjectionService }) => dashboardProjectionService.getGit());
14952
15480
  }),
14953
15481
  subscribeSummary: publicProcedure.subscription(({ ctx }) => createPlanningRootSummaryInvalidationSubscription(ctx)),
14954
15482
  subscribeTrends: publicProcedure.subscription(({ ctx }) => createPlanningRootProjectionWorkSubscription(ctx, (services, listener) => services.dashboardProjectionService.subscribeTrends(listener), (event) => dashboardProjectionEventSchema(DashboardTrendsProjectionSchema).parse(event))),
@@ -14957,7 +15485,7 @@ const dashboardRouter = router({
14957
15485
  scope: z.literal("code"),
14958
15486
  expectedBindingToken: gitBindingTokenSchema,
14959
15487
  reason: z.string().optional()
14960
- })).mutation(async ({ ctx, input }) => {
15488
+ })).query(async ({ ctx, input }) => {
14961
15489
  const reason = input.reason?.trim() || "manual-refresh";
14962
15490
  return runGitScope(ctx, input, async (codeRepository) => {
14963
15491
  await touchDashboardGitRefreshStamp(gitRepositoryCwd(codeRepository), reason);
@@ -15108,7 +15636,7 @@ const gitRouter = router({
15108
15636
  scope: gitRepositoryScopeSchema,
15109
15637
  expectedBindingToken: gitBindingTokenSchema,
15110
15638
  reason: z.string().optional()
15111
- })).mutation(async ({ ctx, input }) => {
15639
+ })).query(async ({ ctx, input }) => {
15112
15640
  return runGitScope(ctx, input, async (repository) => {
15113
15641
  const projectDir = gitRepositoryCwd(repository);
15114
15642
  const reason = input.reason?.trim() || "manual-refresh";
@@ -15171,7 +15699,7 @@ const storesRouter = router({
15171
15699
  refreshProjection: publicProcedure.input(z.discriminatedUnion("kind", [z.object({ kind: z.literal("list") }), z.object({
15172
15700
  kind: z.literal("doctor"),
15173
15701
  id: z.string().optional()
15174
- })])).mutation(({ ctx, input }) => input.kind === "list" ? HostedStoreListProjectionStateSchema.parse(ctx.storeProjectionService.refreshList()) : HostedStoreDoctorProjectionStateSchema.parse(ctx.storeProjectionService.refreshDoctor(input.id))),
15702
+ })])).query(({ ctx, input }) => input.kind === "list" ? HostedStoreListProjectionStateSchema.parse(ctx.storeProjectionService.refreshList()) : HostedStoreDoctorProjectionStateSchema.parse(ctx.storeProjectionService.refreshDoctor(input.id))),
15175
15703
  subscribeProjection: publicProcedure.input(z.discriminatedUnion("kind", [z.object({ kind: z.literal("list") }), z.object({
15176
15704
  kind: z.literal("doctor"),
15177
15705
  id: z.string().optional()
@@ -15255,6 +15783,38 @@ const storesRouter = router({
15255
15783
  });
15256
15784
  })
15257
15785
  });
15786
+ /**
15787
+ * Demand-driven readonly Store-content (Specs/active Changes) Projection Work router (6.10).
15788
+ *
15789
+ * Read procedures expose the hosted typed Pull state for one composite `(envUri, Store id, kind)` identity; Store id
15790
+ * alone is never accepted as sufficient. The composite identity is validated and the envUri treated as opaque.
15791
+ * Procedures run only after normal Access Gate admission and never accept client credentials in projection payloads.
15792
+ */
15793
+ const storeContentIdentityInput = z.object({
15794
+ envUri: z.string().min(1),
15795
+ storeId: z.string().min(1),
15796
+ kind: z.enum(["specs", "changes"])
15797
+ });
15798
+ const storesContentRouter = router({
15799
+ readSpecsProjection: publicProcedure.input(storeContentIdentityInput).query(({ ctx, input }) => HostedStoreContentSpecsProjectionStateSchema.parse(ctx.storeContentProjectionService.readContent({
15800
+ envUri: input.envUri,
15801
+ storeId: input.storeId,
15802
+ kind: "specs"
15803
+ }))),
15804
+ readChangesProjection: publicProcedure.input(storeContentIdentityInput).query(({ ctx, input }) => HostedStoreContentChangesProjectionStateSchema.parse(ctx.storeContentProjectionService.readContent({
15805
+ envUri: input.envUri,
15806
+ storeId: input.storeId,
15807
+ kind: "changes"
15808
+ }))),
15809
+ subscribeProjection: publicProcedure.input(storeContentIdentityInput).subscription(({ ctx, input }) => observable((emit) => {
15810
+ const subscription = ctx.storeContentProjectionService.subscribeContent({
15811
+ envUri: input.envUri,
15812
+ storeId: input.storeId,
15813
+ kind: input.kind
15814
+ }, (notice) => emit.next(notice));
15815
+ return () => subscription.unsubscribe();
15816
+ }))
15817
+ });
15258
15818
  const runtimeInvalidationFacetSchema = z.enum(RUNTIME_INVALIDATION_FACETS);
15259
15819
  /** Push identity-only invalidation tokens; every client pulls its authoritative projection. */
15260
15820
  const runtimeInvalidationRouter = router({ subscribe: publicProcedure.input(z.object({ facets: z.array(runtimeInvalidationFacetSchema).min(1) })).subscription(({ ctx, input }) => {
@@ -15340,7 +15900,7 @@ const planningConfigRouter = router({
15340
15900
  }),
15341
15901
  environmentGlobal: publicProcedure.query(({ ctx }) => ctx.environmentGlobalProjectionService.getCurrentConfig()),
15342
15902
  readEnvironmentGlobalProjection: publicProcedure.query(({ ctx }) => EnvironmentGlobalProjectionStateSchema.parse(ctx.environmentGlobalProjectionService.read())),
15343
- refreshEnvironmentGlobalProjection: publicProcedure.mutation(({ ctx }) => EnvironmentGlobalProjectionStateSchema.parse(ctx.environmentGlobalProjectionService.refresh())),
15903
+ refreshEnvironmentGlobalProjection: publicProcedure.query(({ ctx }) => EnvironmentGlobalProjectionStateSchema.parse(ctx.environmentGlobalProjectionService.refresh())),
15344
15904
  subscribeEnvironmentGlobalProjection: publicProcedure.subscription(({ ctx }) => observable((emit) => {
15345
15905
  const subscription = ctx.environmentGlobalProjectionService.subscribe((notice) => emit.next(CliProjectionNoticeSchema.parse(notice)));
15346
15906
  return () => subscription.unsubscribe();
@@ -15354,7 +15914,7 @@ const planningConfigRouter = router({
15354
15914
  return runOpenSpecCliMutation(ctx, args, () => ctx.cliExecutor.execute(args));
15355
15915
  }),
15356
15916
  readEnvironmentGlobalFileProjection: publicProcedure.query(({ ctx }) => EnvironmentGlobalFileProjectionStateSchema.parse(ctx.environmentGlobalProjectionService.readFile())),
15357
- refreshEnvironmentGlobalFileProjection: publicProcedure.mutation(({ ctx }) => EnvironmentGlobalFileProjectionStateSchema.parse(ctx.environmentGlobalProjectionService.refreshFile())),
15917
+ refreshEnvironmentGlobalFileProjection: publicProcedure.query(({ ctx }) => EnvironmentGlobalFileProjectionStateSchema.parse(ctx.environmentGlobalProjectionService.refreshFile())),
15358
15918
  subscribeEnvironmentGlobalFileProjection: publicProcedure.subscription(({ ctx }) => observable((emit) => {
15359
15919
  const subscription = ctx.environmentGlobalProjectionService.subscribeFile((notice) => emit.next(CliProjectionNoticeSchema.parse(notice)));
15360
15920
  return () => subscription.unsubscribe();
@@ -15371,7 +15931,7 @@ const planningConfigRouter = router({
15371
15931
  const rootContextRouter = router({
15372
15932
  get: publicProcedure.query(async ({ ctx }) => HostedRootContextResolvedStateSchema.parse(await ctx.rootContextProjectionService.getCurrent())),
15373
15933
  readProjection: publicProcedure.query(({ ctx }) => HostedRootContextProjectionStateSchema.parse(ctx.rootContextProjectionService.read())),
15374
- refreshProjection: publicProcedure.mutation(({ ctx }) => HostedRootContextProjectionStateSchema.parse(ctx.rootContextProjectionService.refresh())),
15934
+ refreshProjection: publicProcedure.query(({ ctx }) => HostedRootContextProjectionStateSchema.parse(ctx.rootContextProjectionService.refresh())),
15375
15935
  subscribeProjection: publicProcedure.subscription(({ ctx }) => observable((emit) => {
15376
15936
  const subscription = ctx.rootContextProjectionService.subscribe((notice) => emit.next(HostedCliProjectionNoticeSchema.parse(notice)));
15377
15937
  return () => subscription.unsubscribe();
@@ -15404,11 +15964,205 @@ const appRouter = router({
15404
15964
  planningCliProjection: planningCliProjectionRouter,
15405
15965
  opsx: opsxRouter,
15406
15966
  stores: storesRouter,
15967
+ storesContent: storesContentRouter,
15407
15968
  kv: kvRouter,
15408
15969
  search: searchRouter,
15409
15970
  system: systemRouter
15410
15971
  });
15411
15972
 
15973
+ //#endregion
15974
+ //#region src/store-content-projection-service.ts
15975
+ /**
15976
+ * Orthogonal intents (created 2026-07-30 Asia/Shanghai):
15977
+ * 1. Own Store Specs and active Changes as demand-driven CLI Projection Work (6.4/6.5).
15978
+ * 2. Call the official CLI contract with an explicit Store selector; never parse files or reparse stdout (6.6).
15979
+ * 3. Keep Specs and active Changes regions independent for load/refresh/error/recovery (6.7).
15980
+ * 4. Key Projection Work by composite (envUri, Store id, kind) identity; reject stale/cross-Store completion (6.8).
15981
+ * 5. Reuse Store-root observation invalidation and data-free Push -> Pull transport (6.9).
15982
+ *
15983
+ * Original request (2026-07-30): "Stores 完全可以融入 `Environment Center` 这个东西。"
15984
+ * Spec: hosted-environment-delivery › "Environment-Scoped Store Content Projection".
15985
+ *
15986
+ * This service mirrors `store-projection-service.ts` but projects typed Spec/active-Change lists for one selected
15987
+ * composite identity. It is demand-driven: only a subscribed Store Detail starts work (6.12).
15988
+ */
15989
+ function createStoreContentProjectionWorkOwner(runtime) {
15990
+ return { registry: runtime.createRegistry() };
15991
+ }
15992
+ function estimateSnapshotBytes$1(data) {
15993
+ return Buffer$1.byteLength(JSON.stringify(data) ?? "", "utf8");
15994
+ }
15995
+ /** Demand-driven, Store-selected Spec/active-Change CLI Projection Work owner. */
15996
+ var StoreContentProjectionService = class {
15997
+ releaseStoreObservation = null;
15998
+ releaseRuntimeInvalidation = null;
15999
+ disposed = false;
16000
+ constructor(options) {
16001
+ this.options = options;
16002
+ this.releaseStoreObservation = options.storeObservation.subscribe((change) => this.invalidateStoreContent(change));
16003
+ this.releaseRuntimeInvalidation = options.invalidation.subscribe(["stores"], () => this.invalidateAllStoreContent());
16004
+ }
16005
+ dispose() {
16006
+ if (this.disposed) return;
16007
+ this.disposed = true;
16008
+ this.releaseStoreObservation?.();
16009
+ this.releaseRuntimeInvalidation?.();
16010
+ }
16011
+ /** Subscribe to one composite Store-content Work item (demand-driven; only a Detail starts it). */
16012
+ subscribeContent(identity, listener) {
16013
+ return this.options.workOwner.registry.subscribeLifecycle(this.contentRequest(identity), listener);
16014
+ }
16015
+ /** Read the current Pull state for one composite Store-content Work item. */
16016
+ readContent(identity) {
16017
+ const request = this.contentRequest(identity);
16018
+ const state = this.options.workOwner.registry.read(request.identity) ?? this.loadingState(request.identity);
16019
+ return this.selectState(state, identity.kind);
16020
+ }
16021
+ /** Explicitly invalidate one composite Store-content Work item (e.g. after a Store mutation). */
16022
+ refreshContent(identity) {
16023
+ this.options.workOwner.registry.invalidate(this.contentIdentity(identity), "explicit-refresh");
16024
+ return this.readContent(identity);
16025
+ }
16026
+ contentIdentity(identity) {
16027
+ return {
16028
+ projectionKind: `store-content-${identity.kind}`,
16029
+ planningRoot: {
16030
+ identity: this.options.dataScopePath,
16031
+ source: "environment-data-home",
16032
+ storeSelector: identity.storeId
16033
+ },
16034
+ owner: {
16035
+ generation: null,
16036
+ gitBindingToken: null
16037
+ },
16038
+ selector: JSON.stringify({
16039
+ envUri: identity.envUri,
16040
+ storeId: identity.storeId,
16041
+ kind: identity.kind
16042
+ }),
16043
+ inputFingerprint: `openspec-cli-1.6:store-content:${identity.kind}`,
16044
+ protocolVersion: 1
16045
+ };
16046
+ }
16047
+ contentRequest(identity) {
16048
+ return {
16049
+ identity: this.contentIdentity(identity),
16050
+ resourceClass: "cli",
16051
+ priority: "foreground",
16052
+ estimateSnapshotBytes: estimateSnapshotBytes$1,
16053
+ load: async ({ reportStage }) => {
16054
+ reportStage("root-ready");
16055
+ const cliVersion = await this.resolveCliVersion();
16056
+ const storeId = identity.storeId;
16057
+ if (identity.kind === "specs") {
16058
+ const classification$1 = classifyStoreCliResult({
16059
+ result: await this.options.cliExecutor.contracts.listSpecs({ store: storeId }),
16060
+ schema: StoreContentSpecListSchema,
16061
+ cliVersion
16062
+ });
16063
+ const projection$1 = this.toSpecsEnvelope(classification$1, storeId, cliVersion);
16064
+ reportStage("leaf-settled");
16065
+ return {
16066
+ kind: "specs",
16067
+ value: projection$1
16068
+ };
16069
+ }
16070
+ const classification = classifyStoreCliResult({
16071
+ result: await this.options.cliExecutor.contracts.listChanges({ store: storeId }),
16072
+ schema: StoreContentChangeListSchema,
16073
+ cliVersion
16074
+ });
16075
+ const projection = this.toChangesEnvelope(classification, storeId, cliVersion);
16076
+ reportStage("leaf-settled");
16077
+ return {
16078
+ kind: "changes",
16079
+ value: projection
16080
+ };
16081
+ }
16082
+ };
16083
+ }
16084
+ async resolveCliVersion() {
16085
+ try {
16086
+ const availability = await this.options.cliExecutor.checkAvailability();
16087
+ return availability.available ? availability.version : void 0;
16088
+ } catch {
16089
+ return;
16090
+ }
16091
+ }
16092
+ /** Build the Specs-content envelope from a CLI classification, matching the hosted schema. */
16093
+ toSpecsEnvelope(classification, storeId, cliVersion) {
16094
+ if (classification.kind === "ok") return {
16095
+ available: true,
16096
+ specs: classification.data.specs,
16097
+ storeId,
16098
+ ...cliVersion ? { cliVersion } : {}
16099
+ };
16100
+ return {
16101
+ available: false,
16102
+ specs: [],
16103
+ storeId,
16104
+ error: {
16105
+ kind: classification.kind,
16106
+ message: classification.message
16107
+ },
16108
+ ...classification.cliVersion ? { cliVersion: classification.cliVersion } : {}
16109
+ };
16110
+ }
16111
+ /** Build the active-Changes-content envelope from a CLI classification, matching the hosted schema. */
16112
+ toChangesEnvelope(classification, storeId, cliVersion) {
16113
+ if (classification.kind === "ok") return {
16114
+ available: true,
16115
+ changes: classification.data.changes,
16116
+ storeId,
16117
+ ...cliVersion ? { cliVersion } : {}
16118
+ };
16119
+ return {
16120
+ available: false,
16121
+ changes: [],
16122
+ storeId,
16123
+ error: {
16124
+ kind: classification.kind,
16125
+ message: classification.message
16126
+ },
16127
+ ...classification.cliVersion ? { cliVersion: classification.cliVersion } : {}
16128
+ };
16129
+ }
16130
+ invalidateStoreContent(change) {
16131
+ const selectedStoreIds = new Set(change.kind === "inventory" ? change.storeIds : [change.storeId]);
16132
+ this.options.workOwner.registry.invalidateMatching((identity) => {
16133
+ if (!selectedStoreIds.has(identity.planningRoot.storeSelector ?? "")) return false;
16134
+ if (change.kind === "spec-root") return identity.projectionKind === "store-content-specs";
16135
+ return identity.projectionKind === "store-content-specs" || identity.projectionKind === "store-content-changes";
16136
+ }, "dependency");
16137
+ }
16138
+ invalidateAllStoreContent() {
16139
+ this.options.workOwner.registry.invalidateMatching((identity) => identity.projectionKind === "store-content-specs" || identity.projectionKind === "store-content-changes", "dependency");
16140
+ }
16141
+ loadingState(identity) {
16142
+ return {
16143
+ state: "loading",
16144
+ identity: projectionWorkIdentityKey(identity),
16145
+ workGeneration: 0,
16146
+ invalidationCause: "initial",
16147
+ data: null,
16148
+ freshness: null,
16149
+ snapshotGeneration: null,
16150
+ error: null
16151
+ };
16152
+ }
16153
+ selectState(state, kind) {
16154
+ if (state.data === null) return {
16155
+ ...state,
16156
+ data: null
16157
+ };
16158
+ if (state.data.kind !== kind) throw new Error(`Unexpected Store content kind: ${state.data.kind}`);
16159
+ return {
16160
+ ...state,
16161
+ data: state.data.value
16162
+ };
16163
+ }
16164
+ };
16165
+
15412
16166
  //#endregion
15413
16167
  //#region src/store-observation-fallback.ts
15414
16168
  /**
@@ -16087,6 +16841,88 @@ var ToolCommandObservationService = class {
16087
16841
  }
16088
16842
  };
16089
16843
 
16844
+ //#endregion
16845
+ //#region src/tracing.ts
16846
+ /**
16847
+ * Orthogonal intents (created 2026-07-30 Asia/Shanghai):
16848
+ * 1. Bootstrap OpenTelemetry tracing for the backend (diagnostic only, gated by `--otel`).
16849
+ * 2. Expose a single Tracer value (real or noop) so instrumentation sites stay branch-free.
16850
+ * 3. Let the NodeSDK auto-configure from standard OTEL_* environment variables, with an optional
16851
+ * explicit `--otel-endpoint` override; refuse to pollute the console on incomplete config.
16852
+ *
16853
+ * Original request (2026-07-30): "我需要集成 OpenTelemetry ... 通过 --otel --otel-endpoint 来开启。
16854
+ * 因为我要你分析目前速度慢的根本原因。"
16855
+ * Owner correction (2026-07-30): --otel is just the switch; concrete config (endpoint, headers,
16856
+ * sampling) should come from standard OTel ENV (OTEL_EXPORTER_OTLP_ENDPOINT etc.).
16857
+ *
16858
+ * Design: `--otel` flips the switch. When on, a NodeSDK is started WITHOUT an explicit
16859
+ * traceExporter so it auto-resolves from OTEL_* env vars (the canonical OTel workflow). An explicit
16860
+ * `--otel-endpoint` provides one convenience override for the OTLP/HTTP endpoint. When `--otel` is
16861
+ * off, `initTracing` returns a noop tracer and never touches the OTel SDK, so every span operation
16862
+ * is a side-effect-free no-op and the manager/lease/invalidation correctness models are untouched.
16863
+ *
16864
+ * Supported ENV (read by NodeSDK, not by us):
16865
+ * OTEL_EXPORTER_OTLP_ENDPOINT Collector base URL
16866
+ * OTEL_EXPORTER_OTLP_HEADERS e.g. "Authorization=Basic <base64>"
16867
+ * OTEL_TRACES_SAMPLER ... standard sampling config
16868
+ */
16869
+ /** No-op tracer returned when tracing is disabled: span methods are inert. */
16870
+ const NOOP_TRACER = trace.getTracer("openspecui-noop");
16871
+ let activeSdk = null;
16872
+ /**
16873
+ * Initialize backend tracing and return the Tracer every instrumentation site should use.
16874
+ *
16875
+ * - `enabled` → start a NodeSDK that auto-configures from OTEL_* env vars. An explicit `endpoint`
16876
+ * overrides the OTLP/HTTP export target; otherwise the SDK honors OTEL_EXPORTER_OTLP_ENDPOINT /
16877
+ * OTEL_EXPORTER_OTLP_HEADERS etc. directly.
16878
+ * - `!enabled` → return NOOP_TRACER without constructing or starting any SDK.
16879
+ */
16880
+ /** Whether any standard OTel exporter env var is set (so the SDK has something to export to). */
16881
+ function hasOtelExporterEnv() {
16882
+ return Boolean(process.env.OTEL_EXPORTER_OTLP_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_HEADERS || process.env.OTEL_TRACES_EXPORTER);
16883
+ }
16884
+ /**
16885
+ * Normalize a user-supplied OTLP/HTTP endpoint into the full traces export URL.
16886
+ *
16887
+ * `OTLPTraceExporter({ url })` uses the url verbatim — it does NOT append the OTLP signal path.
16888
+ * A bare Collector base like `http://host:4318` therefore POSTs to the root and gets 404. Users
16889
+ * naturally pass a base URL (the same value they'd put in OTEL_EXPORTER_OTLP_ENDPOINT, which the
16890
+ * SDK *does* append to). To keep `--otel-endpoint` ergonomic and consistent with ENV semantics,
16891
+ * append `/v1/traces` only when the URL has no OTLP signal path of its own.
16892
+ *
16893
+ * Exported for unit testing.
16894
+ */
16895
+ function normalizeOtlpHttpEndpoint(rawEndpoint) {
16896
+ const endpoint = rawEndpoint.trim();
16897
+ if (!endpoint) return endpoint;
16898
+ if (/\/v1\/(traces|metrics|logs)$/i.test(endpoint)) return endpoint;
16899
+ return `${endpoint.replace(/\/+$/, "")}/v1/traces`;
16900
+ }
16901
+ function initTracing(config) {
16902
+ if (!config.enabled) return NOOP_TRACER;
16903
+ const hasEndpoint = Boolean(config.endpoint && config.endpoint.length > 0);
16904
+ if (!hasEndpoint && !hasOtelExporterEnv()) {
16905
+ console.warn("[openspecui] --otel is enabled but no exporter is configured. Set OTEL_EXPORTER_OTLP_ENDPOINT (and OTEL_EXPORTER_OTLP_HEADERS if needed), or pass --otel-endpoint, to export spans. Tracing stays a no-op until configured.");
16906
+ return NOOP_TRACER;
16907
+ }
16908
+ const rawLogLevel = process.env.OTEL_LOG_LEVEL;
16909
+ diag.setLogger(new DiagConsoleLogger(), rawLogLevel ? Number(rawLogLevel) : DiagLogLevel.INFO);
16910
+ const resolvedEndpoint = hasEndpoint ? normalizeOtlpHttpEndpoint(config.endpoint) : void 0;
16911
+ activeSdk = new NodeSDK({
16912
+ serviceName: "openspecui-server",
16913
+ traceExporter: resolvedEndpoint ? new OTLPTraceExporter({ url: resolvedEndpoint }) : void 0
16914
+ });
16915
+ activeSdk.start();
16916
+ diag.info("[openspecui] OpenTelemetry tracing started -> %s", resolvedEndpoint ?? "OTEL_* env");
16917
+ return trace.getTracer("openspecui-server");
16918
+ }
16919
+ /** Flush and dispose any active SDK started by `initTracing`. Safe to call when none is active. */
16920
+ async function shutdownTracing() {
16921
+ const sdk = activeSdk;
16922
+ activeSdk = null;
16923
+ if (sdk) await sdk.shutdown();
16924
+ }
16925
+
16090
16926
  //#endregion
16091
16927
  //#region src/translation-cache-adapter.ts
16092
16928
  var SqliteTranslationCacheAdapter = class {
@@ -16321,6 +17157,14 @@ var TranslationCacheService = class {
16321
17157
 
16322
17158
  //#endregion
16323
17159
  //#region src/runtime-package-host.ts
17160
+ /**
17161
+ * Orthogonal intents (updated 2026-07-31 Asia/Shanghai):
17162
+ * 1. Resolve the physical host manifest that owns on-demand runtime installation.
17163
+ * 2. Read declared runtime ranges without treating optional peers as install-time dependencies.
17164
+ * 3. Normalize legacy on-demand optional dependency records after package-manager installation.
17165
+ *
17166
+ * Original request (2026-07-31): "这个依赖好像会导致安装的时候仍然会被强制装上去,可能要改成 peerDependencies 会更好"
17167
+ */
16324
17168
  function resolveRuntimeHostPackageContext(startDir) {
16325
17169
  let currentDir = startDir;
16326
17170
  let openspecuiPackage = null;
@@ -16350,7 +17194,7 @@ function resolveRuntimeHostPackageContext(startDir) {
16350
17194
  }
16351
17195
  function readRuntimeHostPackageDependencyRequest(input) {
16352
17196
  const parsed = readRuntimeHostPackageJson(input.runtimeHost.packageJsonPath);
16353
- const range = parsed.optionalDependencies?.[input.packageName] ?? parsed.dependencies?.[input.packageName] ?? input.fallbackRange;
17197
+ const range = parsed.optionalDependencies?.[input.packageName] ?? parsed.dependencies?.[input.packageName] ?? parsed.peerDependencies?.[input.packageName] ?? input.fallbackRange;
16354
17198
  return `${input.packageName}@${range}`;
16355
17199
  }
16356
17200
  async function readRuntimeHostPackageDependencyTree(input) {
@@ -17025,7 +17869,7 @@ var TranslationEngineService = class {
17025
17869
  defaultModel: model ?? globalSettings.translationEngines.localLlama.model,
17026
17870
  cacheDir: this.localLlamaCacheDir
17027
17871
  });
17028
- return (await import("./src-DTmyMpWU.mjs")).createOpenAICompletionTranslatorFactory({
17872
+ return (await import("./src-Ct1Z-zfA.mjs")).createOpenAICompletionTranslatorFactory({
17029
17873
  baseUrl: globalSettings.translationEngines.openai.baseUrl,
17030
17874
  token: globalSettings.translationEngines.openai.token,
17031
17875
  model: model ?? globalSettings.translationEngines.openai.model
@@ -17950,7 +18794,7 @@ if (!isMainThread) {
17950
18794
  //#endregion
17951
18795
  //#region src/server.ts
17952
18796
  /**
17953
- * Orthogonal intents (updated 2026-07-27 Asia/Shanghai):
18797
+ * Orthogonal intents (updated 2026-07-31 Asia/Shanghai):
17954
18798
  * 1. Bootstrap the HTTP/tRPC server and launch-project runtime services.
17955
18799
  * 2. Delegate OpenSpec filesystem ownership to the CLI-selected planning-root manager.
17956
18800
  * 3. Host Server-local terminal/Root Context notifications, sound, preview-resource, and translation HTTP boundaries.
@@ -17959,6 +18803,9 @@ if (!isMainThread) {
17959
18803
  * 5. Own the runtime observation environment and external Codex command lease while deferring expensive
17960
18804
  * root-scoped projections until a foreground consumer requests them; keep Store-list registry/mutation
17961
18805
  * evidence distinct from selector-exact Store-root observation.
18806
+ * 6. Initialize backend OpenTelemetry tracing (--otel diagnostic only) and expose the resolved Tracer
18807
+ * through Context so procedure/planning-root instrumentation stays branch-free.
18808
+ * 7. Admit dynamic loopback App origins while keeping remote browser origins explicitly allowlisted.
17962
18809
  *
17963
18810
  * Original request (2026-07-15): "你先负责后端(内核)的开发。"
17964
18811
  * Original request (2026-07-17): "Every Planning-root execution surface uses the same operation lifetime owner."
@@ -17972,6 +18819,8 @@ if (!isMainThread) {
17972
18819
  * Owner-reported defect (2026-07-26): App-embedded Project Web must enter its canonical Dashboard
17973
18820
  * route directly instead of visibly normalizing the backend root after iframe launch.
17974
18821
  * Original request (2026-07-26): "展开全面的接口升级和内核升级和测试升级。"
18822
+ * Built-runtime defect (2026-07-30): Direct Web shutdown must await HTTP closure and retire non-cooperative WebSocket and buffered CLI children after one signal.
18823
+ * Original request (2026-07-31): Trace every Planning-root write lock with timing, source, and stack evidence.
17975
18824
  *
17976
18825
  * @module server
17977
18826
  */
@@ -18005,6 +18854,7 @@ function createRootContextProjectionNotificationSource(service) {
18005
18854
  * Create an OpenSpecUI HTTP server with optional WebSocket support
18006
18855
  */
18007
18856
  function createServer(config) {
18857
+ const tracer = initTracing(config.tracing ?? { enabled: false });
18008
18858
  const accessGate = createAccessGate({
18009
18859
  credential: config.accessGate ?? null,
18010
18860
  loopback: true
@@ -18057,6 +18907,13 @@ function createServer(config) {
18057
18907
  storeObservation,
18058
18908
  workOwner: storeProjectionWorkOwner
18059
18909
  });
18910
+ const storeContentProjectionService = new StoreContentProjectionService({
18911
+ dataScopePath: dataScope.path,
18912
+ cliExecutor,
18913
+ invalidation: runtimeInvalidation,
18914
+ storeObservation,
18915
+ workOwner: createStoreContentProjectionWorkOwner(projectionWorkRuntime)
18916
+ });
18060
18917
  const planningRootServices = new PlanningRootServiceManager({
18061
18918
  launchProjectDir: config.projectDir,
18062
18919
  previewAssetsDir: config.previewAssetsDir ?? join(__dirname, "..", "..", "web", "dist"),
@@ -18069,7 +18926,8 @@ function createServer(config) {
18069
18926
  codeBinding: codeGitBinding,
18070
18927
  dashboardProjectionWorkOwner,
18071
18928
  changesProjectionWorkOwner,
18072
- planningCliProjectionWorkOwner
18929
+ planningCliProjectionWorkOwner,
18930
+ tracer
18073
18931
  });
18074
18932
  const rootContextProjectionService = new RootContextProjectionService({
18075
18933
  launchProjectDir: config.projectDir,
@@ -18182,7 +19040,15 @@ function createServer(config) {
18182
19040
  const app = new Hono();
18183
19041
  const corsOrigins = config.corsOrigins ?? ["http://localhost:5173", "http://localhost:3000"];
18184
19042
  app.use("*", cors({
18185
- origin: corsOrigins,
19043
+ origin: (origin) => {
19044
+ if (corsOrigins.includes(origin)) return origin;
19045
+ try {
19046
+ const url = new URL(origin);
19047
+ return (url.protocol === "http:" || url.protocol === "https:") && isLoopbackHostname(url.hostname) ? origin : void 0;
19048
+ } catch {
19049
+ return;
19050
+ }
19051
+ },
18186
19052
  credentials: true
18187
19053
  }));
18188
19054
  app.use("*", createAccessGateMiddleware(accessGate));
@@ -18259,6 +19125,7 @@ function createServer(config) {
18259
19125
  runtimeInvalidation,
18260
19126
  storeObservation,
18261
19127
  storeProjectionService,
19128
+ storeContentProjectionService,
18262
19129
  rootContextProjectionService,
18263
19130
  environmentGlobalProjectionService,
18264
19131
  storeMutationService,
@@ -18277,7 +19144,8 @@ function createServer(config) {
18277
19144
  gitWorktreeHandoff: config.gitWorktreeHandoff,
18278
19145
  watcher,
18279
19146
  projectDir: config.projectDir,
18280
- envUri
19147
+ envUri,
19148
+ tracer
18281
19149
  })
18282
19150
  });
18283
19151
  });
@@ -18299,6 +19167,7 @@ function createServer(config) {
18299
19167
  runtimeInvalidation,
18300
19168
  storeObservation,
18301
19169
  storeProjectionService,
19170
+ storeContentProjectionService,
18302
19171
  rootContextProjectionService,
18303
19172
  environmentGlobalProjectionService,
18304
19173
  storeMutationService,
@@ -18317,7 +19186,8 @@ function createServer(config) {
18317
19186
  gitWorktreeHandoff: config.gitWorktreeHandoff,
18318
19187
  watcher,
18319
19188
  projectDir: config.projectDir,
18320
- envUri
19189
+ envUri,
19190
+ tracer
18321
19191
  };
18322
19192
  };
18323
19193
  return {
@@ -18333,6 +19203,7 @@ function createServer(config) {
18333
19203
  dataHomeObserver,
18334
19204
  storeObservation,
18335
19205
  storeProjectionService,
19206
+ storeContentProjectionService,
18336
19207
  rootContextProjectionService,
18337
19208
  environmentGlobalProjectionService,
18338
19209
  storeObservationFallback,
@@ -18353,6 +19224,7 @@ function createServer(config) {
18353
19224
  projectionWorkRuntime,
18354
19225
  watcher,
18355
19226
  createContext,
19227
+ tracer,
18356
19228
  port: config.port ?? 3100
18357
19229
  };
18358
19230
  }
@@ -18424,6 +19296,13 @@ async function createWebSocketServer(server, httpServer, config) {
18424
19296
  const results = await Promise.allSettled(tasks.map((task) => Promise.resolve().then(task)));
18425
19297
  for (const result of results) if (result.status === "rejected") failures.push(result.reason);
18426
19298
  };
19299
+ const closeWebSocketServer = (socketServer) => new Promise((resolve$1, reject) => {
19300
+ for (const client of socketServer.clients) client.terminate();
19301
+ socketServer.close((error) => {
19302
+ if (error) reject(error);
19303
+ else resolve$1();
19304
+ });
19305
+ });
18427
19306
  return {
18428
19307
  wss,
18429
19308
  ptyWss,
@@ -18435,15 +19314,17 @@ async function createWebSocketServer(server, httpServer, config) {
18435
19314
  await settleCleanupPhase(failures, [
18436
19315
  () => handler.broadcastReconnectNotification(),
18437
19316
  () => ptyManager.closeAll(),
18438
- () => ptyWss.close(),
18439
- () => wss.close(),
19317
+ () => closeWebSocketServer(ptyWss),
19318
+ () => closeWebSocketServer(wss),
18440
19319
  () => server.watcher?.stop()
18441
19320
  ]);
18442
19321
  await settleCleanupPhase(failures, [() => server.storeObservationFallback.dispose()]);
18443
19322
  await settleCleanupPhase(failures, [() => server.storeMutationService.dispose()]);
18444
19323
  await settleCleanupPhase(failures, [() => server.rootContextNotificationBridge.dispose()]);
18445
19324
  await settleCleanupPhase(failures, [() => server.storeProjectionService.dispose()]);
19325
+ await settleCleanupPhase(failures, [() => server.storeContentProjectionService.dispose()]);
18446
19326
  await settleCleanupPhase(failures, [() => server.environmentGlobalProjectionService.dispose()]);
19327
+ await settleCleanupPhase(failures, [() => server.cliExecutor.dispose()]);
18447
19328
  await settleCleanupPhase(failures, [() => server.planningRootServices.dispose()]);
18448
19329
  await settleCleanupPhase(failures, [() => server.projectionWorkRuntime.clear()]);
18449
19330
  await settleCleanupPhase(failures, [() => server.storeObservation.dispose()]);
@@ -18491,6 +19372,13 @@ async function startServer(config, setupApp) {
18491
19372
  fetch: server.app.fetch,
18492
19373
  port
18493
19374
  });
19375
+ const closeHttpServer = () => new Promise((resolve$1, reject) => {
19376
+ httpServer.close((error) => {
19377
+ if (error) reject(error);
19378
+ else resolve$1();
19379
+ });
19380
+ if ("closeAllConnections" in httpServer) httpServer.closeAllConnections();
19381
+ });
18494
19382
  const wsServer = await createWebSocketServer(server, httpServer, { projectDir: config.projectDir });
18495
19383
  let closePromise = null;
18496
19384
  return {
@@ -18507,11 +19395,13 @@ async function startServer(config, setupApp) {
18507
19395
  Promise.resolve().then(() => context.localLlamaModelAssetService.close())
18508
19396
  ]);
18509
19397
  const websocketResults = await Promise.allSettled([Promise.resolve().then(() => wsServer.close())]);
18510
- const httpResults = await Promise.allSettled([Promise.resolve().then(() => httpServer.close())]);
19398
+ const httpResults = await Promise.allSettled([closeHttpServer()]);
19399
+ const tracingResults = await Promise.allSettled([shutdownTracing()]);
18511
19400
  const failures = [
18512
19401
  ...modelResults,
18513
19402
  ...websocketResults,
18514
- ...httpResults
19403
+ ...httpResults,
19404
+ ...tracingResults
18515
19405
  ].flatMap((result) => result.status === "rejected" ? [result.reason] : []);
18516
19406
  if (failures.length > 0) throw new AggregateError(failures, "Server shutdown failed.");
18517
19407
  })();