@mininglamp-oss/octo-runtime-client 0.0.0-stage → 1.1.11
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/LICENSE +190 -0
- package/README.md +38 -2
- package/build-info.json +5 -0
- package/dist/async-question-reply.d.ts +30 -0
- package/dist/async-question-reply.js +199 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +1 -0
- package/dist/client.d.ts +10 -0
- package/dist/client.js +465 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/internal.d.ts +4 -0
- package/dist/internal.js +5 -0
- package/dist/invocation-lifecycle.d.ts +59 -0
- package/dist/invocation-lifecycle.js +287 -0
- package/dist/notification.d.ts +1 -0
- package/dist/notification.js +1 -0
- package/dist/protocol.d.ts +1 -0
- package/dist/protocol.js +1 -0
- package/dist/runtime-path.d.ts +21 -0
- package/dist/runtime-path.js +138 -0
- package/dist/startup-catalog.d.ts +8 -0
- package/dist/startup-catalog.js +65 -0
- package/dist/startup-diagnostics.d.ts +82 -0
- package/dist/startup-diagnostics.js +371 -0
- package/dist/submission-error.d.ts +10 -0
- package/dist/submission-error.js +23 -0
- package/dist/types.d.ts +72 -0
- package/dist/types.js +10 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/actmode.d.ts +4 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/actmode.js +36 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/adapter-metadata.d.ts +7 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/adapter-metadata.js +1 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/browser.d.ts +7 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/browser.js +6 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/context-catalog.d.ts +11 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/context-catalog.js +33 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/context-configuration.d.ts +5 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/context-configuration.js +58 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/context-hash.d.ts +2 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/context-hash.js +5 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/context.d.ts +29 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/context.js +566 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/execution-plan.d.ts +3 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/execution-plan.js +80 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/experience.d.ts +4 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/experience.js +166 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/failure.d.ts +82 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/failure.js +562 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/generated/protocol-types.d.ts +3022 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/generated/protocol-types.js +6 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/index.d.ts +124 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/index.js +709 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/invoke-input.d.ts +129 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/invoke-input.js +177 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/invoke-size.d.ts +3 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/invoke-size.js +11 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/json-size.d.ts +2 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/json-size.js +91 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/model-context.d.ts +3 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/model-context.js +9 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/multimodal.d.ts +15 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/multimodal.js +166 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/notification.d.ts +2 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/notification.js +137 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/questions.d.ts +9 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/questions.js +84 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/run-protocol.d.ts +20 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/run-protocol.js +294 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/runtime-selection.d.ts +3 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/runtime-selection.js +68 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/service-processes.d.ts +5 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/service-processes.js +65 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/stream-limits.d.ts +27 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/dist/stream-limits.js +56 -0
- package/node_modules/@mininglamp-oss/octo-runtime-protocol/package.json +15 -0
- package/package.json +55 -4
- package/schema/context-configuration-fixtures.json +1219 -0
- package/schema/context-fixtures.json +5988 -0
- package/schema/kernel-client.behavior.json +56 -0
- package/schema/kernel-ipc-fixtures.json +53 -0
- package/schema/kernel-ipc.schema.json +538 -0
- package/schema/native-invoke-fixtures.json +116 -0
- package/schema/protocol.schema.json +4525 -0
- package/schema/run-protocol-fixtures.json +761 -0
- package/schema/runtime-selection-fixtures.json +663 -0
- package/schema/session-name-fixtures.json +191 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
import { admissionClosedError } from "./submission-error.js";
|
|
3
|
+
import { isHostTerminal } from "@mininglamp-oss/octo-runtime-protocol";
|
|
4
|
+
import { KernelError } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* 每个 client 独立保存 invoke 的身份、取消和观察状态;执行与断线恢复由 client.ts 负责。
|
|
7
|
+
* 最多登记 128 项,不缓存正文;未知结果继续占用额度,不能通过删除记录伪造排空成功。
|
|
8
|
+
*/
|
|
9
|
+
export class InvocationRegistry {
|
|
10
|
+
transport;
|
|
11
|
+
#records = new Map();
|
|
12
|
+
#stopped = false;
|
|
13
|
+
#draining;
|
|
14
|
+
#drainDeadline;
|
|
15
|
+
#closed = false;
|
|
16
|
+
#cancelling = 0;
|
|
17
|
+
#cancelQueue = [];
|
|
18
|
+
#cancelControllers = new Set();
|
|
19
|
+
constructor(transport) {
|
|
20
|
+
this.transport = transport;
|
|
21
|
+
}
|
|
22
|
+
begin(key) {
|
|
23
|
+
this.assertSubmission();
|
|
24
|
+
if (this.#records.has(key.invocation_id))
|
|
25
|
+
throw new KernelError("EXECUTION_IDENTITY_CONFLICT");
|
|
26
|
+
if (this.#records.size >= 128)
|
|
27
|
+
throw new KernelError("CLIENT_INVOCATION_CAPACITY_EXHAUSTED");
|
|
28
|
+
let settleAdmission;
|
|
29
|
+
const admitted = new Promise((resolve) => {
|
|
30
|
+
settleAdmission = resolve;
|
|
31
|
+
});
|
|
32
|
+
const record = {
|
|
33
|
+
key,
|
|
34
|
+
submitted: false,
|
|
35
|
+
phase: "admitting",
|
|
36
|
+
cancellation: "none",
|
|
37
|
+
lastSeq: 0,
|
|
38
|
+
admitted,
|
|
39
|
+
settleAdmission
|
|
40
|
+
};
|
|
41
|
+
this.#records.set(key.invocation_id, record);
|
|
42
|
+
return record;
|
|
43
|
+
}
|
|
44
|
+
assertSubmission() {
|
|
45
|
+
if (this.#stopped)
|
|
46
|
+
throw admissionClosedError();
|
|
47
|
+
}
|
|
48
|
+
accepted(record, ref, terminal) {
|
|
49
|
+
record.ref = ref;
|
|
50
|
+
record.instance = ref.kernel_instance_id;
|
|
51
|
+
record.phase = "accepted";
|
|
52
|
+
record.settleAdmission();
|
|
53
|
+
if (terminal)
|
|
54
|
+
this.observe(ref, terminal);
|
|
55
|
+
if (record.reason !== undefined && !terminal && this.#records.get(ref.invocation_id) === record)
|
|
56
|
+
void this.cancel(record, record.reason).catch(() => undefined);
|
|
57
|
+
}
|
|
58
|
+
reconcile(ref, terminal) {
|
|
59
|
+
const record = this.#records.get(ref.invocation_id);
|
|
60
|
+
if (!record ||
|
|
61
|
+
(record.instance && record.instance !== ref.kernel_instance_id) ||
|
|
62
|
+
Object.entries(record.key).some(([field, value]) => Reflect.get(ref, field) !== value))
|
|
63
|
+
return;
|
|
64
|
+
if (record.ref &&
|
|
65
|
+
(record.ref.stream_id !== ref.stream_id || record.ref.protocol !== ref.protocol))
|
|
66
|
+
throw new KernelError("RUN_IDENTITY_MISMATCH");
|
|
67
|
+
if (!record.ref)
|
|
68
|
+
this.accepted(record, ref, terminal);
|
|
69
|
+
else if (terminal)
|
|
70
|
+
this.observe(ref, terminal);
|
|
71
|
+
}
|
|
72
|
+
rejected(record, error) {
|
|
73
|
+
if (record.ref) {
|
|
74
|
+
record.settleAdmission();
|
|
75
|
+
this.interrupted(record, error);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
record.admissionError = error instanceof Error ? error : new KernelError("ACCEPTANCE_UNKNOWN");
|
|
79
|
+
record.observationError = errorCode(error);
|
|
80
|
+
record.phase = "acceptance_unknown";
|
|
81
|
+
record.settleAdmission();
|
|
82
|
+
const rejectedByServer = error instanceof KernelError &&
|
|
83
|
+
error.data !== undefined &&
|
|
84
|
+
!["KERNEL_INSTANCE_CHANGED", "RUN_IDENTITY_MISMATCH"].includes(error.code);
|
|
85
|
+
// 仅未发送或明确拒绝可安全移除;已发送但 ACK 未知时保留身份,后续 drain 只能查询确认。
|
|
86
|
+
if (!record.submitted || rejectedByServer)
|
|
87
|
+
this.#records.delete(record.key.invocation_id);
|
|
88
|
+
}
|
|
89
|
+
observe(ref, frame) {
|
|
90
|
+
const record = this.#records.get(ref.invocation_id);
|
|
91
|
+
if (!record ||
|
|
92
|
+
!record.ref ||
|
|
93
|
+
record.ref.stream_id !== ref.stream_id ||
|
|
94
|
+
record.ref.kernel_instance_id !== ref.kernel_instance_id)
|
|
95
|
+
return;
|
|
96
|
+
record.lastSeq = Math.max(record.lastSeq, frame.seq);
|
|
97
|
+
if (!isHostTerminal(frame))
|
|
98
|
+
return;
|
|
99
|
+
if (frame.kind === "control" &&
|
|
100
|
+
frame.control.type === "state" &&
|
|
101
|
+
frame.control.state === "lost") {
|
|
102
|
+
record.phase = "unobserved";
|
|
103
|
+
record.observationError = "RUN_OUTCOME_UNKNOWN";
|
|
104
|
+
}
|
|
105
|
+
else
|
|
106
|
+
this.#records.delete(ref.invocation_id);
|
|
107
|
+
}
|
|
108
|
+
interrupted(record, error) {
|
|
109
|
+
if (this.#records.get(record.key.invocation_id) !== record || !record.ref)
|
|
110
|
+
return;
|
|
111
|
+
record.phase = "unobserved";
|
|
112
|
+
if (error !== undefined)
|
|
113
|
+
record.observationError = errorCode(error);
|
|
114
|
+
}
|
|
115
|
+
active(id) {
|
|
116
|
+
const record = this.#records.get(id);
|
|
117
|
+
if (!record)
|
|
118
|
+
throw new KernelError("INVOCATION_NOT_TRACKED");
|
|
119
|
+
return record;
|
|
120
|
+
}
|
|
121
|
+
ref(id) {
|
|
122
|
+
const record = this.active(id);
|
|
123
|
+
if (!record.ref)
|
|
124
|
+
throw new KernelError("INVOCATION_NOT_ACCEPTED");
|
|
125
|
+
return record.ref;
|
|
126
|
+
}
|
|
127
|
+
cancel(record, reason = "caller_cancelled", { explicit = false } = {}) {
|
|
128
|
+
if (record.ref && this.#records.get(record.key.invocation_id) !== record)
|
|
129
|
+
return Promise.resolve({ accepted: false });
|
|
130
|
+
if (record.observationError === "RUN_OUTCOME_UNKNOWN")
|
|
131
|
+
return Promise.reject(new KernelError("RUN_OUTCOME_UNKNOWN"));
|
|
132
|
+
record.reason ??= reason;
|
|
133
|
+
if (record.cancellation === "none")
|
|
134
|
+
record.cancellation = "requested";
|
|
135
|
+
// 取消可能早于接纳 ACK:记住首次原因,等同一执行获得 RunRef 后发出一次控制请求。
|
|
136
|
+
if (!record.ref)
|
|
137
|
+
return record.admitted.then(() => {
|
|
138
|
+
if (!record.ref)
|
|
139
|
+
throw record.admissionError ?? new KernelError("ACCEPTANCE_UNKNOWN");
|
|
140
|
+
// ACK 接续仍属于原意图;不能因其首次控制已失败而暗中再发送一次。
|
|
141
|
+
return this.cancel(record, reason);
|
|
142
|
+
});
|
|
143
|
+
// 仅新的公开 Stop 可重试未知控制;drain/signal 保留原 Promise,并发 Stop 共享在途尝试。
|
|
144
|
+
const retry = explicit && record.cancellation === "unknown";
|
|
145
|
+
if (!record.cancelTask || retry) {
|
|
146
|
+
if (retry)
|
|
147
|
+
record.reason = reason;
|
|
148
|
+
record.cancellation = "requested";
|
|
149
|
+
const controller = new AbortController();
|
|
150
|
+
this.#cancelControllers.add(controller);
|
|
151
|
+
record.cancelTask = new Promise((resolve, reject) => {
|
|
152
|
+
this.#cancelQueue.push(async () => {
|
|
153
|
+
try {
|
|
154
|
+
if (this.#closed)
|
|
155
|
+
throw new KernelError("CLIENT_CLOSED");
|
|
156
|
+
if (this.#records.get(record.key.invocation_id) !== record) {
|
|
157
|
+
resolve({ accepted: false });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (record.observationError === "RUN_OUTCOME_UNKNOWN")
|
|
161
|
+
throw new KernelError("RUN_OUTCOME_UNKNOWN");
|
|
162
|
+
const deadline = Math.min(Date.now() + 5000, this.#drainDeadline ?? Infinity);
|
|
163
|
+
if (deadline <= Date.now())
|
|
164
|
+
throw new KernelError("OPERATION_OUTCOME_UNKNOWN");
|
|
165
|
+
resolve(await this.transport.cancel(record.ref, record.reason, deadline, controller.signal));
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
reject(error instanceof Error ? error : new KernelError("OPERATION_OUTCOME_UNKNOWN"));
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
this.#pumpCancellations();
|
|
172
|
+
})
|
|
173
|
+
.then((result) => {
|
|
174
|
+
record.cancellation = "acknowledged";
|
|
175
|
+
return result;
|
|
176
|
+
})
|
|
177
|
+
.catch((error) => {
|
|
178
|
+
record.cancellation = "unknown";
|
|
179
|
+
record.observationError = errorCode(error);
|
|
180
|
+
throw error;
|
|
181
|
+
})
|
|
182
|
+
.finally(() => this.#cancelControllers.delete(controller));
|
|
183
|
+
record.firstCancelTask ??= record.cancelTask;
|
|
184
|
+
// Automatic signal cancellation may outlive its consumer; always consume rejection.
|
|
185
|
+
void record.cancelTask.catch(() => undefined);
|
|
186
|
+
}
|
|
187
|
+
return record.cancelTask;
|
|
188
|
+
}
|
|
189
|
+
#pumpCancellations() {
|
|
190
|
+
while (this.#cancelling < 4 && this.#cancelQueue.length) {
|
|
191
|
+
this.#cancelling++;
|
|
192
|
+
void this.#cancelQueue.shift()().finally(() => {
|
|
193
|
+
this.#cancelling--;
|
|
194
|
+
this.#pumpCancellations();
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
snapshot() {
|
|
199
|
+
return [...this.#records.values()].map((record) => ({
|
|
200
|
+
key: { ...record.key },
|
|
201
|
+
...(record.ref ? { ref: { ...record.ref } } : {}),
|
|
202
|
+
phase: record.phase,
|
|
203
|
+
cancellation: record.cancellation,
|
|
204
|
+
lastSeq: record.lastSeq,
|
|
205
|
+
...(record.observationError ? { observationError: record.observationError } : {})
|
|
206
|
+
}));
|
|
207
|
+
}
|
|
208
|
+
stopAdmission() {
|
|
209
|
+
this.#stopped = true;
|
|
210
|
+
}
|
|
211
|
+
close() {
|
|
212
|
+
this.#closed = true;
|
|
213
|
+
this.stopAdmission();
|
|
214
|
+
for (const controller of this.#cancelControllers)
|
|
215
|
+
controller.abort();
|
|
216
|
+
}
|
|
217
|
+
drain(options) {
|
|
218
|
+
if (!Number.isSafeInteger(options.deadline) || options.deadline - Date.now() > 2_147_483_647)
|
|
219
|
+
throw new KernelError("INVOCATION_DEADLINE_INVALID");
|
|
220
|
+
// 首次关闭锁定绝对期限;重复调用不能延长预算,也不能启动第二轮取消副作用。
|
|
221
|
+
if (this.#draining)
|
|
222
|
+
return this.#draining;
|
|
223
|
+
this.stopAdmission();
|
|
224
|
+
this.#drainDeadline = options.deadline;
|
|
225
|
+
return (this.#draining = this.#drain(options));
|
|
226
|
+
}
|
|
227
|
+
async #drain({ deadline, reason }) {
|
|
228
|
+
const controller = new AbortController();
|
|
229
|
+
const timer = setTimeout(() => {
|
|
230
|
+
controller.abort();
|
|
231
|
+
for (const cancelController of this.#cancelControllers)
|
|
232
|
+
cancelController.abort();
|
|
233
|
+
}, Math.max(0, deadline - Date.now()));
|
|
234
|
+
try {
|
|
235
|
+
while (this.#records.size && !controller.signal.aborted && Date.now() < deadline) {
|
|
236
|
+
const records = [...this.#records.values()];
|
|
237
|
+
// 最多四路查询/控制 IO;只查终态元数据,不新增正文订阅,避免与业务消费者争夺尾流。
|
|
238
|
+
let index = 0;
|
|
239
|
+
await Promise.all(Array.from({ length: Math.min(4, records.length) }, async () => {
|
|
240
|
+
while (index < records.length && !controller.signal.aborted && Date.now() < deadline) {
|
|
241
|
+
const record = records[index++];
|
|
242
|
+
record.reason ??= reason;
|
|
243
|
+
if (record.cancellation === "none")
|
|
244
|
+
record.cancellation = "requested";
|
|
245
|
+
if (!record.instance)
|
|
246
|
+
continue;
|
|
247
|
+
try {
|
|
248
|
+
const info = await this.transport.get(record.key, record.instance, deadline, controller.signal);
|
|
249
|
+
if (!record.ref)
|
|
250
|
+
this.accepted(record, info.ref);
|
|
251
|
+
if (info.terminal)
|
|
252
|
+
this.observe(info.ref, info.terminal);
|
|
253
|
+
if (!this.#records.has(record.key.invocation_id))
|
|
254
|
+
continue;
|
|
255
|
+
// lost 表示执行结果未知,必须留在 unresolved,不能据此重放可能已有副作用的执行。
|
|
256
|
+
if (record.observationError !== "RUN_OUTCOME_UNKNOWN")
|
|
257
|
+
await this.cancel(record, reason);
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
if (record.observationError !== "RUN_OUTCOME_UNKNOWN")
|
|
261
|
+
record.observationError = errorCode(error);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}));
|
|
265
|
+
if (this.#records.size && Date.now() < deadline) {
|
|
266
|
+
await delay(Math.min(100, deadline - Date.now()), undefined, {
|
|
267
|
+
signal: controller.signal
|
|
268
|
+
}).catch(() => undefined);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return { complete: this.#records.size === 0, unresolved: this.snapshot() };
|
|
272
|
+
}
|
|
273
|
+
finally {
|
|
274
|
+
clearTimeout(timer);
|
|
275
|
+
controller.abort();
|
|
276
|
+
for (const cancelController of this.#cancelControllers)
|
|
277
|
+
cancelController.abort();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
export function errorCode(error) {
|
|
282
|
+
return error instanceof KernelError
|
|
283
|
+
? error.code
|
|
284
|
+
: error instanceof Error && /^[A-Z][A-Z0-9_]+$/.test(error.message)
|
|
285
|
+
? error.message
|
|
286
|
+
: "INVOCATION_OBSERVATION_FAILED";
|
|
287
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "@mininglamp-oss/octo-runtime-protocol/notification";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "@mininglamp-oss/octo-runtime-protocol/notification";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "@mininglamp-oss/octo-runtime-protocol";
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "@mininglamp-oss/octo-runtime-protocol";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type StartupDiagnostic, type StartupPathDiagnostic } from "./startup-diagnostics.js";
|
|
2
|
+
export type { StartupPathDiagnostic } from "./startup-diagnostics.js";
|
|
3
|
+
/** 启动域的受控路径失败;保留公共码、cause 和 #306 的平铺属性读取。 */
|
|
4
|
+
export declare class RuntimeHomeError extends Error {
|
|
5
|
+
readonly code: string;
|
|
6
|
+
readonly diagnostic: Readonly<StartupDiagnostic & StartupPathDiagnostic>;
|
|
7
|
+
constructor(diagnostic: Omit<StartupPathDiagnostic, "domain" | "action"> & {
|
|
8
|
+
action?: string;
|
|
9
|
+
}, cause?: unknown);
|
|
10
|
+
}
|
|
11
|
+
/** 先检查原始输入,再规范化;这是纯路径计算,不代表目录已存在或启动已验证。 */
|
|
12
|
+
export declare function normalizeRuntimeHome(input: string, platform?: NodeJS.Platform, resource?: string): string;
|
|
13
|
+
export interface PathLimits {
|
|
14
|
+
path: number;
|
|
15
|
+
name: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function platformLimits(platform: NodeJS.Platform): PathLimits;
|
|
18
|
+
/** 检查实际附加文件名的预算;Windows 按 UTF-16 code unit,POSIX 按 UTF-8 字节。 */
|
|
19
|
+
export declare function assertRuntimePath(path: string, resource: string, platform?: NodeJS.Platform, limits?: PathLimits): void;
|
|
20
|
+
/** endpoint-only API 只拥有传输,不推断或创建 Home;同样在连接/监听前拒绝无效或超长地址。 */
|
|
21
|
+
export declare function assertIpcEndpoint(endpoint: string, platform?: NodeJS.Platform): void;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { posix, win32 } from "node:path";
|
|
2
|
+
import { describeStartupError, startupPathDiagnostic } from "./startup-diagnostics.js";
|
|
3
|
+
/** 启动域的受控路径失败;保留公共码、cause 和 #306 的平铺属性读取。 */
|
|
4
|
+
export class RuntimeHomeError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
diagnostic;
|
|
7
|
+
constructor(diagnostic, cause) {
|
|
8
|
+
super(diagnostic.code, { cause });
|
|
9
|
+
this.name = "RuntimeHomeError";
|
|
10
|
+
this.code = diagnostic.code;
|
|
11
|
+
Object.defineProperty(this, "startupContext", { value: diagnostic });
|
|
12
|
+
const shared = describeStartupError(this);
|
|
13
|
+
const legacy = startupPathDiagnostic(shared);
|
|
14
|
+
// 兼容读取的别名不进入 JSON;序列化只有一个标准诊断结构,文案不能由输入 action 覆盖。
|
|
15
|
+
for (const [key, value] of Object.entries(legacy ?? {})) {
|
|
16
|
+
if (!(key in shared))
|
|
17
|
+
Object.defineProperty(shared, key, { value });
|
|
18
|
+
}
|
|
19
|
+
this.diagnostic = Object.freeze(shared);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** 先检查原始输入,再规范化;这是纯路径计算,不代表目录已存在或启动已验证。 */
|
|
23
|
+
export function normalizeRuntimeHome(input, platform = process.platform, resource = "home") {
|
|
24
|
+
const fail = (code) => {
|
|
25
|
+
throw new RuntimeHomeError({
|
|
26
|
+
code,
|
|
27
|
+
...(typeof input === "string" ? { path: input } : {}),
|
|
28
|
+
resource,
|
|
29
|
+
operation: "validate-input"
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
if (typeof input !== "string" ||
|
|
33
|
+
!input.trim() ||
|
|
34
|
+
input.includes("\0") ||
|
|
35
|
+
/[\uD800-\uDFFF]/u.test(input))
|
|
36
|
+
fail("HOME_INVALID");
|
|
37
|
+
if (!["win32", "darwin", "linux"].includes(platform))
|
|
38
|
+
fail("HOME_UNSUPPORTED");
|
|
39
|
+
const api = platform === "win32" ? win32 : posix;
|
|
40
|
+
if (!api.isAbsolute(input))
|
|
41
|
+
fail("HOME_INVALID");
|
|
42
|
+
if (platform === "win32") {
|
|
43
|
+
// 仅支持本地盘绝对路径。拒绝 UNC/device/root-relative/ADS 和 Win32 别名,避免 hash 与 IO 指向不同资源。
|
|
44
|
+
const local = input.replaceAll("/", "\\");
|
|
45
|
+
if (!/^[a-z]:\\/i.test(local) ||
|
|
46
|
+
/[<>"|?*]/.test(local) ||
|
|
47
|
+
[...local].some((character) => character.codePointAt(0) < 32) ||
|
|
48
|
+
local.slice(2).includes(":"))
|
|
49
|
+
fail("HOME_UNSUPPORTED");
|
|
50
|
+
for (const part of local.slice(3).split("\\")) {
|
|
51
|
+
if (part === "." || part === "..")
|
|
52
|
+
continue;
|
|
53
|
+
if (/[ .]$/.test(part) || /^(con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/i.test(part))
|
|
54
|
+
fail("HOME_UNSUPPORTED");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const home = api.resolve(input);
|
|
58
|
+
if (home === api.parse(home).root)
|
|
59
|
+
fail("HOME_UNSUPPORTED");
|
|
60
|
+
return home;
|
|
61
|
+
}
|
|
62
|
+
// Unix 的纯计算上限来自对应 OS 的 PATH_MAX/NAME_MAX;真实启动再查询所选文件系统。
|
|
63
|
+
// Windows Node/libuv 使用扩展 UTF-16 路径:32767 包括 NUL 和自动追加的 \\?\ 前缀,不使用 MAX_PATH=260。
|
|
64
|
+
export function platformLimits(platform) {
|
|
65
|
+
if (platform === "win32")
|
|
66
|
+
return { path: 32767 - 1 - 4, name: 255 };
|
|
67
|
+
if (platform === "linux")
|
|
68
|
+
return { path: 4096 - 1, name: 255 };
|
|
69
|
+
if (platform === "darwin")
|
|
70
|
+
return { path: 1024 - 1, name: 255 };
|
|
71
|
+
throw new RuntimeHomeError({
|
|
72
|
+
code: "HOME_UNSUPPORTED",
|
|
73
|
+
resource: "home",
|
|
74
|
+
operation: "path-budget"
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** 检查实际附加文件名的预算;Windows 按 UTF-16 code unit,POSIX 按 UTF-8 字节。 */
|
|
78
|
+
export function assertRuntimePath(path, resource, platform = process.platform, limits = platformLimits(platform)) {
|
|
79
|
+
const unit = platform === "win32" ? "utf16-units" : "utf8-bytes";
|
|
80
|
+
const measure = (value) => Buffer.byteLength(value, platform === "win32" ? "utf16le" : "utf8") /
|
|
81
|
+
(platform === "win32" ? 2 : 1);
|
|
82
|
+
const fail = (measured, limit, operation) => {
|
|
83
|
+
throw new RuntimeHomeError({
|
|
84
|
+
code: "HOME_PATH_TOO_LONG",
|
|
85
|
+
path,
|
|
86
|
+
resource,
|
|
87
|
+
operation,
|
|
88
|
+
measured,
|
|
89
|
+
limit,
|
|
90
|
+
unit
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
if (measure(path) > limits.path)
|
|
94
|
+
fail(measure(path), limits.path, "path-budget");
|
|
95
|
+
const api = platform === "win32" ? win32 : posix;
|
|
96
|
+
for (const part of path.slice(api.parse(path).root.length).split(api.sep)) {
|
|
97
|
+
if (measure(part) > limits.name)
|
|
98
|
+
fail(measure(part), limits.name, "filename-budget");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** endpoint-only API 只拥有传输,不推断或创建 Home;同样在连接/监听前拒绝无效或超长地址。 */
|
|
102
|
+
export function assertIpcEndpoint(endpoint, platform = process.platform) {
|
|
103
|
+
const fail = (code, extra = {}) => {
|
|
104
|
+
throw new RuntimeHomeError({
|
|
105
|
+
code,
|
|
106
|
+
...(typeof endpoint === "string" ? { path: endpoint, endpoint } : {}),
|
|
107
|
+
resource: "ipc-endpoint",
|
|
108
|
+
operation: "ipc-address",
|
|
109
|
+
...extra
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
if (typeof endpoint !== "string" ||
|
|
113
|
+
!endpoint ||
|
|
114
|
+
endpoint.includes("\0") ||
|
|
115
|
+
/[\uD800-\uDFFF]/u.test(endpoint))
|
|
116
|
+
fail("IPC_ENDPOINT_INVALID");
|
|
117
|
+
if (platform === "win32") {
|
|
118
|
+
if (!/^\\\\[.?]\\pipe\\[^\\]+$/i.test(endpoint))
|
|
119
|
+
fail("IPC_ENDPOINT_INVALID");
|
|
120
|
+
const measured = Buffer.byteLength(endpoint, "utf16le") / 2;
|
|
121
|
+
// CreateNamedPipeW: 完整 pipe 名最多 256 WCHAR;namespace 不是文件路径,不套用 Unix sun_path。
|
|
122
|
+
if (measured > 256)
|
|
123
|
+
fail("IPC_ENDPOINT_TOO_LONG", { measured, limit: 256, unit: "utf16-units" });
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
if (!posix.isAbsolute(endpoint))
|
|
127
|
+
fail("IPC_ENDPOINT_INVALID");
|
|
128
|
+
// Node net + libuv 的 pathname socket 必须留出结尾 NUL:Linux 108、Darwin 104 字节 sun_path。
|
|
129
|
+
const limit = platform === "linux" ? 107 : platform === "darwin" ? 103 : undefined;
|
|
130
|
+
if (limit === undefined)
|
|
131
|
+
fail("IPC_ENDPOINT_UNSUPPORTED");
|
|
132
|
+
const measured = Buffer.byteLength(endpoint, "utf8");
|
|
133
|
+
if (measured > limit)
|
|
134
|
+
fail("IPC_ENDPOINT_TOO_LONG", { measured, limit, unit: "utf8-bytes" });
|
|
135
|
+
assertRuntimePath(endpoint, "ipc-endpoint", platform);
|
|
136
|
+
assertRuntimePath(`${endpoint}.owner.recovery`, "endpoint-ownership", platform);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** client 与服务端共用的唯一启动文案目录;不含 Run 控制码,不触发修复或网络请求。 */
|
|
2
|
+
export interface StartupCatalogEntry {
|
|
3
|
+
readonly summary: string;
|
|
4
|
+
readonly action: string;
|
|
5
|
+
readonly retryable: boolean;
|
|
6
|
+
readonly configKey?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const STARTUP_CATALOG: Readonly<Record<string, StartupCatalogEntry>>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const entry = (summary, action, retryable = false, configKey) => Object.freeze({ summary, action, retryable, ...(configKey ? { configKey } : {}) });
|
|
2
|
+
const homeAction = "The SDK host must select and prepare an existing accessible absolute Runtime Home, then retry. CLI users can select their intended Home with --home. Existing data is not moved automatically.";
|
|
3
|
+
const endpointAction = "Inspect the endpoint owner and component status. Stop the owning service through its host before retrying; do not remove an active endpoint or ownership record.";
|
|
4
|
+
const inspectAction = "Inspect the component configuration and controlled lifecycle logs in the host. Capture the diagnostic codes and safe resource context before retrying; the cause is not yet known.";
|
|
5
|
+
export const STARTUP_CATALOG = Object.freeze({
|
|
6
|
+
STARTUP_UNKNOWN: entry("The startup cause is unknown.", inspectAction),
|
|
7
|
+
LOG_DIRECTORY_MUST_BE_ABSOLUTE: entry("The logging directory must be absolute.", "Pass an absolute directory in logging.directory, then retry startup through the host.", false, "logging.directory"),
|
|
8
|
+
LOG_STATUS_OUTPUT_INVALID: entry("The logging status output callback is invalid.", "Provide a function in logging.statusOutput or omit that optional callback, then retry construction.", false, "logging.statusOutput"),
|
|
9
|
+
SERVICE_INITIALIZATION_FAILED: entry("Component initialization failed.", inspectAction),
|
|
10
|
+
KERNEL_START_CLEANUP_INCOMPLETE: entry("Kernel startup rollback is incomplete.", endpointAction),
|
|
11
|
+
KERNEL_CLEANUP_INCOMPLETE: entry("Kernel resources have not all been released.", endpointAction),
|
|
12
|
+
HOME_UNAVAILABLE: entry("Runtime Home is unavailable.", homeAction),
|
|
13
|
+
HOME_INVALID: entry("Runtime Home must be a valid absolute directory path.", homeAction),
|
|
14
|
+
HOME_UNSUPPORTED: entry("This Runtime Home path or platform is unsupported.", homeAction),
|
|
15
|
+
HOME_NOT_FOUND: entry("The required Runtime Home resource does not exist.", homeAction),
|
|
16
|
+
HOME_NOT_DIRECTORY: entry("The required directory has the wrong resource type.", homeAction),
|
|
17
|
+
HOME_PERMISSION_DENIED: entry("Access to the Runtime Home resource was denied.", "Check permissions for the reported resource and operation under the host account, then retry. Do not reset identities or replace Home to bypass the error."),
|
|
18
|
+
HOME_PATH_TOO_LONG: entry("The Runtime resource exceeds a path limit.", "The host must select and prepare a shorter supported path within the reported limit, then retry. Existing data is not moved automatically."),
|
|
19
|
+
HOME_LIMIT_UNAVAILABLE: entry("Filesystem path limits could not be determined.", "Make the operating system getconf utility available to query PATH_MAX/NAME_MAX, then retry with the same prepared Home."),
|
|
20
|
+
HOME_UNUSABLE: entry("The Runtime Home resource could not be used.", inspectAction),
|
|
21
|
+
IPC_ENDPOINT_INVALID: entry("The IPC endpoint is invalid.", "Pass a valid local IPC endpoint through the host's endpoint option, then retry."),
|
|
22
|
+
IPC_ENDPOINT_UNSUPPORTED: entry("The IPC transport platform is unsupported.", "Use a supported platform and local IPC transport."),
|
|
23
|
+
IPC_ENDPOINT_TOO_LONG: entry("The IPC endpoint exceeds the transport limit.", "Select a shorter Home or endpoint within the reported transport limit, then retry. Existing data is not moved automatically."),
|
|
24
|
+
ENDPOINT_IN_USE: entry("The IPC endpoint already has an owner.", endpointAction),
|
|
25
|
+
EADDRINUSE: entry("The IPC endpoint is already in use.", endpointAction),
|
|
26
|
+
ENDPOINT_PATH_INVALID: entry("The endpoint path contains a different resource type.", "Inspect the reported endpoint and correct the host's endpoint selection. Do not delete an unrelated file."),
|
|
27
|
+
ENDPOINT_OWNER_INVALID: entry("The endpoint ownership record is invalid.", endpointAction),
|
|
28
|
+
ENDPOINT_UNVERIFIABLE: entry("Endpoint ownership could not be verified.", endpointAction),
|
|
29
|
+
ENDPOINT_RECOVERY_IN_PROGRESS: entry("Endpoint recovery is in progress.", "Wait for the owning recovery operation to finish, inspect status, then retry.", true),
|
|
30
|
+
ENOENT: entry("A required startup resource was not found.", "Correct the reported resource selection or prepare the required dependency, then retry."),
|
|
31
|
+
ENOTDIR: entry("A required directory has the wrong resource type.", "Correct the reported directory path to an existing directory, then retry."),
|
|
32
|
+
EISDIR: entry("A required file is a directory.", "Correct the selected file path to the intended regular file, then retry."),
|
|
33
|
+
IM_CONTEXT_LOADING_CONFIG_INVALID: entry("The Extra context-loading configuration is invalid.", "Set context_loading.timeout_ms to an integer from 1 to 30000 and max_in_flight to an integer from 1 to 32; remove unsupported keys, then retry Extra initialization.", false, "context_loading"),
|
|
34
|
+
EACCES: entry("The startup operation was denied access.", "Check the host account's permissions for the reported resource and operation, then retry."),
|
|
35
|
+
EPERM: entry("The operating system denied the startup operation.", "Check the host account's permissions and the reported resource, then retry."),
|
|
36
|
+
EROFS: entry("The startup operation requires a writable filesystem.", "Use an accessible writable resource for the reported operation, then retry."),
|
|
37
|
+
ENAMETOOLONG: entry("The startup resource name exceeds a system limit.", "Select a shorter supported path or endpoint, then retry."),
|
|
38
|
+
ECONNREFUSED: entry("The component is not accepting IPC connections.", "Inspect the target component's lifecycle and logs, start it through its host, then retry.", true),
|
|
39
|
+
SETUP_IN_PROGRESS: entry("A component lifecycle operation is in progress.", "Wait for that operation to finish, then retry.", true),
|
|
40
|
+
SERVICE_INITIALIZATION_CANCELLED: entry("Component initialization was cancelled.", "Check the host cancellation request and cleanup outcome before starting again.", true),
|
|
41
|
+
KERNEL_CONFIGURATION_INVALID: entry("Kernel construction inputs are invalid.", "Pass endpoint and an adapters array to createKernelService. Compose Adapter objects in the host; retired worker/manifest/env options are unsupported."),
|
|
42
|
+
ERR_MODULE_NOT_FOUND: entry("A selected module or one of its dependencies was not found.", "Check the selected module and its installed dependency graph in the host; correct the declaration or install the required package, then retry."),
|
|
43
|
+
MODULE_NOT_FOUND: entry("A selected module or one of its dependencies was not found.", "Check the selected module and its installed dependency graph in the host; correct the declaration or install the required package, then retry."),
|
|
44
|
+
EXTRA_MODULE_REQUIRED: entry("The Extra module selection is missing.", "Supply the module field in the host's Extra declaration before loading it.", false, "module"),
|
|
45
|
+
EXTRA_CONFIG_OBJECT_REQUIRED: entry("The Extra configuration must be an object.", "Provide a JSON object in the Extra config field, then retry initialization.", false, "config"),
|
|
46
|
+
EXTRA_CONFIG_JSON_REQUIRED: entry("The Extra configuration contains a non-JSON value.", "Pass only finite JSON values in config; supply functions separately through dependencies.", false, "config"),
|
|
47
|
+
EXTRA_CONFIG_PERMISSIONS_INVALID: entry("The Extra configuration file failed its file or access checks.", "Inspect the reported configuration file's type, identity and permissions under the host account; use a protected regular file, then retry."),
|
|
48
|
+
MANIFEST_INVALID: entry("The CLI module manifest is invalid.", "Check the selected manifest.json: version must be 1 and adapters/extras must be valid arrays without duplicate keys."),
|
|
49
|
+
EXTRA_NOT_REGISTERED: entry("The Extra has no saved CLI declaration.", "Save this component's declaration with its configure or extraset command, then start it."),
|
|
50
|
+
EXTRA_MODULE_LOAD_FAILED: entry("The selected Extra module could not be loaded.", "Check the selected module, package installation and ESM exports; correct the declaration or reinstall the dependency, then retry."),
|
|
51
|
+
EXTRA_INITIALIZER_REQUIRED: entry("The Extra did not return an initializer.", "Provide an Extra factory that synchronously returns its initialization function."),
|
|
52
|
+
EXTRA_NAME_MISMATCH: entry("The Extra module name differs from its declaration.", "Make the selected module's exported name match its catalog entry."),
|
|
53
|
+
EXTRA_ALREADY_LOADED: entry("This Extra already has an active owner.", "Close the existing Extra through its host before constructing another instance."),
|
|
54
|
+
EXTRA_DISPOSE_FAILED: entry("The previous Extra still has unconfirmed resources.", "Inspect the previous cleanup result and pending resources. Complete the owning host's cleanup before retrying."),
|
|
55
|
+
LOCAL_STATE_INVALID: entry("The local state could not be read.", "Inspect the reported file and system cause. Preserve existing identities and configuration; correct the file or access through the host."),
|
|
56
|
+
WRITE_FAILED: entry("A local state update failed.", "Inspect the reported path, operation and system cause; correct access before retrying the host operation."),
|
|
57
|
+
COMPONENT_UNAVAILABLE: entry("The component did not become ready.", "Inspect component status and controlled lifecycle logs. Process liveness alone does not establish Kernel execution readiness or Extra business connectivity.", true),
|
|
58
|
+
SERVICE_PROCESS_EXITED: entry("The service process exited unexpectedly.", "Inspect this startup attempt's controlled diagnostic and lifecycle logs before restarting."),
|
|
59
|
+
SERVICE_NOT_INSTALLED: entry("The component service is not installed.", "Run this component's install command, then start it."),
|
|
60
|
+
NOT_INSTALLED: entry("The component service is not installed.", "Run this component's install command, then start it."),
|
|
61
|
+
SERVICE_DISABLED: entry("The component service is disabled.", "Run this component's install --autostart=true command, then start it."),
|
|
62
|
+
MANAGER_UNAVAILABLE: entry("The native service manager is unavailable.", "Run doctor and inspect the current user's native service manager."),
|
|
63
|
+
ORPHAN_CLEANUP_INCOMPLETE: entry("Previous Kernel resource cleanup is incomplete.", "CLI users: run doctor with the same --home. After confirming all owned orphan tools have stopped, run executor-kernel recover --acknowledge-orphan-cleanup with that Home, then start again. SDK hosts must use the corresponding inspect/acknowledge API; do not remove ownership records."),
|
|
64
|
+
ORPHAN_RECORD_UNSUPPORTED: entry("The stored Kernel ownership format is unsupported.", "Preserve the ownership evidence. Stop and clean up using the version and host that created it before upgrading; do not acknowledge or delete an unknown record.")
|
|
65
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export { STARTUP_CATALOG, type StartupCatalogEntry } from "./startup-catalog.js";
|
|
2
|
+
/** 诊断仅保存生命周期事实;服务实现复用这些数据类型,不在客户端启动或清理服务。 */
|
|
3
|
+
export type ServicePhase = "created" | "initializing" | "ready" | "closing" | "closed" | "failed";
|
|
4
|
+
export interface ServiceCleanupResult {
|
|
5
|
+
readonly complete: boolean;
|
|
6
|
+
readonly errors: readonly {
|
|
7
|
+
resource: string;
|
|
8
|
+
code: string;
|
|
9
|
+
}[];
|
|
10
|
+
readonly pendingResources: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
/** 只接受启动调用方已确认的资源事实;不接受原始配置、环境、stderr 或异常正文。 */
|
|
13
|
+
export interface StartupContext {
|
|
14
|
+
path?: string;
|
|
15
|
+
resource?: string;
|
|
16
|
+
operation?: string;
|
|
17
|
+
configKey?: string;
|
|
18
|
+
endpoint?: string;
|
|
19
|
+
executable?: string;
|
|
20
|
+
module?: string;
|
|
21
|
+
systemCode?: string;
|
|
22
|
+
measured?: number;
|
|
23
|
+
limit?: number;
|
|
24
|
+
unit?: "utf8-bytes" | "utf16-units";
|
|
25
|
+
}
|
|
26
|
+
/** #306 路径诊断兼容投影;所有字段均从共享诊断白名单生成。 */
|
|
27
|
+
export interface StartupPathDiagnostic extends Pick<StartupContext, "path" | "systemCode" | "endpoint" | "measured" | "limit" | "unit"> {
|
|
28
|
+
domain: "startup";
|
|
29
|
+
code: string;
|
|
30
|
+
resource: string;
|
|
31
|
+
operation: string;
|
|
32
|
+
action: string;
|
|
33
|
+
}
|
|
34
|
+
/** CLI 继续提供 #306 的 startup 字段;恢复建议始终来自同一 catalog。 */
|
|
35
|
+
export declare function startupPathDiagnostic(diagnostic: StartupDiagnostic): StartupPathDiagnostic | undefined;
|
|
36
|
+
export interface StartupCause {
|
|
37
|
+
code: string;
|
|
38
|
+
context: Readonly<StartupContext>;
|
|
39
|
+
component?: string;
|
|
40
|
+
phase?: ServicePhase;
|
|
41
|
+
cleanup?: ServiceCleanupResult;
|
|
42
|
+
}
|
|
43
|
+
export interface StartupDiagnostic {
|
|
44
|
+
domain: "startup";
|
|
45
|
+
code: string;
|
|
46
|
+
rootCode: string;
|
|
47
|
+
component: string;
|
|
48
|
+
phase: ServicePhase;
|
|
49
|
+
summary: string;
|
|
50
|
+
explanation: string;
|
|
51
|
+
actions: readonly string[];
|
|
52
|
+
retryable: boolean;
|
|
53
|
+
userActionRequired: boolean;
|
|
54
|
+
context: Readonly<StartupContext>;
|
|
55
|
+
causes: readonly StartupCause[];
|
|
56
|
+
cleanup?: ServiceCleanupResult;
|
|
57
|
+
truncated: boolean;
|
|
58
|
+
}
|
|
59
|
+
export interface StartupLocation {
|
|
60
|
+
component: string;
|
|
61
|
+
phase: ServicePhase;
|
|
62
|
+
context?: StartupContext;
|
|
63
|
+
}
|
|
64
|
+
/** 保持原类型/message/code/cause,仅在可扩展错误上增加安全诊断;冻结错误可由调用边界直接投影。 */
|
|
65
|
+
export declare function withStartupContext<T>(error: T, location: StartupLocation, classification?: string): T;
|
|
66
|
+
/** 文本与 JSON 同用确定性规范化:去控制字符,去 URL 认证/query/hash 和显式敏感赋值。 */
|
|
67
|
+
export declare function safeStartupValue(value: string, maximum?: number): string;
|
|
68
|
+
/**
|
|
69
|
+
* 在失败调用边界投影启动事实;不依赖 IPC/Run,不改变原错误、退出码或清理行为。
|
|
70
|
+
* cause/aggregate 按主失败优先有界遍历(16 节点/6 层);循环和原始异常正文均不输出。
|
|
71
|
+
*/
|
|
72
|
+
export declare function describeStartupError(error: unknown, location?: StartupLocation): StartupDiagnostic;
|
|
73
|
+
/** 可读投影只消费已净化的共享结构;SDK 可直接按 rootCode/context 分支。 */
|
|
74
|
+
export declare function formatStartupDiagnostic(diagnostic: StartupDiagnostic): string;
|
|
75
|
+
/** 持久化/JSON 是不可信边界:仅重建码、位置、资源和清理,文案始终重新来自当前 catalog。 */
|
|
76
|
+
export declare function restoreStartupDiagnostic(value: unknown): StartupDiagnostic | undefined;
|
|
77
|
+
/** 仅供启动边界补充已确认的位置事实;cause 保留给本地深度排障,公开输出用 diagnostic。 */
|
|
78
|
+
export declare class StartupError extends Error {
|
|
79
|
+
readonly code: string;
|
|
80
|
+
readonly diagnostic: StartupDiagnostic;
|
|
81
|
+
constructor(code: string, location: StartupLocation, cause?: unknown);
|
|
82
|
+
}
|