@openspecui/server 6.0.1 → 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 +1140 -250
- package/package.json +15 -4
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 (
|
|
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
|
|
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:
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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-
|
|
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-
|
|
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
|
|
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$
|
|
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$
|
|
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
|
-
|
|
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
|
|
8587
|
-
const
|
|
8588
|
-
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
|
|
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
|
-
|
|
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 ??
|
|
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,
|
|
8652
|
-
const baseWorktrees = await listGitWorktrees(resolvedProjectDir,
|
|
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-
|
|
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
|
-
|
|
9215
|
-
|
|
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]:
|
|
9221
|
-
|
|
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-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
10367
|
-
|
|
10368
|
-
this.
|
|
10369
|
-
|
|
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
|
-
|
|
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
|
|
10497
|
-
|
|
10498
|
-
|
|
10499
|
-
|
|
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)
|
|
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.
|
|
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
|
-
|
|
10511
|
-
|
|
10512
|
-
|
|
10513
|
-
|
|
10514
|
-
|
|
10515
|
-
|
|
10516
|
-
|
|
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
|
-
|
|
10530
|
-
|
|
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 (
|
|
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
|
-
|
|
10541
|
-
|
|
10542
|
-
|
|
10543
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10576
|
-
|
|
10577
|
-
|
|
10578
|
-
|
|
10579
|
-
|
|
10580
|
-
|
|
10581
|
-
|
|
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
|
-
|
|
10618
|
-
|
|
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
|
|
10622
|
-
|
|
10623
|
-
|
|
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
|
-
|
|
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 =
|
|
10674
|
-
await
|
|
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$
|
|
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$
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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() })).
|
|
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() })).
|
|
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() })).
|
|
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() })).
|
|
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
|
|
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
|
|
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
|
|
14683
|
+
return runPlanningRootRead(ctx, ({ adapter }) => adapter.listChanges());
|
|
14154
14684
|
}),
|
|
14155
14685
|
listWithMeta: publicProcedure.query(async ({ ctx }) => {
|
|
14156
|
-
return
|
|
14686
|
+
return runPlanningRootRead(ctx, ({ adapter }) => adapter.listChangesWithMeta());
|
|
14157
14687
|
}),
|
|
14158
14688
|
listArchived: publicProcedure.query(async ({ ctx }) => {
|
|
14159
|
-
return
|
|
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
|
|
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
|
|
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
|
|
14768
|
+
return runPlanningRootRead(ctx, ({ adapter }) => adapter.listArchivedChanges());
|
|
14239
14769
|
}),
|
|
14240
14770
|
listWithMeta: publicProcedure.query(async ({ ctx }) => {
|
|
14241
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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).
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
15476
|
+
return runPlanningRootRead(ctx, ({ dashboardProjectionService }) => dashboardProjectionService.getTrends());
|
|
14949
15477
|
}),
|
|
14950
15478
|
getGit: publicProcedure.query(({ ctx }) => {
|
|
14951
|
-
return
|
|
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
|
-
})).
|
|
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
|
-
})).
|
|
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
|
-
})])).
|
|
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.
|
|
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.
|
|
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.
|
|
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) {
|
|
@@ -17950,7 +18794,7 @@ if (!isMainThread) {
|
|
|
17950
18794
|
//#endregion
|
|
17951
18795
|
//#region src/server.ts
|
|
17952
18796
|
/**
|
|
17953
|
-
* Orthogonal intents (updated 2026-07-
|
|
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:
|
|
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
|
|
18439
|
-
() => wss
|
|
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([
|
|
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
|
})();
|