@deepseek-ai/dsh-app-boot 0.1.7-alpha.1 → 0.1.7-alpha.2

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/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/boot/app-boot/README.md
5
- README.md: 5bbe75d641eeeaf3b87c31221b3491ad1408934e
6
- README.zh.md: b3ac89c1cb082880622175b494d540888b19e804
5
+ README.md: 70a8e235ac5846061ea47e79e7dc575881110a50
6
+ README.zh.md: 9849b033e01827a4c50a22b72ea20deebb0fdd85
package/README.md CHANGED
@@ -40,7 +40,7 @@ installFailLoud('dsh')
40
40
  const ctx = await boot('dsh', resolveConfigPath(argv[2], process.env.DSH_SNAPSHOT))
41
41
  ```
42
42
 
43
- With that entry point, startup keeps every plugin that can activate. An enabled failed plugin produces a labelled warning. A failed required entry makes startup dispose the whole app and exit nonzero; required ids absent from a profile and disabled required entries do not affect startup. The global required list covers shared Agent execution, application endpoints, and Web bootstrap/transport: `agent-loop`, `webserver`, `modules`, `connection`, `headless-runner`, `acp`, and `sdk-jsonrpc-server`.
43
+ `installFailLoud` writes one labelled `util.inspect` diagnostic to stderr for an unhandled rejection or an uncaught exception, awaits the surface's release hook under a fixed timeout, and exits 1; control never returns to the failed operation, because only the throw site knows which state is intact, and the event loop runs only until the release settles or times out. With that entry point, startup keeps every plugin that can activate. An enabled failed plugin produces a labelled warning. A failed required entry makes startup dispose the whole app and exit nonzero; required ids absent from a profile and disabled required entries do not affect startup. The global required list covers shared Agent execution, application endpoints, and Web bootstrap/transport: `agent-loop`, `webserver`, `modules`, `connection`, `headless-runner`, `acp`, and `sdk-jsonrpc-server`.
44
44
 
45
45
  <a id="profiles"></a>
46
46
  ### Profiles
@@ -97,6 +97,7 @@ After the Loader settles, app-boot warns when only optional entries are inactive
97
97
  | An injected service is unavailable | Warn; continue while the entry waits for its dependencies | Stop startup | Keep the entry waiting; adding the missing provider can activate it |
98
98
  | HTTP port binding fails | Warn; continue without that endpoint | Stop startup | Keep the process running without the failed endpoint; corrected config can restore it |
99
99
  | Detached asynchronous work outside the `apply()` return Promise produces an unhandled rejection | Fatal: dispose the app and exit nonzero | Fatal: dispose the app and exit nonzero | Fatal: dispose the app and exit nonzero, regardless of entry id |
100
+ | A synchronous callback (a stream `'data'` listener, a timer) throws an uncaught exception at any point in the process lifetime | Fatal: dispose the app and exit nonzero; the failed operation is not resumed | Fatal: dispose the app and exit nonzero; the failed operation is not resumed | Fatal: dispose the app and exit nonzero, regardless of entry id |
100
101
  | Entry is absent or explicitly disabled | Ignore it | Ignore it | Do not activate it; no required-startup audit |
101
102
 
102
103
  The required list above includes `modules` and `connection`; Web startup cannot succeed when either enabled entry fails. Failure of an optional provider can also prevent a required consumer from activating. Schema rejection before an existing entry updates is not a transactional rollback of sibling changes.
package/README.zh.md CHANGED
@@ -40,7 +40,7 @@ installFailLoud('dsh')
40
40
  const ctx = await boot('dsh', resolveConfigPath(argv[2], process.env.DSH_SNAPSHOT))
41
41
  ```
42
42
 
43
- 有了这个入口,启动会保留所有能够激活的插件。启用但失败的插件会产生带标签的警告。required entry 失败时,启动会拆卸整个应用并以非零码退出;profile 中不存在的 required id 和已禁用的 required entry 不影响启动。全局 required list 覆盖共享 Agent 执行、应用 endpoint,以及 Web 启动与传输:`agent-loop`、`webserver`、`modules`、`connection`、`headless-runner`、`acp` 和 `sdk-jsonrpc-server`。
43
+ `installFailLoud` 会为未处理 rejection 或未捕获异常向 stderr 写一条带标签的 `util.inspect` 诊断,在固定超时内等待界面的 release 钩子,然后以 1 退出;控制流不会回到失败的操作,因为只有抛出点知道哪些状态仍然完整,事件循环只运行到 release 结束或超时。有了这个入口,启动会保留所有能够激活的插件。启用但失败的插件会产生带标签的警告。required entry 失败时,启动会拆卸整个应用并以非零码退出;profile 中不存在的 required id 和已禁用的 required entry 不影响启动。全局 required list 覆盖共享 Agent 执行、应用 endpoint,以及 Web 启动与传输:`agent-loop`、`webserver`、`modules`、`connection`、`headless-runner`、`acp` 和 `sdk-jsonrpc-server`。
44
44
 
45
45
  <a id="profiles"></a>
46
46
  ### Profile
@@ -97,6 +97,7 @@ Loader 结算后,app-boot 在仅 optional 条目未激活时输出警告。如
97
97
  | 注入的服务不可用 | 警告;继续,条目等待依赖 | 终止启动 | 条目继续等待;补上缺失的提供方后可以激活 |
98
98
  | HTTP 端口绑定失败 | 警告;继续,但该端点不可用 | 终止启动 | 进程继续运行,但失败的端点不可用;修正配置后可以恢复 |
99
99
  | 脱离 `apply()` 返回 Promise 的异步任务产生未处理 rejection | 致命错误:释放应用并以非零码退出 | 致命错误:释放应用并以非零码退出 | 致命错误:释放应用并以非零码退出,与条目 id 无关 |
100
+ | 同步回调(流的 `'data'` 监听器、定时器)在进程生命周期任一时刻抛出未捕获异常 | 致命错误:释放应用并以非零码退出;失败的操作不会被恢复 | 致命错误:释放应用并以非零码退出;失败的操作不会被恢复 | 致命错误:释放应用并以非零码退出,与条目 id 无关 |
100
101
  | 条目缺失或被显式禁用 | 忽略 | 忽略 | 不激活该条目;不执行 required 启动审计 |
101
102
 
102
103
  上面的 required 列表包含 `modules` 与 `connection`;只要其中一个已启用条目失败,Web 就无法成功启动。Optional 提供方失败也可能使 required 消费方无法激活。现有条目的新配置在更新前被 schema 校验拒绝,并不等于对兄弟插件的变更做事务回滚。
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createRequire, isBuiltin } from "node:module";
2
2
  import { fileURLToPath, pathToFileURL } from "node:url";
3
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
4
- import { parseEnv } from "node:util";
4
+ import { inspect, parseEnv } from "node:util";
5
5
  import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep, win32 } from "node:path";
6
6
  import * as yaml from "js-yaml";
7
7
  import { Context, Service } from "@deepseek-ai/cordis";
@@ -3363,11 +3363,22 @@ async function observeLoaderRejectionCheckpoint(reasons) {
3363
3363
  */
3364
3364
  const FAIL_LOUD_RELEASE_TIMEOUT_MS = 2e3;
3365
3365
  /**
3366
- * Install before boot to turn a late unhandled plugin-init rejection into one
3367
- * labelled stderr diagnostic and `exit(1)`. A rejection already included by
3368
- * {@link auditStartupEntries} is ignored during its process checkpoint;
3369
- * every other rejection remains fatal. Stdout remains untouched for ACP; the
3370
- * returned function removes the handler.
3366
+ * Install before boot to turn an unhandled rejection or an uncaught exception,
3367
+ * at any point in the process lifetime, into one labelled stderr diagnostic and
3368
+ * `exit(1)`. A rejection already included by {@link auditStartupEntries} is
3369
+ * ignored during its process checkpoint; every other rejection and every
3370
+ * uncaught exception remains fatal. Control never returns to the failed
3371
+ * operation after either: only the throw site knows which state is intact, and
3372
+ * a listener that threw mid-update (a stream `'data'` handler, a half-applied
3373
+ * registry write) leaves silently wrong results behind if it were resumed. The
3374
+ * event loop keeps running only until the release hook settles or times out.
3375
+ * Stdout remains untouched for ACP; the returned function removes both handlers.
3376
+ *
3377
+ * The diagnostic is `util.inspect(err)`, not `err.stack`: a `node:fs` error's
3378
+ * `code`, `syscall`, and `path` and any `cause` chain are enumerable properties
3379
+ * that the stack line omits, and they are what a crash report needs. Once a
3380
+ * handler is installed Node prints nothing of its own, so this line is the
3381
+ * only record of the failure.
3371
3382
  *
3372
3383
  * The Loader mounts entries concurrently, so a surface that owns the terminal
3373
3384
  * can already hold it when a sibling entry rejects. Exiting straight from the
@@ -3389,15 +3400,17 @@ const FAIL_LOUD_RELEASE_TIMEOUT_MS = 2e3;
3389
3400
  * @param release - optional teardown awaited before exit, used by a
3390
3401
  * terminal-owning surface to restore the terminal. Its own failure is
3391
3402
  * swallowed because the pending fatal exit already owns the outcome.
3392
- * @returns the uninstaller that removes the rejection handler.
3403
+ * @returns the uninstaller that removes both handlers.
3393
3404
  */
3394
3405
  function installFailLoud(binName, proc = process, release) {
3395
3406
  let exiting = false;
3396
- const handler = (err) => {
3397
- if (assembledActivationRejections.has(err)) return;
3407
+ const report = (err, label) => {
3398
3408
  if (exiting) return;
3399
3409
  exiting = true;
3400
- proc.stderr.write(`${binName}: fatal load failure: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
3410
+ proc.stderr.write(`${binName}: ${label}: ${inspect(err, {
3411
+ depth: 4,
3412
+ maxArrayLength: 50
3413
+ })}\n`);
3401
3414
  if (release === void 0) {
3402
3415
  proc.exit(1);
3403
3416
  return;
@@ -3413,8 +3426,19 @@ function installFailLoud(binName, proc = process, release) {
3413
3426
  proc.exit(1);
3414
3427
  })();
3415
3428
  };
3416
- const uninstall = () => void proc.off("unhandledRejection", handler);
3417
- proc.on("unhandledRejection", handler);
3429
+ const onRejection = (err) => {
3430
+ if (assembledActivationRejections.has(err)) return;
3431
+ report(err, "fatal load failure");
3432
+ };
3433
+ const onException = (err) => {
3434
+ report(err, "fatal uncaught exception");
3435
+ };
3436
+ const uninstall = () => {
3437
+ proc.off("unhandledRejection", onRejection);
3438
+ proc.off("uncaughtException", onException);
3439
+ };
3440
+ proc.on("unhandledRejection", onRejection);
3441
+ proc.on("uncaughtException", onException);
3418
3442
  return uninstall;
3419
3443
  }
3420
3444
  /**
@@ -144,13 +144,15 @@ export declare function renderConfigDump(binName: string, absoluteConfigPath: st
144
144
  * entry creation was in flight.
145
145
  */
146
146
  export declare function mountRootInclude(ctx: Context, absoluteConfigPath: string, patches?: readonly PatchOptions[], bareModuleBaseUrl?: string): Promise<Entry | undefined>;
147
+ /** The two process events {@link installFailLoud} turns into a fatal exit. */
148
+ export type FailLoudEvent = 'unhandledRejection' | 'uncaughtException';
147
149
  /**
148
150
  * The slice of `process` {@link installFailLoud} needs — injectable so tests
149
151
  * exercise the handler without registering on (or exiting) the real process.
150
152
  */
151
153
  export interface FailLoudProcess {
152
- on(event: 'unhandledRejection', handler: (err: unknown) => void): unknown;
153
- off(event: 'unhandledRejection', handler: (err: unknown) => void): unknown;
154
+ on(event: FailLoudEvent, handler: (err: unknown) => void): unknown;
155
+ off(event: FailLoudEvent, handler: (err: unknown) => void): unknown;
154
156
  stderr: {
155
157
  write(chunk: string): unknown;
156
158
  };
@@ -167,11 +169,22 @@ export interface FailLoudProcess {
167
169
  */
168
170
  export declare const FAIL_LOUD_RELEASE_TIMEOUT_MS = 2000;
169
171
  /**
170
- * Install before boot to turn a late unhandled plugin-init rejection into one
171
- * labelled stderr diagnostic and `exit(1)`. A rejection already included by
172
- * {@link auditStartupEntries} is ignored during its process checkpoint;
173
- * every other rejection remains fatal. Stdout remains untouched for ACP; the
174
- * returned function removes the handler.
172
+ * Install before boot to turn an unhandled rejection or an uncaught exception,
173
+ * at any point in the process lifetime, into one labelled stderr diagnostic and
174
+ * `exit(1)`. A rejection already included by {@link auditStartupEntries} is
175
+ * ignored during its process checkpoint; every other rejection and every
176
+ * uncaught exception remains fatal. Control never returns to the failed
177
+ * operation after either: only the throw site knows which state is intact, and
178
+ * a listener that threw mid-update (a stream `'data'` handler, a half-applied
179
+ * registry write) leaves silently wrong results behind if it were resumed. The
180
+ * event loop keeps running only until the release hook settles or times out.
181
+ * Stdout remains untouched for ACP; the returned function removes both handlers.
182
+ *
183
+ * The diagnostic is `util.inspect(err)`, not `err.stack`: a `node:fs` error's
184
+ * `code`, `syscall`, and `path` and any `cause` chain are enumerable properties
185
+ * that the stack line omits, and they are what a crash report needs. Once a
186
+ * handler is installed Node prints nothing of its own, so this line is the
187
+ * only record of the failure.
175
188
  *
176
189
  * The Loader mounts entries concurrently, so a surface that owns the terminal
177
190
  * can already hold it when a sibling entry rejects. Exiting straight from the
@@ -193,7 +206,7 @@ export declare const FAIL_LOUD_RELEASE_TIMEOUT_MS = 2000;
193
206
  * @param release - optional teardown awaited before exit, used by a
194
207
  * terminal-owning surface to restore the terminal. Its own failure is
195
208
  * swallowed because the pending fatal exit already owns the outcome.
196
- * @returns the uninstaller that removes the rejection handler.
209
+ * @returns the uninstaller that removes both handlers.
197
210
  */
198
211
  export declare function installFailLoud(binName: string, proc?: FailLoudProcess, release?: () => Promise<void> | void): () => void;
199
212
  interface InactiveEntry {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-app-boot",
3
3
  "description": "Shared boot glue for the app bins: .env loading, fail-loud Loader guards, snapshot-aware config resolution, and the Loader boot sequence",
4
- "version": "0.1.7-alpha.1",
4
+ "version": "0.1.7-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -37,27 +37,27 @@
37
37
  "js-yaml": "^4.2.0",
38
38
  "node-addon-require-builtin": "^0.1.6",
39
39
  "resolve.exports": "^2.0.3",
40
- "@deepseek-ai/dsh-package-manifest": "^0.1.7-alpha.1"
40
+ "@deepseek-ai/dsh-package-manifest": "0.1.7-alpha.2"
41
41
  },
42
42
  "peerDependencies": {
43
- "@deepseek-ai/cordis-plugin-include": "^1.0.8",
44
- "@deepseek-ai/cordis-plugin-group": "^1.0.3",
45
- "@deepseek-ai/dsh-launch-environment": "^0.1.7-alpha.1",
46
- "@deepseek-ai/dsh-system-prompt": "^0.1.7-alpha.1",
47
- "@deepseek-ai/cordis": "^4.0.3",
48
- "@deepseek-ai/dsh-home-paths": "^0.1.7-alpha.1",
49
- "@deepseek-ai/cordis-plugin-loader": "^1.0.4"
43
+ "@deepseek-ai/cordis-plugin-group": "~1.0.4",
44
+ "@deepseek-ai/cordis-plugin-include": "~1.0.9",
45
+ "@deepseek-ai/cordis-plugin-loader": "~1.0.5",
46
+ "@deepseek-ai/dsh-launch-environment": "0.1.7-alpha.2",
47
+ "@deepseek-ai/dsh-home-paths": "0.1.7-alpha.2",
48
+ "@deepseek-ai/dsh-system-prompt": "0.1.7-alpha.2",
49
+ "@deepseek-ai/cordis": "~4.0.4"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/js-yaml": "^4.0.9",
53
- "@deepseek-ai/schemastery": "^3.18.3",
54
- "@deepseek-ai/cordis-plugin-group": "^1.0.3",
55
- "@deepseek-ai/cordis-plugin-include": "^1.0.8",
56
- "@deepseek-ai/cordis-plugin-loader": "^1.0.4",
57
- "@deepseek-ai/dsh-launch-environment": "^0.1.7-alpha.1",
58
- "@deepseek-ai/cordis-plugin-timer": "^1.1.5",
59
- "@deepseek-ai/dsh-home-paths": "^0.1.7-alpha.1",
60
- "@deepseek-ai/dsh-system-prompt": "^0.1.7-alpha.1",
61
- "@deepseek-ai/cordis": "^4.0.3"
53
+ "@deepseek-ai/schemastery": "~3.18.4",
54
+ "@deepseek-ai/cordis-plugin-include": "~1.0.9",
55
+ "@deepseek-ai/cordis-plugin-group": "~1.0.4",
56
+ "@deepseek-ai/cordis-plugin-timer": "~1.1.6",
57
+ "@deepseek-ai/cordis-plugin-loader": "~1.0.5",
58
+ "@deepseek-ai/dsh-home-paths": "0.1.7-alpha.2",
59
+ "@deepseek-ai/dsh-launch-environment": "0.1.7-alpha.2",
60
+ "@deepseek-ai/dsh-system-prompt": "0.1.7-alpha.2",
61
+ "@deepseek-ai/cordis": "~4.0.4"
62
62
  }
63
63
  }