@zq-silk/yui 2.17.0 → 2.18.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.
Files changed (70) hide show
  1. package/dist/cli/commandCatalog.js +2 -1
  2. package/dist/commands/controllerCommands.js +13 -4
  3. package/dist/commands/durableJobCommands.js +50 -3
  4. package/dist/commands/globalRoleCommands.js +18 -12
  5. package/dist/commands/inboxCommands.js +1 -0
  6. package/dist/commands/operatorCommands.js +10 -2
  7. package/dist/controlPlaneCli.js +21 -4
  8. package/dist/controller/clientRuntime.js +34 -13
  9. package/dist/controller/controller.js +35 -22
  10. package/dist/controller/diagnosticJournal.js +214 -0
  11. package/dist/controller/fileSchedulerStoreAdapter.js +139 -101
  12. package/dist/controller/globalInputDelivery.js +6 -2
  13. package/dist/controller/globalRuntimeAttention.js +11 -3
  14. package/dist/controller/jobControl.js +1 -1
  15. package/dist/controller/jobDefinitionControl.js +24 -13
  16. package/dist/controller/jobSupervisor.js +4 -0
  17. package/dist/controller/runtime.js +66 -4
  18. package/dist/controller/workerSchedulerStore.js +24 -0
  19. package/dist/core/boundedRpc.js +4 -0
  20. package/dist/core/controllerServer.js +1 -1
  21. package/dist/core/controllerTelemetry.js +92 -19
  22. package/dist/job/globalShell.js +51 -0
  23. package/dist/job/jobBusinessResult.js +4 -3
  24. package/dist/job/jobDefinition.js +6 -1
  25. package/dist/job/jobRunner.js +9 -0
  26. package/dist/job/scheduleService.js +2 -0
  27. package/dist/message/globalInboxRecovery.js +30 -19
  28. package/dist/message/globalProviderRetry.js +7 -1
  29. package/dist/runtime/builtinAgentErrorMappers.js +4 -0
  30. package/dist/runtime/globalSessionHealth.js +39 -0
  31. package/dist/scheduler/roleRunStall.js +10 -10
  32. package/dist/storage/persistenceWorker.js +41 -19
  33. package/dist/storage/sqliteSchema.js +35 -11
  34. package/dist/storage/sqliteStore.js +86 -8
  35. package/dist/storage/storageVersions.js +1 -1
  36. package/dist/storage/storeRpc.js +25 -11
  37. package/dist/storage/taskReadRevisionSchema.js +39 -0
  38. package/dist/web/assets/client/app/catalog.js +0 -29
  39. package/dist/web/assets/client/app/jobs.js +115 -0
  40. package/dist/web/assets/client/app/main.js +17 -6
  41. package/dist/web/assets/client/app/manage.js +1 -0
  42. package/dist/web/assets/client/app/settings.js +1 -5
  43. package/dist/web/assets/client/i18n/en.js +203 -18
  44. package/dist/web/assets/client/i18n/zh-CN.js +202 -17
  45. package/dist/web/assets/client/index.js +2 -0
  46. package/dist/web/assets/client/layout/manageShell.js +61 -15
  47. package/dist/web/assets/client/layout/router.js +2 -1
  48. package/dist/web/assets/client/lib/prefs.js +1 -1
  49. package/dist/web/assets/client/views/jobs.js +1042 -160
  50. package/dist/web/assets/client/views/sidebar.js +3 -0
  51. package/dist/web/assets/shared/manageNav.js +3 -1
  52. package/dist/web/assets/shell/appRail.js +2 -1
  53. package/dist/web/assets/shell/manage.js +7 -2
  54. package/dist/web/assets/shell/models.js +4 -3
  55. package/dist/web/assets/shell/settingsContent.js +0 -3
  56. package/dist/web/assets/shell/sidebar.js +0 -1
  57. package/dist/web/assets/styles/views/jobs.js +301 -12
  58. package/dist/web/assets/styles/views/sidebar.js +1 -2
  59. package/dist/web/webServer.js +28 -1
  60. package/docs/global-shell-jobs.md +78 -0
  61. package/docs/job-runs.md +6 -3
  62. package/docs/managed-turn-and-session-runtime.md +74 -0
  63. package/docs/managed-turn-and-session-runtime.zh-CN.md +51 -0
  64. package/docs/release-workflow.md +43 -0
  65. package/docs/release-workflow.zh-CN.md +29 -0
  66. package/docs/storage-baseline.md +14 -1
  67. package/docs/storage-baseline.zh-CN.md +6 -1
  68. package/docs/testing/verification-levels.md +2 -2
  69. package/docs/testing/verification-levels.zh-CN.md +2 -2
  70. package/package.json +1 -1
@@ -0,0 +1,214 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { mkdir, open, rename } from "node:fs/promises";
4
+ import { dirname, join, relative } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { monotonicMilliseconds } from "../core/controllerTelemetry.js";
7
+ import { YUI_VERSION } from "../version.js";
8
+ // Operational evidence only: no business records, recovery policy or SQLite
9
+ // writes. Admission is bounded in memory and all filesystem work is async.
10
+ const MAX_RECORD_BYTES = 16 * 1024;
11
+ const MAX_PENDING = 32;
12
+ const NUMERIC_KEYS = new Set([
13
+ "completed", "failed", "received", "inFlight", "maximumMs", "maximumLagMs",
14
+ "maximumObservedAtMs", "oldestInFlightAgeMs", "startedAtMs", "elapsedMs",
15
+ "ageMs", "depth", "semanticDepth", "progressDepth", "pendingKeys", "queueDepth",
16
+ "hits", "rebuilds", "invalidations", "passes", "listedEvents", "selectedEvents"
17
+ ]);
18
+ const CONTAINER_KEYS = new Set([
19
+ "commands", "observations", "routes", "eventLoopDelay", "phases", "reads",
20
+ "persistence", "main", "worker", "database", "roundTrips", "work", "body",
21
+ "lock", "commit", "payloadReads", "inbox", "scheduler", "projectionCache",
22
+ "active", "recent", "slowest", "latest"
23
+ ]);
24
+ const ERROR_CODES = new Set([
25
+ "SQLITE_BUSY", "SQLITE_LOCKED", "SQLITE_IOERR", "SQLITE_FULL", "SQLITE_READONLY",
26
+ "ENOENT", "EACCES", "EPERM", "ENOSPC", "CONTROLLER_TIMEOUT", "COMMAND_TIMED_OUT"
27
+ ]);
28
+ const SOURCE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
29
+ function errorEvidence(error) {
30
+ const frames = [];
31
+ let candidate = error instanceof Error ? error : null;
32
+ let code = "UNCLASSIFIED";
33
+ const fingerprint = createHash("sha256").update((candidate?.message ?? "non-error").slice(0, 4096)).digest("hex");
34
+ for (let depth = 0; candidate !== null && depth < 4; depth++) {
35
+ const rawCode = "code" in candidate ? candidate.code : undefined;
36
+ if (typeof rawCode === "string" && ERROR_CODES.has(rawCode))
37
+ code = rawCode;
38
+ for (const line of (candidate.stack ?? "").slice(0, 8192).split("\n").slice(1, 12)) {
39
+ const match = line.match(/(file:\/\/\/[^()]+):(\d+):(\d+)\)?$/);
40
+ if (match === null || frames.length >= 8)
41
+ continue;
42
+ try {
43
+ const module = relative(SOURCE_ROOT, fileURLToPath(match[1]));
44
+ // Only source locations inside this build; never keep the raw stack,
45
+ // function names, absolute paths, exception text or user arguments.
46
+ if (/^(?:[a-zA-Z0-9_-]+\/)*[a-zA-Z0-9_-]+\.[jt]s$/.test(module)) {
47
+ frames.push({ module, line: Number(match[2]), column: Number(match[3]) });
48
+ }
49
+ }
50
+ catch { /* Not a source location. */ }
51
+ }
52
+ candidate = candidate.cause instanceof Error ? candidate.cause : null;
53
+ }
54
+ return { code, fingerprint, frames };
55
+ }
56
+ /** Allowlisted observations only; never serialize arbitrary errors or params. */
57
+ function safeSnapshot(value, depth = 0) {
58
+ if (depth > 10 || value === null || typeof value !== "object")
59
+ return null;
60
+ if (Array.isArray(value))
61
+ return value.slice(-4).map(item => safeSnapshot(item, depth + 1));
62
+ const safe = {};
63
+ for (const [key, item] of Object.entries(value)) {
64
+ if (NUMERIC_KEYS.has(key) && (typeof item === "boolean"
65
+ || (typeof item === "number" && Number.isFinite(item))))
66
+ safe[key] = item;
67
+ else if (CONTAINER_KEYS.has(key))
68
+ safe[key] = safeSnapshot(item, depth + 1);
69
+ else if (key === "target" && typeof item === "string" && /^[a-f0-9]{16}$/.test(item))
70
+ safe[key] = item;
71
+ else if (key === "phase" && typeof item === "string" && /^[a-z][a-z0-9.-]{0,63}$/.test(item))
72
+ safe[key] = item;
73
+ else if (key === "instance" && typeof item === "string" && /^[a-f0-9-]{36}$/.test(item))
74
+ safe[key] = item;
75
+ else if ((key === "running" || key === "pendingFull") && typeof item === "boolean")
76
+ safe[key] = item;
77
+ }
78
+ return safe;
79
+ }
80
+ /** A bounded diagnostic journal, readable after exit/restart. Failure is counted
81
+ * but cannot fail semantic work. Force-kill/disk failure can lose pending data. */
82
+ export class ControllerDiagnosticJournal {
83
+ #file;
84
+ #instance = randomUUID();
85
+ #maxBytes;
86
+ #pending = [];
87
+ #drain;
88
+ #closed = false;
89
+ #dropped = 0;
90
+ #writeFailures = 0;
91
+ #written = 0;
92
+ #lastSampleAt = -Infinity;
93
+ #observedThrough = 0;
94
+ constructor(home, options = {}) {
95
+ this.#file = join(home, "runtime", "controller-diagnostics.jsonl");
96
+ this.#maxBytes = options.maxBytes ?? 1024 * 1024;
97
+ if (!Number.isSafeInteger(this.#maxBytes) || this.#maxBytes < 1024) {
98
+ throw new Error("Diagnostic file bound must be at least 1024 bytes.");
99
+ }
100
+ }
101
+ error(error) {
102
+ this.#enqueue("error", { error: errorEvidence(error) });
103
+ }
104
+ sample(snapshot, lagMs) {
105
+ const now = monotonicMilliseconds();
106
+ if (now - this.#lastSampleAt < 1000) {
107
+ this.#dropped++;
108
+ return;
109
+ }
110
+ this.#lastSampleAt = now;
111
+ this.#enqueue("latency", { lagMs, snapshot: safeSnapshot(snapshot) });
112
+ }
113
+ /** Called with memory-only metric snapshots, never a Store read or RPC.
114
+ * Retained completed samples are emitted once; long active work is bounded
115
+ * by the one-second sample rate and rolling files. */
116
+ observe(snapshot, lagMs) {
117
+ let anomaly = lagMs >= 1000;
118
+ let through = this.#observedThrough;
119
+ const visit = (value, depth) => {
120
+ if (depth > 10 || value === null || typeof value !== "object")
121
+ return;
122
+ const item = value;
123
+ if (typeof item.startedAtMs === "number" && typeof item.elapsedMs === "number") {
124
+ const finishedAt = item.startedAtMs + item.elapsedMs;
125
+ if (finishedAt > this.#observedThrough && (item.elapsedMs >= 1000 || item.failed === true))
126
+ anomaly = true;
127
+ through = Math.max(through, finishedAt);
128
+ }
129
+ if (typeof item.ageMs === "number" && item.ageMs >= 1000)
130
+ anomaly = true;
131
+ for (const child of Object.values(item))
132
+ visit(child, depth + 1);
133
+ };
134
+ visit(snapshot, 0);
135
+ this.#observedThrough = through;
136
+ if (anomaly)
137
+ this.sample(snapshot, lagMs);
138
+ }
139
+ metrics() {
140
+ return { written: this.#written, dropped: this.#dropped,
141
+ writeFailures: this.#writeFailures, pending: this.#pending.length };
142
+ }
143
+ #enqueue(kind, detail) {
144
+ if (this.#closed)
145
+ return;
146
+ if (this.#pending.length >= MAX_PENDING) {
147
+ this.#dropped++;
148
+ return;
149
+ }
150
+ const line = JSON.stringify({ schemaVersion: 1, kind, at: new Date().toISOString(),
151
+ monotonicMs: monotonicMilliseconds(), pid: process.pid, instance: this.#instance,
152
+ version: YUI_VERSION, dropped: this.#dropped, ...detail }) + "\n";
153
+ if (Buffer.byteLength(line) > Math.min(MAX_RECORD_BYTES, this.#maxBytes)) {
154
+ this.#dropped++;
155
+ return;
156
+ }
157
+ this.#pending.push(line);
158
+ this.#startDrain();
159
+ }
160
+ #startDrain() {
161
+ if (this.#drain === undefined) {
162
+ this.#drain = this.#writePending().finally(() => {
163
+ this.#drain = undefined;
164
+ if (this.#pending.length > 0)
165
+ this.#startDrain();
166
+ });
167
+ }
168
+ }
169
+ async #writePending() {
170
+ while (this.#pending.length > 0) {
171
+ const line = this.#pending.shift();
172
+ try {
173
+ await mkdir(join(this.#file, ".."), { recursive: true, mode: 0o700 });
174
+ let file = await this.#open();
175
+ try {
176
+ if ((await file.stat()).size + Buffer.byteLength(line) > this.#maxBytes) {
177
+ await file.close();
178
+ await rename(`${this.#file}.1`, `${this.#file}.2`).catch(error => {
179
+ if (error.code !== "ENOENT")
180
+ throw error;
181
+ });
182
+ await rename(this.#file, `${this.#file}.1`);
183
+ file = await this.#open();
184
+ }
185
+ await file.writeFile(line);
186
+ this.#written++;
187
+ }
188
+ finally {
189
+ await file.close();
190
+ }
191
+ }
192
+ catch {
193
+ this.#writeFailures++;
194
+ }
195
+ }
196
+ }
197
+ async #open() {
198
+ const file = await open(this.#file, constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | constants.O_NOFOLLOW, 0o600);
199
+ try {
200
+ const info = await file.stat();
201
+ if (!info.isFile() || info.nlink !== 1)
202
+ throw new Error("Invalid diagnostic log target.");
203
+ await file.chmod(0o600);
204
+ return file;
205
+ }
206
+ catch (error) {
207
+ await file.close();
208
+ throw error;
209
+ }
210
+ }
211
+ async flush() { while (this.#drain !== undefined)
212
+ await this.#drain; }
213
+ async close() { this.#closed = true; await this.flush(); }
214
+ }