@mininglamp-oss/octo-runtime 1.1.0 → 1.1.1

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 (127) hide show
  1. package/README.md +4 -2
  2. package/apps/cli/README.md +3 -1
  3. package/apps/cli/dist/build-info.json +2 -2
  4. package/apps/cli/dist/extras.d.ts +4 -2
  5. package/apps/cli/dist/extras.js +20 -21
  6. package/apps/cli/dist/help.js +3 -0
  7. package/apps/cli/dist/logs.d.ts +10 -0
  8. package/apps/cli/dist/logs.js +186 -0
  9. package/apps/cli/dist/main.d.ts +1 -0
  10. package/apps/cli/dist/main.js +94 -19
  11. package/apps/cli/dist/{default_adapters.json → manifest.json} +5 -0
  12. package/apps/cli/dist/module-manifest.d.ts +3 -0
  13. package/apps/cli/dist/module-manifest.js +11 -0
  14. package/apps/cli/dist/openclaw.js +30 -6
  15. package/apps/cli/dist/output.js +3 -0
  16. package/apps/cli/dist/playboard.js +10 -8
  17. package/apps/cli/dist/status-output.d.ts +10 -0
  18. package/apps/cli/dist/status-output.js +65 -0
  19. package/apps/cli/dist/worker.d.ts +1 -0
  20. package/apps/cli/dist/worker.js +18 -0
  21. package/dist/public/host.d.ts +1 -0
  22. package/node_modules/@mininglamp-oss/octo-runtime-acp-client/package.json +3 -3
  23. package/node_modules/@mininglamp-oss/octo-runtime-adapter-acp/package.json +4 -4
  24. package/node_modules/@mininglamp-oss/octo-runtime-adapter-antigravity/package.json +3 -3
  25. package/node_modules/@mininglamp-oss/octo-runtime-adapter-claude-code/package.json +3 -3
  26. package/node_modules/@mininglamp-oss/octo-runtime-adapter-codebuddy/package.json +4 -4
  27. package/node_modules/@mininglamp-oss/octo-runtime-adapter-codex/package.json +3 -3
  28. package/node_modules/@mininglamp-oss/octo-runtime-adapter-hermes/package.json +2 -2
  29. package/node_modules/@mininglamp-oss/octo-runtime-adapter-kimi-code/package.json +2 -2
  30. package/node_modules/@mininglamp-oss/octo-runtime-adapter-mcode/package.json +2 -2
  31. package/node_modules/@mininglamp-oss/octo-runtime-adapter-openclaw/package.json +3 -3
  32. package/node_modules/@mininglamp-oss/octo-runtime-adapter-opencode/package.json +4 -4
  33. package/node_modules/@mininglamp-oss/octo-runtime-adapter-qodercli/package.json +2 -2
  34. package/node_modules/@mininglamp-oss/octo-runtime-adapter-qoderclicn/package.json +2 -2
  35. package/node_modules/@mininglamp-oss/octo-runtime-adapter-sdk/dist/index.d.ts +1 -0
  36. package/node_modules/@mininglamp-oss/octo-runtime-adapter-sdk/dist/index.js +1 -0
  37. package/node_modules/@mininglamp-oss/octo-runtime-adapter-sdk/dist/module-manifest.d.ts +33 -0
  38. package/node_modules/@mininglamp-oss/octo-runtime-adapter-sdk/dist/module-manifest.js +118 -0
  39. package/node_modules/@mininglamp-oss/octo-runtime-adapter-sdk/dist/runtime-log-worker.js +7 -5
  40. package/node_modules/@mininglamp-oss/octo-runtime-adapter-sdk/dist/runtime-log.d.ts +3 -0
  41. package/node_modules/@mininglamp-oss/octo-runtime-adapter-sdk/dist/runtime-log.js +45 -10
  42. package/node_modules/@mininglamp-oss/octo-runtime-adapter-sdk/dist/service-status.d.ts +11 -0
  43. package/node_modules/@mininglamp-oss/octo-runtime-adapter-sdk/dist/service-status.js +70 -0
  44. package/node_modules/@mininglamp-oss/octo-runtime-adapter-sdk/package.json +5 -3
  45. package/node_modules/@mininglamp-oss/octo-runtime-adapter-zcode-gui/package.json +3 -3
  46. package/node_modules/@mininglamp-oss/octo-runtime-cli/dist/build-info.json +2 -2
  47. package/node_modules/@mininglamp-oss/octo-runtime-cli/dist/extras.d.ts +4 -2
  48. package/node_modules/@mininglamp-oss/octo-runtime-cli/dist/extras.js +20 -21
  49. package/node_modules/@mininglamp-oss/octo-runtime-cli/dist/help.js +3 -0
  50. package/node_modules/@mininglamp-oss/octo-runtime-cli/dist/logs.d.ts +10 -0
  51. package/node_modules/@mininglamp-oss/octo-runtime-cli/dist/logs.js +186 -0
  52. package/node_modules/@mininglamp-oss/octo-runtime-cli/dist/main.d.ts +1 -0
  53. package/node_modules/@mininglamp-oss/octo-runtime-cli/dist/main.js +94 -19
  54. package/node_modules/@mininglamp-oss/octo-runtime-cli/dist/{default_adapters.json → manifest.json} +5 -0
  55. package/node_modules/@mininglamp-oss/octo-runtime-cli/dist/module-manifest.d.ts +3 -0
  56. package/node_modules/@mininglamp-oss/octo-runtime-cli/dist/module-manifest.js +11 -0
  57. package/node_modules/@mininglamp-oss/octo-runtime-cli/dist/openclaw.js +30 -6
  58. package/node_modules/@mininglamp-oss/octo-runtime-cli/dist/output.js +3 -0
  59. package/node_modules/@mininglamp-oss/octo-runtime-cli/dist/playboard.js +10 -8
  60. package/node_modules/@mininglamp-oss/octo-runtime-cli/dist/status-output.d.ts +10 -0
  61. package/node_modules/@mininglamp-oss/octo-runtime-cli/dist/status-output.js +65 -0
  62. package/node_modules/@mininglamp-oss/octo-runtime-cli/dist/worker.d.ts +1 -0
  63. package/node_modules/@mininglamp-oss/octo-runtime-cli/dist/worker.js +18 -0
  64. package/node_modules/@mininglamp-oss/octo-runtime-cli/package.json +22 -9
  65. package/node_modules/@mininglamp-oss/octo-runtime-cross-service/package.json +2 -2
  66. package/node_modules/@mininglamp-oss/octo-runtime-executor-core/dist/model-input.d.ts +1 -1
  67. package/node_modules/@mininglamp-oss/octo-runtime-executor-core/dist/model-input.js +5 -5
  68. package/node_modules/@mininglamp-oss/octo-runtime-executor-core/package.json +3 -3
  69. package/node_modules/@mininglamp-oss/octo-runtime-executor-kernel/dist/heartbeat.d.ts +16 -0
  70. package/node_modules/@mininglamp-oss/octo-runtime-executor-kernel/dist/heartbeat.js +59 -0
  71. package/node_modules/@mininglamp-oss/octo-runtime-executor-kernel/dist/invocation-manifest.d.ts +1 -0
  72. package/node_modules/@mininglamp-oss/octo-runtime-executor-kernel/dist/invocation-manifest.js +4 -1
  73. package/node_modules/@mininglamp-oss/octo-runtime-executor-kernel/dist/manifest-adapters.d.ts +6 -0
  74. package/node_modules/@mininglamp-oss/octo-runtime-executor-kernel/dist/manifest-adapters.js +88 -0
  75. package/node_modules/@mininglamp-oss/octo-runtime-executor-kernel/dist/module-specifier.d.ts +1 -2
  76. package/node_modules/@mininglamp-oss/octo-runtime-executor-kernel/dist/module-specifier.js +2 -9
  77. package/node_modules/@mininglamp-oss/octo-runtime-executor-kernel/dist/process.d.ts +7 -2
  78. package/node_modules/@mininglamp-oss/octo-runtime-executor-kernel/dist/process.js +138 -9
  79. package/node_modules/@mininglamp-oss/octo-runtime-executor-kernel/dist/runner.js +3 -92
  80. package/node_modules/@mininglamp-oss/octo-runtime-executor-kernel/dist/serve.js +36 -12
  81. package/node_modules/@mininglamp-oss/octo-runtime-executor-kernel/dist/server.d.ts +4 -1
  82. package/node_modules/@mininglamp-oss/octo-runtime-executor-kernel/dist/server.js +14 -6
  83. package/node_modules/@mininglamp-oss/octo-runtime-executor-kernel/dist/service.js +2 -1
  84. package/node_modules/@mininglamp-oss/octo-runtime-executor-kernel/dist/supervisor.d.ts +2 -2
  85. package/node_modules/@mininglamp-oss/octo-runtime-executor-kernel/dist/supervisor.js +57 -34
  86. package/node_modules/@mininglamp-oss/octo-runtime-executor-kernel/package.json +8 -16
  87. package/node_modules/@mininglamp-oss/octo-runtime-extra-channel-gateway/config-schema.json +2 -2
  88. package/node_modules/@mininglamp-oss/octo-runtime-extra-channel-gateway/dist/application/im-channel.js +16 -17
  89. package/node_modules/@mininglamp-oss/octo-runtime-extra-channel-gateway/package.json +6 -6
  90. package/node_modules/@mininglamp-oss/octo-runtime-extra-channel-wecom/config-schema.json +2 -2
  91. package/node_modules/@mininglamp-oss/octo-runtime-extra-channel-wecom/package.json +5 -5
  92. package/node_modules/@mininglamp-oss/octo-runtime-extra-webui/config-schema.json +2 -2
  93. package/node_modules/@mininglamp-oss/octo-runtime-extra-webui/dist/index.js +1 -1
  94. package/node_modules/@mininglamp-oss/octo-runtime-extra-webui/dist/store.d.ts +4 -1
  95. package/node_modules/@mininglamp-oss/octo-runtime-extra-webui/dist/store.js +54 -4
  96. package/node_modules/@mininglamp-oss/octo-runtime-extra-webui/package.json +2 -2
  97. package/node_modules/@mininglamp-oss/octo-runtime-host-sdk/dist/extra-config.d.ts +8 -1
  98. package/node_modules/@mininglamp-oss/octo-runtime-host-sdk/dist/extra-config.js +27 -12
  99. package/node_modules/@mininglamp-oss/octo-runtime-host-sdk/dist/extra-service.js +39 -2
  100. package/node_modules/@mininglamp-oss/octo-runtime-host-sdk/dist/index.d.ts +1 -0
  101. package/node_modules/@mininglamp-oss/octo-runtime-host-sdk/package.json +4 -4
  102. package/node_modules/@mininglamp-oss/octo-runtime-protocol/package.json +1 -1
  103. package/node_modules/import-meta-resolve/index.d.ts +19 -0
  104. package/node_modules/import-meta-resolve/index.d.ts.map +1 -0
  105. package/node_modules/import-meta-resolve/index.js +47 -0
  106. package/node_modules/import-meta-resolve/lib/errors.d.ts +24 -0
  107. package/node_modules/import-meta-resolve/lib/errors.d.ts.map +1 -0
  108. package/node_modules/import-meta-resolve/lib/errors.js +504 -0
  109. package/node_modules/import-meta-resolve/lib/get-format.d.ts +13 -0
  110. package/node_modules/import-meta-resolve/lib/get-format.d.ts.map +1 -0
  111. package/node_modules/import-meta-resolve/lib/get-format.js +159 -0
  112. package/node_modules/import-meta-resolve/lib/package-json-reader.d.ts +31 -0
  113. package/node_modules/import-meta-resolve/lib/package-json-reader.d.ts.map +1 -0
  114. package/node_modules/import-meta-resolve/lib/package-json-reader.js +177 -0
  115. package/node_modules/import-meta-resolve/lib/resolve.d.ts +29 -0
  116. package/node_modules/import-meta-resolve/lib/resolve.d.ts.map +1 -0
  117. package/node_modules/import-meta-resolve/lib/resolve.js +1301 -0
  118. package/node_modules/import-meta-resolve/lib/utils.d.ts +6 -0
  119. package/node_modules/import-meta-resolve/lib/utils.d.ts.map +1 -0
  120. package/node_modules/import-meta-resolve/lib/utils.js +47 -0
  121. package/node_modules/import-meta-resolve/license +74 -0
  122. package/node_modules/import-meta-resolve/package.json +78 -0
  123. package/node_modules/import-meta-resolve/readme.md +252 -0
  124. package/package.json +28 -26
  125. package/packages/octo-runtime-extra-channel-gateway/config-schema.json +2 -2
  126. package/packages/octo-runtime-extra-channel-wecom/config-schema.json +2 -2
  127. package/packages/octo-runtime-extra-webui/config-schema.json +2 -2
@@ -110,6 +110,7 @@ export async function reapOrphanedInvocationManifest(path) {
110
110
  await rm(path, { force: true });
111
111
  return { found: true, reaped: true, pid: host.pid };
112
112
  }
113
+ /** 查询系统启动时间以区分 PID 复用;失败时不返回可用于回收的身份,绝不以 PID 本身兜底。 */
113
114
  export async function processStartIdentity(pid) {
114
115
  if (!Number.isSafeInteger(pid) || pid < 1)
115
116
  return undefined;
@@ -141,7 +142,9 @@ export async function processStartIdentity(pid) {
141
142
  "-NoProfile",
142
143
  "-NonInteractive",
143
144
  "-Command",
144
- `(Get-Process -Id ${pid} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks`
145
+ // 直接调用系统进程对象,避免 Get-Process 的命令发现/模块自动加载占用身份查询预算。
146
+ // 保持 UTC ticks 身份格式;不增加超时,也不缓存可能已经复用的 PID。
147
+ `$PSModuleAutoLoadingPreference='None'; $target=[System.Diagnostics.Process]::GetProcessById(${pid}); try { [Console]::Out.Write($target.StartTime.ToUniversalTime().Ticks.ToString([Globalization.CultureInfo]::InvariantCulture)) } finally { $target.Dispose() }`
145
148
  ], { encoding: "utf8", timeout: 10_000, windowsHide: true }, (error, stdout) => {
146
149
  const value = stdout.trim();
147
150
  resolve(error || !/^\d+$/.test(value) ? undefined : `windows:${value}`);
@@ -0,0 +1,6 @@
1
+ import type { HostedAdapter } from "./index.js";
2
+ /**
3
+ * 仅供清单驱动的 worker 在自身进程装配:只实例化所选模块,返回后由入口注册及 serveWorker。
4
+ * CLI 可在注册前用实际 metadata 做产品准备;SDK 原生 import 入口不经过此文件加载边界。
5
+ */
6
+ export declare function loadManifestAdapters(): Promise<HostedAdapter[]>;
@@ -0,0 +1,88 @@
1
+ import { parseModuleManifest, readModuleManifest, resolveModuleSpecifier } from "@mininglamp-oss/octo-runtime-adapter-sdk/module-manifest";
2
+ /**
3
+ * 仅供清单驱动的 worker 在自身进程装配:只实例化所选模块,返回后由入口注册及 serveWorker。
4
+ * CLI 可在注册前用实际 metadata 做产品准备;SDK 原生 import 入口不经过此文件加载边界。
5
+ */
6
+ export async function loadManifestAdapters() {
7
+ const index = process.argv.indexOf("--manifest");
8
+ const entries = process.argv.includes("--adapters-from-ipc")
9
+ ? await receiveAdapters()
10
+ : index >= 0 && process.argv[index + 1]
11
+ ? (await readModuleManifest(process.argv[index + 1], { legacy: true })).adapters
12
+ : (() => {
13
+ throw new Error("usage: octo-runtime-executor-kernel --manifest <file>");
14
+ })();
15
+ const adapters = [];
16
+ for (const entry of entries) {
17
+ const specifier = resolveModuleSpecifier(entry.module, entry.sourceManifest);
18
+ const loaded = await import(specifier);
19
+ const exports = asRecord(loaded, `adapter module ${entry.module}`);
20
+ const exported = exports[entry.export];
21
+ if (exported === undefined)
22
+ throw new Error(`missing export ${entry.export} from ${entry.module}`);
23
+ // 动态模块先校验工厂与 Adapter 形状,再交给唯一 Core 注册;不提前探测未选模块。
24
+ const instance = entry.factory ? callFactory(exported, entry) : exported;
25
+ if (!isHostedAdapter(instance))
26
+ throw new TypeError(`${entry.module}#${entry.export} is not an Octo Runtime adapter`);
27
+ if (entry.type && instance.metadata.type !== entry.type)
28
+ throw new Error(`manifest type ${entry.type} does not match adapter ${instance.metadata.type}`);
29
+ adapters.push(instance);
30
+ }
31
+ return adapters;
32
+ }
33
+ /** 内部启动消息只携带 JSON 模块声明,不传函数/实例;就绪仍沿用既有 list/probe 握手。 */
34
+ function receiveAdapters() {
35
+ return new Promise((resolve, reject) => {
36
+ const cleanup = () => {
37
+ clearTimeout(timer);
38
+ process.off("message", onMessage);
39
+ process.off("disconnect", onDisconnect);
40
+ };
41
+ const onDisconnect = () => {
42
+ cleanup();
43
+ reject(new Error("Adapter initialization channel disconnected"));
44
+ };
45
+ const onMessage = (message) => {
46
+ if (!isRecord(message) || message.type !== "kernel.adapters")
47
+ return;
48
+ cleanup();
49
+ try {
50
+ // 快照已经解析为绝对位置;这个路径只用于结构校验,不读取或生成任何文件。
51
+ resolve(parseModuleManifest({ version: 1, adapters: message.adapters, extras: [] }, process.cwd(), { legacy: true }).adapters);
52
+ }
53
+ catch (error) {
54
+ reject(error instanceof Error ? error : new Error("Invalid Adapter initialization"));
55
+ }
56
+ };
57
+ const timer = setTimeout(() => {
58
+ cleanup();
59
+ reject(new Error("Adapter initialization timed out"));
60
+ }, 15000);
61
+ process.on("message", onMessage);
62
+ process.once("disconnect", onDisconnect);
63
+ if (!process.connected)
64
+ onDisconnect();
65
+ });
66
+ }
67
+ function callFactory(value, entry) {
68
+ if (typeof value !== "function")
69
+ throw new TypeError(`${entry.module}#${entry.export} is marked as a factory but is not callable`);
70
+ return value(entry.options);
71
+ }
72
+ function isHostedAdapter(value) {
73
+ if (!isRecord(value) || !isRecord(value.metadata))
74
+ return false;
75
+ return (typeof value.metadata.type === "string" &&
76
+ Array.isArray(value.metadata.agentSources) &&
77
+ value.metadata.agentSources.length > 0 &&
78
+ value.metadata.agentSources.every((source) => source === "preset" || source === "provisioned") &&
79
+ typeof value.invoke === "function");
80
+ }
81
+ function asRecord(value, label) {
82
+ if (!isRecord(value))
83
+ throw new TypeError(`${label} must be an object`);
84
+ return value;
85
+ }
86
+ function isRecord(value) {
87
+ return value !== null && typeof value === "object" && !Array.isArray(value);
88
+ }
@@ -1,2 +1 @@
1
- /** 内部加载边界:按本机路径语义识别文件,避免把 Windows 盘符交给 ESM 当作 URL 协议。 */
2
- export declare function resolveAdapterModuleSpecifier(module: string, manifestPath: string): string;
1
+ export { resolveModuleSpecifier as resolveAdapterModuleSpecifier } from "@mininglamp-oss/octo-runtime-adapter-sdk/module-manifest";
@@ -1,9 +1,2 @@
1
- import { dirname, isAbsolute, resolve } from "node:path";
2
- import { pathToFileURL } from "node:url";
3
- /** 内部加载边界:按本机路径语义识别文件,避免把 Windows 盘符交给 ESM 当作 URL 协议。 */
4
- export function resolveAdapterModuleSpecifier(module, manifestPath) {
5
- // 相对路径属于 manifest 所在目录;包名及现有 URL 保持 Node 的解析语义。
6
- return module.startsWith(".") || isAbsolute(module)
7
- ? pathToFileURL(resolve(dirname(manifestPath), module)).href
8
- : module;
9
- }
1
+ // 内部兼容入口,CLI 与 runner 共用原始 manifest 锚点下的 ESM 解析。
2
+ export { resolveModuleSpecifier as resolveAdapterModuleSpecifier } from "@mininglamp-oss/octo-runtime-adapter-sdk/module-manifest";
@@ -2,14 +2,17 @@ import { type ChildProcess, type ChildProcessByStdio } from "node:child_process"
2
2
  import type { Readable, Writable } from "node:stream";
3
3
  import type { HostedAdapterMetadata } from "./index.js";
4
4
  import { WorkerRpcClient } from "./rpc.js";
5
+ import type { AdapterModuleEntry } from "@mininglamp-oss/octo-runtime-adapter-sdk/module-manifest";
5
6
  export interface SpawnWorkerOptions {
6
7
  entrypoint: string;
7
8
  /** CLI JSON 来源;SDK 的直接 import 入口不需要此参数。 */
8
9
  manifest?: string;
10
+ /** CLI 已解析的不可变装配快照;与 manifest 互斥,不改变 SDK 自有 worker 的入口。 */
11
+ adapterModules?: AdapterModuleEntry[];
9
12
  env?: NodeJS.ProcessEnv;
10
13
  /** 取消就绪等待并请求回收子进程;POSIX 使用进程组,Windows taskkill 不保证父先退后的后代回收。 */
11
14
  signal?: AbortSignal;
12
- /** Maximum time allowed for the initial RPC readiness handshake. */
15
+ /** 启动快照发送和首次 RPC 就绪握手共用的最大等待时间。 */
13
16
  readyTimeoutMs?: number;
14
17
  onSpawn?: (child: ChildProcess) => Promise<void>;
15
18
  }
@@ -21,10 +24,12 @@ export interface WorkerProcess {
21
24
  close(): Promise<void>;
22
25
  stderr(): string;
23
26
  }
27
+ /** Kernel 内部传递关闭预算;公开 WorkerProcess.close() 仍然无参数。故障恢复不等待日志排空。 */
28
+ export declare function closeWorker(worker: WorkerProcess, until: number, immediate?: boolean): Promise<void>;
24
29
  /**
25
30
  * 启动 Node worker,以首次 list RPC 成功作为就绪确认,再返回客户端与进程所有权。
26
31
  * 入口/manifest/环境错误在启动阶段携带有界 stderr 抛出;失败路径仍须回收已创建的进程。
27
32
  * owner 必须等待 close;清理失败不能当作可安全启动替代 worker 的普通启动错误。
28
33
  */
29
- export declare function spawnWorker({ entrypoint, manifest, env, signal, onSpawn, readyTimeoutMs }: SpawnWorkerOptions): Promise<WorkerProcess>;
34
+ export declare function spawnWorker({ entrypoint, manifest, adapterModules, env, signal, onSpawn, readyTimeoutMs }: SpawnWorkerOptions): Promise<WorkerProcess>;
30
35
  export {};
@@ -1,19 +1,37 @@
1
1
  import { createProcessCleanup } from "@mininglamp-oss/octo-runtime-adapter-sdk/process-cleanup";
2
- import { spawn } from "node:child_process";
2
+ import { execFile, spawn } from "node:child_process";
3
+ import { setTimeout as delay } from "node:timers/promises";
3
4
  import { WorkerRpcClient } from "./rpc.js";
5
+ // Reserve more time for tree cleanup until the worker confirms it has started shutdown.
6
+ // A cooperating worker can use its existing 2.5 s shutdown + 1 s logging bounds.
7
+ const WINDOWS_CLOSE_GRACE_MS = 4_000;
8
+ const WINDOWS_TREE_CLEANUP_RESERVE_MS = 1_900;
9
+ const WINDOWS_COOPERATIVE_CLEANUP_RESERVE_MS = 1_000;
10
+ const closeOptions = new WeakMap();
11
+ /** Kernel 内部传递关闭预算;公开 WorkerProcess.close() 仍然无参数。故障恢复不等待日志排空。 */
12
+ export function closeWorker(worker, until, immediate = false) {
13
+ closeOptions.set(worker, { until, immediate });
14
+ return worker.close();
15
+ }
4
16
  /**
5
17
  * 启动 Node worker,以首次 list RPC 成功作为就绪确认,再返回客户端与进程所有权。
6
18
  * 入口/manifest/环境错误在启动阶段携带有界 stderr 抛出;失败路径仍须回收已创建的进程。
7
19
  * owner 必须等待 close;清理失败不能当作可安全启动替代 worker 的普通启动错误。
8
20
  */
9
- export async function spawnWorker({ entrypoint, manifest, env = process.env, signal, onSpawn, readyTimeoutMs = 15_000 }) {
21
+ export async function spawnWorker({ entrypoint, manifest, adapterModules, env = process.env, signal, onSpawn, readyTimeoutMs = 15_000 }) {
10
22
  if (signal?.aborted)
11
23
  throw new Error("adapter-host spawn cancelled");
24
+ if (manifest && adapterModules !== undefined)
25
+ throw new TypeError("manifest and adapterModules are mutually exclusive");
12
26
  if (!Number.isFinite(readyTimeoutMs) || readyTimeoutMs < 1) {
13
27
  throw new Error("adapter-host readyTimeoutMs must be positive");
14
28
  }
15
- // 跨进程只传程序入口及可选文件路径,不序列化 Adapter 实例或工厂函数。
16
- const child = spawn(process.execPath, [entrypoint, ...(manifest ? ["--manifest", manifest] : [])], {
29
+ // 快照通过已有 Node IPC 发送,不占命令行/环境长度,不落盘;永不序列化模块实例。
30
+ const child = spawn(process.execPath, [
31
+ entrypoint,
32
+ ...(manifest ? ["--manifest", manifest] : []),
33
+ ...(adapterModules === undefined ? [] : ["--adapters-from-ipc"])
34
+ ], {
17
35
  argv0: "octo-runtime-executor-kernel",
18
36
  env,
19
37
  stdio: ["pipe", "pipe", "pipe", "ipc"],
@@ -38,8 +56,16 @@ export async function spawnWorker({ entrypoint, manifest, env = process.env, sig
38
56
  child.on("error", (error) => client.close(error));
39
57
  let adapters;
40
58
  try {
59
+ // 先登记进程所有权;快照发送也可能因子进程未读取 IPC 而阻塞,须纳入就绪预算。
41
60
  await onSpawn?.(child);
42
- adapters = await waitForReady(client.list(), readyTimeoutMs, signal);
61
+ const initialize = async () => {
62
+ if (adapterModules !== undefined)
63
+ await new Promise((resolve, reject) => {
64
+ child.send({ type: "kernel.adapters", adapters: adapterModules }, (error) => error ? reject(error) : resolve());
65
+ });
66
+ return client.list();
67
+ };
68
+ adapters = await waitForReady(initialize(), readyTimeoutMs, signal);
43
69
  }
44
70
  catch (error) {
45
71
  client.close(error instanceof Error ? error : new Error(errorMessage(error)));
@@ -51,18 +77,121 @@ export async function spawnWorker({ entrypoint, manifest, env = process.env, sig
51
77
  }
52
78
  throw new Error(`${errorMessage(error)}${stderr ? `: ${stderr}` : ""}`, { cause: error });
53
79
  }
54
- return {
80
+ let closing;
81
+ const worker = {
55
82
  client,
56
83
  child,
57
84
  adapters,
58
- // 先断开 RPC,再等待平台清理;POSIX 不以 leader exit 结束进程组回收。
59
- // Windows 仅有 Node/taskkill 的常规树清理能力,不能承诺父进程提前退出后的后代已清空。
85
+ // 关闭 RPC 后等待正常收尾与平台清理;POSIX 不以 leader exit 结束进程组回收。
86
+ // Windows 排空后须保留 leader 到 taskkill;宽限内提前退出不能当作树已清空。
60
87
  close() {
88
+ if (closing)
89
+ return closing;
90
+ const { until = Date.now() + 5000, immediate = false } = closeOptions.get(worker) ?? {};
61
91
  client.close();
62
- return cleanup();
92
+ // Windows 的 kill(SIGTERM) 是强制终止;先用 EOF 触发现有子进程收尾。
93
+ // S02:POSIX 仍回收整个所属进程组,不能只因 leader 退出而跳过 cleanup。
94
+ return (closing = (async () => {
95
+ const drained = process.platform === "win32" && !immediate
96
+ ? await endWindowsWorkerInput(child, until)
97
+ : undefined;
98
+ try {
99
+ // 启动回滚与 POSIX 保留共享清理;Windows 正常关闭须验证本次 taskkill 的结果。
100
+ // leader 在通知后退出时,不能再用它的退出状态掩盖 taskkill 未能找到树的错误。
101
+ if (process.platform === "win32" && drained !== "skipped")
102
+ await cleanupWindowsWorker(child, until);
103
+ else
104
+ await cleanup();
105
+ // Reaping the OS tree does not erase an observed Adapter cleanup failure.
106
+ if (drained === "incomplete")
107
+ throw Object.assign(new Error("EXECUTOR_CLEANUP_INCOMPLETE"), {
108
+ code: "EXECUTOR_CLEANUP_INCOMPLETE"
109
+ });
110
+ }
111
+ finally {
112
+ if (process.platform === "win32") {
113
+ // 正常收尾或 fallback 结束后释放本层管道;回收失败仍向调用方传播。
114
+ child.stdin.destroy();
115
+ child.stdout.destroy();
116
+ child.stderr.destroy();
117
+ }
118
+ }
119
+ })());
63
120
  },
64
121
  stderr: () => stderr
65
122
  };
123
+ return worker;
124
+ }
125
+ /** 只回收本次 worker 的树。命令和退出确认共用截止时间,任何命令错误都保留为回收失败。 */
126
+ async function cleanupWindowsWorker(child, until) {
127
+ const incomplete = (cause) => Object.assign(new Error("PROCESS_TREE_CLEANUP_INCOMPLETE", { cause }), {
128
+ code: "PROCESS_TREE_CLEANUP_INCOMPLETE"
129
+ });
130
+ if (child.pid === undefined || child.exitCode !== null || child.signalCode !== null)
131
+ throw incomplete();
132
+ const remaining = until - Date.now();
133
+ if (remaining <= 0)
134
+ throw incomplete();
135
+ await new Promise((resolve, reject) => {
136
+ execFile("taskkill", ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true, timeout: remaining }, (error) => (error ? reject(incomplete(error)) : resolve()));
137
+ });
138
+ const confirmUntil = Math.min(until, Date.now() + 200);
139
+ while (child.exitCode === null && child.signalCode === null) {
140
+ const left = confirmUntil - Date.now();
141
+ if (left <= 0)
142
+ throw Object.assign(new Error("PROCESS_CLEANUP_TIMEOUT"), {
143
+ code: "PROCESS_CLEANUP_TIMEOUT"
144
+ });
145
+ await delay(Math.min(10, left));
146
+ }
147
+ }
148
+ /** 私有正常关闭阶段:EOF 触发排空,IPC 只确认排空结果,实际进程树仍由 cleanup 回收。 */
149
+ function endWindowsWorkerInput(child, until) {
150
+ if (child.exitCode !== null || child.signalCode !== null || child.stdin.destroyed)
151
+ return Promise.resolve("skipped");
152
+ return new Promise((resolve) => {
153
+ const graceUntil = Math.min(Date.now() + WINDOWS_CLOSE_GRACE_MS, until - WINDOWS_COOPERATIVE_CLEANUP_RESERVE_MS);
154
+ let settled = false;
155
+ const finish = (result) => {
156
+ if (settled)
157
+ return;
158
+ settled = true;
159
+ clearTimeout(timer);
160
+ child.off("close", fallback);
161
+ child.off("error", fallback);
162
+ child.stdin.off("error", fallback);
163
+ child.off("message", onMessage);
164
+ resolve(result);
165
+ };
166
+ const fallback = () => finish("fallback");
167
+ const onMessage = (message) => {
168
+ if (!message || typeof message !== "object" || !("type" in message))
169
+ return;
170
+ if (message.type === "worker.stopping") {
171
+ // This is progress, not cleanup success. Repeated messages never extend the deadline.
172
+ clearTimeout(timer);
173
+ timer = setTimeout(fallback, Math.max(0, graceUntil - Date.now()));
174
+ return;
175
+ }
176
+ if (message.type === "worker.drained" &&
177
+ "cleanup" in message &&
178
+ (message.cleanup === "complete" || message.cleanup === "incomplete"))
179
+ finish(message.cleanup);
180
+ };
181
+ let timer = setTimeout(fallback, Math.max(0, Math.min(graceUntil, until - WINDOWS_TREE_CLEANUP_RESERVE_MS) - Date.now()));
182
+ // 先监听再发送 EOF;worker 排空后仍存活,输出在通知/超时前持续消费。
183
+ child.once("close", fallback);
184
+ child.once("error", fallback);
185
+ child.stdin.once("error", fallback);
186
+ child.on("message", onMessage);
187
+ try {
188
+ child.stdin.end();
189
+ }
190
+ catch {
191
+ // EPIPE 等故障不新增公开错误合同;真正的回收失败仍由 cleanup 抛出。
192
+ fallback();
193
+ }
194
+ });
66
195
  }
67
196
  function waitForReady(operation, timeoutMs, signal) {
68
197
  return new Promise((resolve, reject) => {
@@ -1,94 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // CLI 使用的独立 worker 入口:只加载显式 manifest 中的 Adapter,注册到唯一 AgentExecutor。
3
- // 通过 stdio NDJSON 为父 Kernel 提供执行;SDK 可用自有代码入口调用 serveWorker,毋须采用此 manifest。
4
- import { readFile } from "node:fs/promises";
5
- import { resolve } from "node:path";
6
- import { resolveAdapterModuleSpecifier } from "./module-specifier.js";
2
+ // 通用清单 worker 不包含产品准备逻辑;SDK 自有 worker 原生 import 后直接调用 serveWorker。
3
+ import { loadManifestAdapters } from "./manifest-adapters.js";
7
4
  import { serveWorker } from "./index.js";
8
- const index = process.argv.indexOf("--manifest");
9
- if (index < 0 || !process.argv[index + 1])
10
- throw new Error("usage: octo-runtime-executor-kernel --manifest <file>");
11
- const manifestPath = resolve(process.argv[index + 1]);
12
- const rawManifest = JSON.parse(await readFile(manifestPath, "utf8"));
13
- const manifest = parseManifest(rawManifest);
14
- const adapters = [];
15
- for (const entry of manifest.adapters) {
16
- const specifier = resolveAdapterModuleSpecifier(entry.module, manifestPath);
17
- const loaded = await import(specifier);
18
- const moduleExports = asRecord(loaded, `adapter module ${entry.module}`);
19
- const exported = moduleExports[entry.exportName];
20
- if (exported === undefined) {
21
- throw new Error(`missing export ${entry.exportName} from ${entry.module}`);
22
- }
23
- // The manifest is a dynamic plugin boundary: validate the factory and adapter
24
- // shape before handing the instance to the host for registration.
25
- const instance = entry.factory ? callFactory(exported, entry.options, entry) : exported;
26
- if (!isHostedAdapter(instance)) {
27
- throw new TypeError(`${entry.module}#${entry.exportName} is not an Octo Runtime adapter`);
28
- }
29
- if (entry.type && instance.metadata.type !== entry.type) {
30
- throw new Error(`manifest type ${entry.type} does not match adapter ${instance.metadata.type}`);
31
- }
32
- adapters.push(instance);
33
- }
34
- // JSON 是 CLI 的装配来源;注册、RPC 和退出清理与 SDK 的代码入口共用。
35
- serveWorker(adapters);
36
- /** Parse and validate the on-disk manifest. Only version 1 is supported; each
37
- * entry names a module, an export, whether that export is a factory, and options. */
38
- function parseManifest(value) {
39
- const record = asRecord(value, "adapter manifest");
40
- if (record.version !== 1)
41
- throw new TypeError("adapter manifest.version must be 1");
42
- if (!Array.isArray(record.adapters))
43
- throw new TypeError("adapter manifest.adapters must be an array");
44
- return {
45
- version: 1,
46
- adapters: record.adapters.map((item, position) => {
47
- const entry = asRecord(item, `adapter manifest entry ${position}`);
48
- const type = optionalString(entry.type);
49
- return {
50
- module: requiredString(entry.module, `adapters[${position}].module`),
51
- exportName: optionalString(entry.export) ?? "default",
52
- factory: entry.factory === true,
53
- options: entry.options === undefined
54
- ? {}
55
- : asRecord(entry.options, `adapters[${position}].options`),
56
- ...(type ? { type } : {})
57
- };
58
- })
59
- };
60
- }
61
- function callFactory(value, options, entry) {
62
- if (typeof value !== "function") {
63
- throw new TypeError(`${entry.module}#${entry.exportName} is marked as a factory but is not callable`);
64
- }
65
- const factory = value;
66
- return factory(options);
67
- }
68
- /** Structural type guard: only `metadata.type`, Agent sources and
69
- * an `invoke` method are required, so any conforming object can be a hosted adapter. */
70
- function isHostedAdapter(value) {
71
- if (!isRecord(value) || !isRecord(value.metadata))
72
- return false;
73
- return (typeof value.metadata.type === "string" &&
74
- Array.isArray(value.metadata.agentSources) &&
75
- value.metadata.agentSources.length > 0 &&
76
- value.metadata.agentSources.every((source) => source === "preset" || source === "provisioned") &&
77
- typeof value.invoke === "function");
78
- }
79
- function asRecord(value, name) {
80
- if (!isRecord(value))
81
- throw new TypeError(`${name} must be an object`);
82
- return value;
83
- }
84
- function isRecord(value) {
85
- return value !== null && typeof value === "object" && !Array.isArray(value);
86
- }
87
- function requiredString(value, name) {
88
- if (typeof value !== "string" || value.length === 0)
89
- throw new TypeError(`${name} must be a non-empty string`);
90
- return value;
91
- }
92
- function optionalString(value) {
93
- return typeof value === "string" && value.length > 0 ? value : undefined;
94
- }
5
+ serveWorker(await loadManifestAdapters());
@@ -28,10 +28,29 @@ export function serveWorker(adapters, options = {}) {
28
28
  logging.kernel("info", "host.ready");
29
29
  // 父 Kernel 断开或停止时等待 Core 取消和清理;需要替换时由 Kernel 监督器重启相同入口。
30
30
  let stopping = false;
31
- const stop = () => {
31
+ let drainedExitCode;
32
+ const stop = (holdForParent = false) => {
32
33
  if (stopping)
33
34
  return;
34
35
  stopping = true;
36
+ // Let the launcher reserve the full bounded drain only for a worker that handles EOF.
37
+ if (holdForParent && process.connected)
38
+ process.send?.({ type: "worker.stopping" }, () => { });
39
+ const finish = async (code) => {
40
+ await logging.close();
41
+ drainedExitCode = code;
42
+ if (holdForParent && process.connected) {
43
+ // Windows 的树清理需要 leader 仍存活。通知只表示排空完成,不代表后代已回收。
44
+ // IPC 保持引用直到 launcher 执行 taskkill;owner 断连仍走下方既有退出收尾。
45
+ process.channel?.ref();
46
+ process.send?.({ type: "worker.drained", cleanup: code === 0 ? "complete" : "incomplete" }, (error) => {
47
+ if (error)
48
+ process.exit(code || 1);
49
+ });
50
+ }
51
+ else
52
+ process.exit(code);
53
+ };
35
54
  void host.shutdown().then(async (result) => {
36
55
  if (result.cleanup === "incomplete") {
37
56
  logging.kernel("error", "host.cleanup_incomplete", {
@@ -39,27 +58,32 @@ export function serveWorker(adapters, options = {}) {
39
58
  unresolved_count: result.unresolvedInvocationIds.length
40
59
  });
41
60
  }
42
- await logging.close();
43
- process.exit(result.cleanup === "complete" ? 0 : 1);
61
+ await finish(result.cleanup === "complete" ? 0 : 1);
44
62
  }, async () => {
45
- await logging.close();
46
- process.exit(1);
63
+ await finish(1);
47
64
  });
48
65
  };
49
- process.once("SIGINT", stop);
50
- process.once("SIGTERM", stop);
51
- process.once("SIGHUP", stop);
52
- process.stdin.once("end", stop);
53
- process.stdin.once("close", stop);
66
+ process.once("SIGINT", () => stop());
67
+ process.once("SIGTERM", () => stop());
68
+ process.once("SIGHUP", () => stop());
69
+ process.stdin.once("end", () => stop(process.platform === "win32"));
70
+ process.stdin.once("close", () => stop(process.platform === "win32"));
54
71
  // 心跳必须由执行进程主事件循环处理;独立线程应答会掩盖执行线程阻塞。
55
72
  process.on("message", (message) => {
56
73
  if (message &&
57
74
  typeof message === "object" &&
58
75
  "type" in message &&
59
76
  message.type === "kernel.ping" &&
77
+ "probe" in message &&
78
+ Number.isSafeInteger(message.probe) &&
60
79
  process.connected) {
61
- process.send?.({ type: "worker.pong" });
80
+ process.send?.({ type: "worker.pong", probe: message.probe });
62
81
  }
63
82
  });
64
- process.once("disconnect", stop);
83
+ process.once("disconnect", () => {
84
+ if (drainedExitCode !== undefined)
85
+ process.exit(drainedExitCode);
86
+ else
87
+ stop();
88
+ });
65
89
  }
@@ -1,9 +1,12 @@
1
1
  import { type AdapterProbeOptions } from "@mininglamp-oss/octo-runtime-adapter-sdk";
2
+ import { type AdapterModuleEntry } from "@mininglamp-oss/octo-runtime-adapter-sdk/module-manifest";
2
3
  import { type KernelStatus } from "./types.js";
3
4
  export interface KernelServerOptions {
4
5
  endpoint: string;
5
6
  workerEntrypoint: string;
6
7
  manifest?: string;
8
+ /** CLI 装配快照;SDK 原生 worker 无需清单或此字段。 */
9
+ adapterModules?: AdapterModuleEntry[];
7
10
  env?: NodeJS.ProcessEnv;
8
11
  adapterConfigurations?: Record<string, AdapterProbeOptions>;
9
12
  stateDirectory?: string;
@@ -15,5 +18,5 @@ export interface KernelServerOptions {
15
18
  */
16
19
  export declare function startKernelServer(options: KernelServerOptions, signal?: AbortSignal): Promise<{
17
20
  status: () => KernelStatus;
18
- close(): Promise<void>;
21
+ close(until?: number): Promise<void>;
19
22
  }>;
@@ -8,6 +8,7 @@ import { HostRunService } from "./host-run-service.js";
8
8
  import { WorkerSupervisor } from "./supervisor.js";
9
9
  import { acquireEndpoint } from "./endpoint.js";
10
10
  import { InvocationManifest } from "./invocation-manifest.js";
11
+ import { readModuleManifest, resolveAdapterModules } from "@mininglamp-oss/octo-runtime-adapter-sdk/module-manifest";
11
12
  import { KernelError } from "./types.js";
12
13
  function validateParams(method, params) {
13
14
  const fields = {
@@ -71,9 +72,15 @@ function validateParams(method, params) {
71
72
  * 启动失败也执行回收;close 失败会保留所有权证据,客户端断开不等同于 owner 关闭。
72
73
  */
73
74
  export async function startKernelServer(options, signal) {
75
+ if (options.manifest && options.adapterModules !== undefined)
76
+ throw new TypeError("manifest and adapterModules are mutually exclusive");
77
+ // 文件仅在服务启动时读取一次;worker 自动恢复沿用快照,不因磁盘改动改变加载集合。
78
+ const adapterModules = options.manifest
79
+ ? resolveAdapterModules(await readModuleManifest(options.manifest, { legacy: true }))
80
+ : options.adapterModules;
74
81
  const instance = randomUUID();
75
82
  let manifest;
76
- const worker = new WorkerSupervisor({ entrypoint: options.workerEntrypoint, manifest: options.manifest, env: options.env }, (pid) => manifest?.setWorker(pid, options.workerEntrypoint) ?? Promise.resolve());
83
+ const worker = new WorkerSupervisor({ entrypoint: options.workerEntrypoint, adapterModules, env: options.env }, (pid) => manifest?.setWorker(pid, options.workerEntrypoint) ?? Promise.resolve());
77
84
  const configure = (input) => {
78
85
  const type = contextItem(input.contexts, "octo.execution", "adapter")?.payload.type;
79
86
  return (options.configurations?.(typeof input.runMetadata?.runtimeId === "string" ? input.runMetadata.runtimeId : undefined) ?? options.adapterConfigurations)?.[typeof type === "string" ? type : "codex"];
@@ -312,7 +319,7 @@ export async function startKernelServer(options, signal) {
312
319
  }
313
320
  });
314
321
  });
315
- const teardown = async () => {
322
+ const teardown = async (until = Date.now() + 5000) => {
316
323
  runs.beginDrain();
317
324
  for (const socket of sockets)
318
325
  socket.destroy();
@@ -330,9 +337,10 @@ export async function startKernelServer(options, signal) {
330
337
  if (listening)
331
338
  await attempt(() => new Promise((resolve) => server.close(() => resolve())));
332
339
  // 先终止 worker 才能让失联的源流结束,再清空 Run 缓存;每个资源仍独立尝试回收并汇总失败。
333
- const workerStopped = await attempt(() => worker.close());
340
+ // 内部沿用服务截止时间,为清单和端点收尾预留时间;不重启一份独立关闭预算。
341
+ const workerStopped = await attempt(() => worker.close(until - 200));
334
342
  await attempt(() => runs.close());
335
- // 进程未确认回收时保留身份清单和端点租约,禁止新实例覆盖所有权。
343
+ // An incomplete worker close retains ownership, including an observed executor cleanup failure.
336
344
  if (workerStopped) {
337
345
  await attempt(() => manifest?.close() ?? Promise.resolve());
338
346
  if (listening && process.platform !== "win32")
@@ -385,8 +393,8 @@ export async function startKernelServer(options, signal) {
385
393
  }
386
394
  return {
387
395
  status,
388
- close() {
389
- return (closing ??= teardown());
396
+ close(until) {
397
+ return (closing ??= teardown(until));
390
398
  }
391
399
  };
392
400
  }
@@ -5,6 +5,7 @@ import { beforeDeadline, ServiceInitializationError } from "./lifecycle.js";
5
5
  export function createKernelService(options) {
6
6
  const configuration = {
7
7
  ...options,
8
+ adapterModules: options.adapterModules && structuredClone(options.adapterModules),
8
9
  adapterConfigurations: options.adapterConfigurations && structuredClone(options.adapterConfigurations),
9
10
  env: options.env && { ...options.env }
10
11
  };
@@ -33,7 +34,7 @@ export function createKernelService(options) {
33
34
  await beforeDeadline(starting?.catch(() => undefined) ?? Promise.resolve(), until);
34
35
  if (startupFailure instanceof AggregateError)
35
36
  throw startupFailure;
36
- await beforeDeadline(server?.close() ?? Promise.resolve(), until);
37
+ await beforeDeadline(server?.close(until) ?? Promise.resolve(), until);
37
38
  }
38
39
  catch {
39
40
  errors.push({ resource: "kernel", code: "KERNEL_CLEANUP_INCOMPLETE" });
@@ -1,6 +1,6 @@
1
1
  import { type SpawnWorkerOptions } from "./process.js";
2
2
  import type { KernelStatus } from "./types.js";
3
- /** Kernel 独占 worker 的创建与回收;清理失败时禁止启动替代实例。 */
3
+ /** Kernel exclusively owns worker replacement; incomplete cleanup prevents a second owner. */
4
4
  export declare class WorkerSupervisor {
5
5
  #private;
6
6
  private readonly options;
@@ -9,5 +9,5 @@ export declare class WorkerSupervisor {
9
9
  get status(): KernelStatus["worker"];
10
10
  get client(): import("./rpc.js").WorkerRpcClient;
11
11
  start(signal?: AbortSignal): Promise<void>;
12
- close(): Promise<void>;
12
+ close(until?: number): Promise<void>;
13
13
  }