@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
@@ -1,6 +1,7 @@
1
- import { spawnWorker } from "./process.js";
1
+ import { closeWorker, spawnWorker } from "./process.js";
2
2
  import { KernelError } from "./types.js";
3
- /** Kernel 独占 worker 的创建与回收;清理失败时禁止启动替代实例。 */
3
+ import { WorkerHeartbeat } from "./heartbeat.js";
4
+ /** Kernel exclusively owns worker replacement; incomplete cleanup prevents a second owner. */
4
5
  export class WorkerSupervisor {
5
6
  options;
6
7
  recordWorker;
@@ -11,12 +12,13 @@ export class WorkerSupervisor {
11
12
  #closing = false;
12
13
  #recovery;
13
14
  #timer;
14
- #lastPong = 0;
15
- #pendingPing = false;
16
- #stableSince = 0;
15
+ #heartbeat;
16
+ #activeTime = 0;
17
17
  #attempts = [];
18
+ #slowRecovery = false;
18
19
  #abort = new AbortController();
19
20
  #cleanupFailure;
21
+ #closeUntil;
20
22
  constructor(options, recordWorker) {
21
23
  this.options = options;
22
24
  this.recordWorker = recordWorker;
@@ -63,35 +65,44 @@ export class WorkerSupervisor {
63
65
  throw error;
64
66
  }
65
67
  if (this.#closing) {
66
- await worker.close();
68
+ try {
69
+ await closeWorker(worker, this.#closeUntil ?? Date.now() + 5000, true);
70
+ }
71
+ catch (error) {
72
+ // A late startup still owns resources; failed rollback must block every future replacement.
73
+ throw (this.#cleanupFailure = new AggregateError([error], "WORKER_CLEANUP_INCOMPLETE"));
74
+ }
67
75
  return;
68
76
  }
69
77
  this.#worker = worker;
70
78
  this.#generation++;
71
79
  this.#state = "ready";
72
- this.#stableSince = Date.now();
73
- this.#lastPong = Date.now();
74
- this.#pendingPing = false;
80
+ const heartbeat = new WorkerHeartbeat();
81
+ this.#heartbeat = heartbeat;
82
+ let elapsed = 0;
75
83
  worker.child.on("message", (message) => {
76
84
  if (this.#worker === worker &&
77
85
  message &&
78
86
  typeof message === "object" &&
79
87
  "type" in message &&
80
88
  message.type === "worker.pong") {
81
- this.#lastPong = Date.now();
82
- this.#pendingPing = false;
89
+ heartbeat.pong("probe" in message ? message.probe : undefined);
83
90
  }
84
91
  });
85
92
  worker.child.once("exit", () => this.#failed(worker));
86
93
  worker.child.once("error", () => this.#failed(worker));
87
94
  this.#timer = setInterval(() => {
88
- if (Date.now() - this.#lastPong >= 5000) {
95
+ if (this.#closing || this.#worker !== worker || this.#state !== "ready")
96
+ return;
97
+ const probe = heartbeat.tick();
98
+ this.#activeTime += heartbeat.activeMs - elapsed;
99
+ elapsed = heartbeat.activeMs;
100
+ if (probe.type === "expired") {
89
101
  this.#failed(worker);
90
102
  return;
91
103
  }
92
- if (!this.#pendingPing && worker.child.connected) {
93
- this.#pendingPing = true;
94
- worker.child.send({ type: "kernel.ping" }, (error) => {
104
+ if (probe.type === "ping" && worker.child.connected) {
105
+ worker.child.send({ type: "kernel.ping", probe: probe.id }, (error) => {
95
106
  if (error)
96
107
  this.#failed(worker);
97
108
  });
@@ -106,31 +117,39 @@ export class WorkerSupervisor {
106
117
  return;
107
118
  clearInterval(this.#timer);
108
119
  this.#state = "recovering";
109
- // 先拒绝原 RPC,让 Run 源生成唯一 lost 终态;恢复只允许新 Run。
120
+ // Reject the old RPC first so its source emits one lost terminal. Recovery admits new work only.
110
121
  worker.client.close(new KernelError("WORKER_LOST"));
111
- if (Date.now() - this.#stableSince >= 60_000) {
122
+ if ((this.#heartbeat?.healthyMs ?? 0) >= 60_000) {
112
123
  this.#attempts = [];
113
124
  this.#attempt = 0;
125
+ this.#slowRecovery = false;
114
126
  }
115
- this.#recovery = this.#recover(worker).catch(() => {
116
- this.#state = "exhausted";
127
+ this.#recovery = this.#recover(worker).catch((error) => {
128
+ // Only unconfirmed cleanup or ownership ends recovery; ordinary startup failures stay retryable.
129
+ this.#cleanupFailure ??= new AggregateError([error], "WORKER_RECOVERY_BLOCKED");
130
+ if (!this.#closing)
131
+ this.#state = "exhausted";
117
132
  });
118
133
  }
119
134
  async #recover(worker) {
120
- // 旧 worker 清理成功才允许替换,避免两个 Provider 实例并行持有同一执行资源。
121
- // 只恢复服务可用性,不重放旧 Run;旧流已由 #failed 标为 lost。
122
- await worker.close();
135
+ // Reap the old worker before replacement to prevent concurrent Provider owners.
136
+ // Restore service availability only; #failed already marked old runs lost and never replays them.
137
+ // Preserve baseline recovery for leaders that exited before close began. On Windows this
138
+ // path cannot verify detached descendants; EOF-close checks require a live leader at entry.
139
+ if (worker.child.exitCode !== null || worker.child.signalCode !== null)
140
+ await worker.close();
141
+ else
142
+ await closeWorker(worker, Date.now() + 4800, true);
123
143
  await this.recordWorker?.(undefined);
124
144
  this.#worker = undefined;
125
- for (let attempt = 0; attempt < 3; attempt++) {
126
- if (this.#closing)
127
- return;
128
- this.#attempts = this.#attempts.filter((at) => Date.now() - at < 60_000);
145
+ while (!this.#closing) {
146
+ // Suspend time and clock changes must not replenish a recently exhausted restart budget.
147
+ this.#attempts = this.#attempts.filter((at) => this.#activeTime - at < 60_000);
129
148
  if (this.#attempts.length >= 3)
130
- break;
131
- const delay = [250, 1000, 4000][this.#attempts.length];
132
- this.#attempts.push(Date.now());
133
- this.#attempt++;
149
+ this.#slowRecovery = true;
150
+ // Exhausting the fast budget opens a bounded-rate recovery loop, not a permanent outage.
151
+ // Keep slow recovery across short-lived workers until 60 seconds of health is observed.
152
+ const delay = this.#slowRecovery ? 15_000 : [250, 1000, 4000][this.#attempts.length];
134
153
  await new Promise((resolve) => {
135
154
  const finish = () => {
136
155
  clearTimeout(timer);
@@ -142,6 +161,9 @@ export class WorkerSupervisor {
142
161
  });
143
162
  if (this.#closing)
144
163
  return;
164
+ if (!this.#slowRecovery)
165
+ this.#attempts.push(this.#activeTime);
166
+ this.#attempt = Math.min(Number.MAX_SAFE_INTEGER, this.#attempt + 1);
145
167
  try {
146
168
  await this.#spawn();
147
169
  return;
@@ -149,17 +171,18 @@ export class WorkerSupervisor {
149
171
  catch (error) {
150
172
  if (error instanceof AggregateError)
151
173
  throw error;
152
- /* 普通启动失败已回收;聚合清理错误禁止继续替换。 */
174
+ /* Ordinary startup failures are reaped; aggregate cleanup failures prevent replacement. */
153
175
  }
154
176
  }
155
- this.#state = "exhausted";
156
177
  }
157
- async close() {
178
+ async close(until = Date.now() + 5000) {
158
179
  this.#closing = true;
180
+ this.#closeUntil ??= until;
159
181
  this.#abort.abort();
160
182
  clearInterval(this.#timer);
161
183
  await this.#recovery;
162
- await this.#worker?.close();
184
+ if (this.#worker)
185
+ await closeWorker(this.#worker, this.#closeUntil);
163
186
  if (this.#cleanupFailure)
164
187
  throw this.#cleanupFailure;
165
188
  await this.recordWorker?.(undefined);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mininglamp-oss/octo-runtime-executor-kernel",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -8,27 +8,19 @@
8
8
  "types": "./dist/index.d.ts",
9
9
  "import": "./dist/index.js"
10
10
  },
11
+ "./manifest-adapters": {
12
+ "types": "./dist/manifest-adapters.d.ts",
13
+ "import": "./dist/manifest-adapters.js"
14
+ },
11
15
  "./runner": {
12
16
  "types": "./dist/runner.d.ts",
13
17
  "import": "./dist/runner.js"
14
18
  }
15
19
  },
16
20
  "dependencies": {
17
- "@mininglamp-oss/octo-runtime-executor-core": "1.1.0",
18
- "@mininglamp-oss/octo-runtime-adapter-antigravity": "1.1.0",
19
- "@mininglamp-oss/octo-runtime-adapter-claude-code": "1.1.0",
20
- "@mininglamp-oss/octo-runtime-adapter-codebuddy": "1.1.0",
21
- "@mininglamp-oss/octo-runtime-adapter-codex": "1.1.0",
22
- "@mininglamp-oss/octo-runtime-adapter-hermes": "1.1.0",
23
- "@mininglamp-oss/octo-runtime-adapter-kimi-code": "1.1.0",
24
- "@mininglamp-oss/octo-runtime-adapter-mcode": "1.1.0",
25
- "@mininglamp-oss/octo-runtime-adapter-openclaw": "1.1.0",
26
- "@mininglamp-oss/octo-runtime-adapter-opencode": "1.1.0",
27
- "@mininglamp-oss/octo-runtime-adapter-qodercli": "1.1.0",
28
- "@mininglamp-oss/octo-runtime-adapter-qoderclicn": "1.1.0",
29
- "@mininglamp-oss/octo-runtime-adapter-zcode-gui": "1.1.0",
30
- "@mininglamp-oss/octo-runtime-protocol": "1.1.0",
31
- "@mininglamp-oss/octo-runtime-adapter-sdk": "1.1.0"
21
+ "@mininglamp-oss/octo-runtime-executor-core": "1.1.1",
22
+ "@mininglamp-oss/octo-runtime-protocol": "1.1.1",
23
+ "@mininglamp-oss/octo-runtime-adapter-sdk": "1.1.1"
32
24
  },
33
25
  "types": "./dist/index.d.ts",
34
26
  "files": [
@@ -6,12 +6,12 @@
6
6
  "$comment": "声明文件最多 1 MiB,POSIX 权限须为 0600;单 Bot JSON 最多 256 KiB。Bot 按规范化 api_url 与 bot_id 组合判重。路径可访问性、凭据、身份和 Space 授权、重试起点不大于上限等关系仍需运行时检查。Context 内部对象遵循 octo.context-envelope/v3,具体协议约束由现有运行时校验,本文件不重复展开。",
7
7
  "type": "object",
8
8
  "additionalProperties": false,
9
- "required": ["module", "config"],
9
+ "required": ["config"],
10
10
  "properties": {
11
11
  "module": {
12
12
  "type": "string",
13
13
  "const": "@mininglamp-oss/octo-runtime-extra-channel-gateway",
14
- "description": "必填。Octo IM Extra 的固定包入口;宿主须预先安装并确保模块可解析。此 schema 约束包名声明,宿主通过绝对路径加载同一模块时可自行调整该约束。插件实例名及 CLI 配置目录名为 channel-gateway。"
14
+ "description": "可选旧声明。CLI 从 manifest.json 选择模块;省略时使用有效清单,保留时须与清单文字一致。SDK 原生 import 不需要此字段。此 schema 约束固定包名,宿主使用其他公开路径时可自行调整。"
15
15
  },
16
16
  "enabled": {
17
17
  "type": "boolean",
@@ -49,7 +49,6 @@ export class ImRuntime {
49
49
  #busyNotifications = new Map();
50
50
  #busyNotifiedAt = new Map();
51
51
  #retry = 0;
52
- #everOnline = false;
53
52
  #disconnecting;
54
53
  constructor(config, dependencies) {
55
54
  this.dependencies = dependencies;
@@ -110,7 +109,6 @@ export class ImRuntime {
110
109
  this.dependencies.signal?.addEventListener("abort", this.#ownerStopped, { once: true });
111
110
  this.#delivery = new ImDeliveryDispatcher(this.#signal);
112
111
  }
113
- this.#everOnline = false;
114
112
  this.#retry = 0;
115
113
  this.#error = undefined;
116
114
  this.#state = "connecting";
@@ -125,6 +123,9 @@ export class ImRuntime {
125
123
  async #connect(recovering) {
126
124
  if (this.#closed || this.#draining || this.#signal.aborted)
127
125
  return;
126
+ this.#log(recovering ? "channel.retry_started" : "channel.connecting", {
127
+ attempt: this.#retry
128
+ });
128
129
  this.#connectionAbort?.abort();
129
130
  try {
130
131
  await this.#disconnecting;
@@ -134,7 +135,7 @@ export class ImRuntime {
134
135
  this.#state = this.#closed ? "stopped" : "failed";
135
136
  if (!this.#closed)
136
137
  this.#error = "IM_DISPOSE_FAILED";
137
- this.#everOnline = false;
138
+ this.#diagnostic("IM_DISPOSE_FAILED");
138
139
  return;
139
140
  }
140
141
  if (this.#closed || this.#draining || this.#signal.aborted)
@@ -183,7 +184,6 @@ export class ImRuntime {
183
184
  if (disconnected)
184
185
  throw disconnected;
185
186
  starting = false;
186
- this.#everOnline = true;
187
187
  this.#state = "online";
188
188
  this.#log("channel.connected", { state: "online" });
189
189
  this.#error = undefined;
@@ -196,21 +196,19 @@ export class ImRuntime {
196
196
  const failure = connectionFailure(error);
197
197
  this.#connectionAbort.abort();
198
198
  this.#error = failure.message;
199
- const retry = recovering && this.#everOnline && failure.retryable;
199
+ // Offline startup and later disconnects share recovery; only error semantics decide eligibility.
200
+ const retry = failure.retryable;
200
201
  this.#state = retry ? "retrying" : "failed";
201
202
  this.#log("channel.connection_failed", { state: this.#state, code: failure.message });
202
203
  try {
203
204
  await connector.close();
204
205
  }
205
206
  catch {
206
- this.#everOnline = false;
207
207
  if (!this.#closed)
208
208
  this.#state = "failed";
209
209
  this.#diagnostic("IM_DISPOSE_FAILED");
210
210
  return;
211
211
  }
212
- if (!retry)
213
- this.#everOnline = false;
214
212
  if (retry)
215
213
  this.#scheduleReconnect(failure.retryAfterMs);
216
214
  }
@@ -218,12 +216,10 @@ export class ImRuntime {
218
216
  #lostConnection(connector, failure) {
219
217
  this.#log("channel.disconnected", { code: failure.message });
220
218
  this.#error = failure.message;
221
- const retry = this.#everOnline && failure.retryable;
219
+ const retry = failure.retryable;
222
220
  this.#state = retry ? "retrying" : "failed";
223
- if (!retry)
224
- this.#everOnline = false;
225
221
  this.#connectionAbort?.abort();
226
- // 先收回旧连接资源,再计时;停止信号与迟到回调不能建立第二条连接。
222
+ // Reap the old connection before backoff; shutdown and stale callbacks cannot create a second owner.
227
223
  this.#disconnecting = connector.close();
228
224
  void this.#disconnecting.then(() => {
229
225
  if (retry)
@@ -231,7 +227,6 @@ export class ImRuntime {
231
227
  }, () => {
232
228
  if (!this.#closed)
233
229
  this.#state = "failed";
234
- this.#everOnline = false;
235
230
  this.#diagnostic("IM_DISPOSE_FAILED");
236
231
  });
237
232
  }
@@ -242,9 +237,14 @@ export class ImRuntime {
242
237
  this.#state !== "retrying" ||
243
238
  this.#reconnect)
244
239
  return;
245
- const delayMs = Math.max(Math.min(30_000, 5_000 * 2 ** this.#retry), retryAfterMs);
246
- this.#retry = Math.min(3, this.#retry + 1);
247
- // Node 超过 2^31-1 的 timeout 会变为 1ms;长 Retry-After 分段等待,不能绕过限流。
240
+ const delayMs = Math.max([5_000, 10_000, 15_000][Math.min(2, this.#retry)], retryAfterMs);
241
+ this.#retry = Math.min(Number.MAX_SAFE_INTEGER, this.#retry + 1);
242
+ this.#log("channel.retry_scheduled", {
243
+ state: "retrying",
244
+ delay_ms: delayMs,
245
+ attempt: this.#retry
246
+ });
247
+ // Node clamps oversized timers to 1ms. Partition long Retry-After waits without bypassing throttling.
248
248
  const deadline = performance.now() + delayMs;
249
249
  const wake = () => {
250
250
  if (this.#closed || this.#draining || this.#signal.aborted || this.#state !== "retrying")
@@ -1069,7 +1069,6 @@ export class ImRuntime {
1069
1069
  this.#state = "stopped";
1070
1070
  this.#log("channel.stopped", { state: "stopped" });
1071
1071
  this.#error = undefined;
1072
- this.#everOnline = false;
1073
1072
  this.#retry = 0;
1074
1073
  for (const preparation of this.#preparing.values())
1075
1074
  preparation.abort();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mininglamp-oss/octo-runtime-extra-channel-gateway",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -18,11 +18,11 @@
18
18
  "docs"
19
19
  ],
20
20
  "dependencies": {
21
- "@mininglamp-oss/octo-runtime-executor-kernel": "1.1.0",
22
- "@mininglamp-oss/octo-runtime-adapter-sdk": "1.1.0",
23
- "@mininglamp-oss/octo-runtime-executor-core": "1.1.0",
24
- "@mininglamp-oss/octo-runtime-host-sdk": "1.1.0",
25
- "@mininglamp-oss/octo-runtime-protocol": "1.1.0",
21
+ "@mininglamp-oss/octo-runtime-executor-kernel": "1.1.1",
22
+ "@mininglamp-oss/octo-runtime-adapter-sdk": "1.1.1",
23
+ "@mininglamp-oss/octo-runtime-executor-core": "1.1.1",
24
+ "@mininglamp-oss/octo-runtime-host-sdk": "1.1.1",
25
+ "@mininglamp-oss/octo-runtime-protocol": "1.1.1",
26
26
  "mdast-util-from-markdown": "2.1.0",
27
27
  "ws": "8.22.0"
28
28
  }
@@ -6,12 +6,12 @@
6
6
  "$comment": "声明文件最多 1 MiB,POSIX 权限须为 0600。Bot 按 ws_url 与 bot_id 的组合判重;凭据文件权限、目标 Agent 是否存在、Provider 是否可用等仍需运行时检查。Context 内部对象遵循 octo.context-envelope/v3,具体协议约束由现有运行时校验,本文件不重复展开。",
7
7
  "type": "object",
8
8
  "additionalProperties": false,
9
- "required": ["module", "config"],
9
+ "required": ["config"],
10
10
  "properties": {
11
11
  "module": {
12
12
  "type": "string",
13
13
  "const": "@mininglamp-oss/octo-runtime-extra-channel-wecom",
14
- "description": "必填。企业微信 Extra 的固定包入口;宿主须预先安装并确保模块可解析。此 schema 约束包名声明,宿主通过绝对路径加载同一模块时可自行调整该约束。插件实例名及 CLI 配置目录名为 channel-wecom。"
14
+ "description": "可选旧声明。CLI 从 manifest.json 选择模块;省略时使用有效清单,保留时须与清单文字一致。SDK 原生 import 不需要此字段。此 schema 约束固定包名,宿主使用其他公开路径时可自行调整。"
15
15
  },
16
16
  "enabled": {
17
17
  "type": "boolean",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mininglamp-oss/octo-runtime-extra-channel-wecom",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -18,10 +18,10 @@
18
18
  "docs"
19
19
  ],
20
20
  "dependencies": {
21
- "@mininglamp-oss/octo-runtime-host-sdk": "1.1.0",
22
- "@mininglamp-oss/octo-runtime-executor-core": "1.1.0",
23
- "@mininglamp-oss/octo-runtime-executor-kernel": "1.1.0",
21
+ "@mininglamp-oss/octo-runtime-host-sdk": "1.1.1",
22
+ "@mininglamp-oss/octo-runtime-executor-core": "1.1.1",
23
+ "@mininglamp-oss/octo-runtime-executor-kernel": "1.1.1",
24
24
  "ws": "8.22.0",
25
- "@mininglamp-oss/octo-runtime-protocol": "1.1.0"
25
+ "@mininglamp-oss/octo-runtime-protocol": "1.1.1"
26
26
  }
27
27
  }
@@ -6,12 +6,12 @@
6
6
  "$comment": "声明文件最多 1 MiB,POSIX 权限须为 0600。端口占用、路径可访问性、会话文件内容及文件权限仍需运行时检查。加载生命周期 loaded 不等于监听成功,宿主须检查插件状态中的 listening 与 errors。",
7
7
  "type": "object",
8
8
  "additionalProperties": false,
9
- "required": ["module", "config"],
9
+ "required": ["config"],
10
10
  "properties": {
11
11
  "module": {
12
12
  "type": "string",
13
13
  "const": "@mininglamp-oss/octo-runtime-extra-webui",
14
- "description": "必填。本地 WebUI Extra 的固定包入口;宿主须预先安装并确保模块可解析。此 schema 约束包名声明,宿主通过绝对路径加载同一模块时可自行调整该约束。插件实例名及 CLI 配置目录名为 webui。"
14
+ "description": "可选旧声明。CLI 从 manifest.json 选择模块;省略时使用有效清单,保留时须与清单文字一致。SDK 原生 import 不需要此字段。此 schema 约束固定包名,宿主使用其他公开路径时可自行调整。"
15
15
  },
16
16
  "enabled": {
17
17
  "type": "boolean",
@@ -73,7 +73,7 @@ async function initialize(ctx, config) {
73
73
  errors.push("WEBUI_CONTEXT_INVALID");
74
74
  return;
75
75
  }
76
- const store = new SessionStore(config.sessionStorePath);
76
+ const store = new SessionStore(config.sessionStorePath, ctx.log);
77
77
  try {
78
78
  await store.open();
79
79
  }
@@ -1,3 +1,4 @@
1
+ import type { RuntimeLog } from "@mininglamp-oss/octo-runtime-host-sdk";
1
2
  export interface Session {
2
3
  id: string;
3
4
  title: string;
@@ -14,9 +15,11 @@ export declare function validMessages(value: unknown): value is unknown[];
14
15
  export declare function validBinding(value: unknown): value is Record<string, unknown>;
15
16
  export declare class SessionStore {
16
17
  readonly path: string;
18
+ private readonly log?;
17
19
  records: Session[];
18
20
  private queue;
19
- constructor(path: string);
21
+ constructor(path: string, log?: RuntimeLog | undefined);
22
+ private cleanupError;
20
23
  open(): Promise<void>;
21
24
  change<T>(operation: (records: Session[]) => T): Promise<T>;
22
25
  }
@@ -1,6 +1,26 @@
1
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
1
+ import { mkdir, open, readFile, rename, unlink } from "node:fs/promises";
2
2
  import { dirname } from "node:path";
3
3
  import { randomUUID } from "node:crypto";
4
+ import { setTimeout as delay } from "node:timers/promises";
5
+ // 本机 Windows 的瞬态 EPERM 在约 1.6 秒后恢复;只重试替换,不重放业务变更。
6
+ // 最多 8 次替换、3170 ms 退避;文件系统调用和调度本身的耗时不计入退避预算。
7
+ const replacementDelays = [20, 50, 100, 200, 400, 800, 1600];
8
+ async function replace(temporary, destination) {
9
+ for (let attempt = 0;; attempt++) {
10
+ try {
11
+ await rename(temporary, destination);
12
+ return;
13
+ }
14
+ catch (error) {
15
+ // 返回结束重试的实际错误及其原生字段,不能让最初的 EPERM 掩盖后续错误。
16
+ if (process.platform !== "win32" ||
17
+ error.code !== "EPERM" ||
18
+ attempt === replacementDelays.length)
19
+ throw error;
20
+ await delay(replacementDelays[attempt]);
21
+ }
22
+ }
23
+ }
4
24
  export function validMessages(value) {
5
25
  return (Array.isArray(value) &&
6
26
  value.every((message) => {
@@ -24,10 +44,21 @@ export function validBinding(value) {
24
44
  }
25
45
  export class SessionStore {
26
46
  path;
47
+ log;
27
48
  records = [];
28
49
  queue = Promise.resolve();
29
- constructor(path) {
50
+ constructor(path, log) {
30
51
  this.path = path;
52
+ this.log = log;
53
+ }
54
+ cleanupError(event, error) {
55
+ // 清理可能因未释放句柄等原因失败;仅报告错误码,诊断失败不能覆盖保存错误。
56
+ try {
57
+ this.log?.("warn", event, { code: error?.code });
58
+ }
59
+ catch {
60
+ // 日志回调不能改变存储操作的结果。
61
+ }
31
62
  }
32
63
  async open() {
33
64
  try {
@@ -69,8 +100,27 @@ export class SessionStore {
69
100
  throw new Error("WEBUI_STORE_TOO_LARGE");
70
101
  await mkdir(dirname(this.path), { recursive: true, mode: 0o700 });
71
102
  const temporary = `${this.path}.${randomUUID()}.tmp`;
72
- await writeFile(temporary, text, { mode: 0o600, flag: "wx" });
73
- await rename(temporary, this.path);
103
+ // 独占创建成功才取得所有权;open 失败不能删除可能属于其他写入者的文件。
104
+ const handle = await open(temporary, "wx", 0o600);
105
+ try {
106
+ try {
107
+ await handle.writeFile(text);
108
+ }
109
+ catch (error) {
110
+ await handle
111
+ .close()
112
+ .catch((closeError) => this.cleanupError("webui.store.close_failed", closeError));
113
+ throw error;
114
+ }
115
+ // 关闭失败同样是保存失败,不能尝试替换或提交内存。
116
+ await handle.close();
117
+ await replace(temporary, this.path);
118
+ }
119
+ catch (error) {
120
+ await unlink(temporary).catch((cleanupError) => this.cleanupError("webui.store.temporary_cleanup_failed", cleanupError));
121
+ throw error;
122
+ }
123
+ // rename 成功就是提交点;此后没有可能将成功翻转成失败的日志或清理动作。
74
124
  this.records = next;
75
125
  return result;
76
126
  });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mininglamp-oss/octo-runtime-extra-webui",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "Local Runtime workbench with canonical SSE presentation",
@@ -19,6 +19,6 @@
19
19
  "porting-map.md"
20
20
  ],
21
21
  "dependencies": {
22
- "@mininglamp-oss/octo-runtime-protocol": "1.1.0"
22
+ "@mininglamp-oss/octo-runtime-protocol": "1.1.1"
23
23
  }
24
24
  }
@@ -6,8 +6,15 @@ export interface ExtraDescriptor {
6
6
  enabled?: boolean;
7
7
  config: JsonObject;
8
8
  }
9
- export type StoredExtra = Omit<ExtraDescriptor, "name">;
9
+ /** CLI 配置可由 manifest 提供模块;原生代码声明 ExtraDescriptor 仍显式指定模块。 */
10
+ export interface StoredExtra {
11
+ module?: string;
12
+ enabled?: boolean;
13
+ config: JsonObject;
14
+ }
10
15
  export declare function assertStoredExtra(value: unknown): asserts value is StoredExtra;
11
16
  export declare function extraRegistration(descriptor: ExtraDescriptor): ExtraRegistration;
12
17
  /** Called only by a host that explicitly selects this directory as its source. */
13
18
  export declare function readExtraDirectory(directory: string): Promise<ExtraRegistration[]>;
19
+ /** 宿主显式选取的配置文件;保持凭据权限校验,不解析或导入模块。 */
20
+ export declare function readStoredExtra(directory: string, name: string): Promise<StoredExtra>;
@@ -8,7 +8,7 @@ export function assertStoredExtra(value) {
8
8
  const item = value;
9
9
  if (Object.keys(item).some((key) => !["module", "enabled", "config"].includes(key)))
10
10
  throw new Error("EXTRA_CONFIG_FIELD_INVALID");
11
- if (typeof item.module !== "string" || !validModule(item.module))
11
+ if (item.module !== undefined && (typeof item.module !== "string" || !validModule(item.module)))
12
12
  throw new Error("EXTRA_MODULE_INVALID");
13
13
  if (item.enabled !== undefined && typeof item.enabled !== "boolean")
14
14
  throw new Error("EXTRA_ENABLED_INVALID");
@@ -35,12 +35,15 @@ export function extraRegistration(descriptor) {
35
35
  assertExtraName(descriptor.name);
36
36
  const { name, ...stored } = descriptor;
37
37
  assertStoredExtra(stored);
38
+ const module = stored.module;
39
+ if (!module)
40
+ throw new Error("EXTRA_MODULE_REQUIRED");
38
41
  return {
39
42
  name,
40
- module: stored.module,
43
+ module,
41
44
  config: stored.config,
42
45
  enabled: stored.enabled,
43
- load: () => import(isAbsolute(stored.module) ? pathToFileURL(stored.module).href : stored.module)
46
+ load: () => import(isAbsolute(module) ? pathToFileURL(module).href : module)
44
47
  };
45
48
  }
46
49
  /** Called only by a host that explicitly selects this directory as its source. */
@@ -56,15 +59,14 @@ export async function readExtraDirectory(directory) {
56
59
  continue;
57
60
  assertExtraName(entry.name);
58
61
  try {
59
- const file = join(directory, entry.name, "config.json");
60
- const info = await lstat(file);
61
- if (!info.isFile() || (process.platform !== "win32" && info.mode & 0o077))
62
- throw new Error("EXTRA_CONFIG_PERMISSIONS_INVALID");
63
- if (info.size > 1024 * 1024)
64
- throw new Error("EXTRA_CONFIG_TOO_LARGE");
65
- const stored = JSON.parse(await readFile(file, "utf8"));
66
- assertStoredExtra(stored);
67
- registrations.push(extraRegistration({ name: entry.name, ...stored }));
62
+ const stored = await readStoredExtra(directory, entry.name);
63
+ registrations.push(stored.module
64
+ ? extraRegistration({ ...stored, name: entry.name, module: stored.module })
65
+ : {
66
+ ...stored,
67
+ name: entry.name,
68
+ load: () => Promise.reject(new Error("EXTRA_MODULE_REQUIRED"))
69
+ });
68
70
  }
69
71
  catch (error) {
70
72
  const code = error instanceof Error && /^EXTRA_[A-Z_]+$/.test(error.message)
@@ -79,6 +81,19 @@ export async function readExtraDirectory(directory) {
79
81
  }
80
82
  return registrations;
81
83
  }
84
+ /** 宿主显式选取的配置文件;保持凭据权限校验,不解析或导入模块。 */
85
+ export async function readStoredExtra(directory, name) {
86
+ assertExtraName(name);
87
+ const file = join(directory, name, "config.json");
88
+ const info = await lstat(file);
89
+ if (!info.isFile() || (process.platform !== "win32" && info.mode & 0o077))
90
+ throw new Error("EXTRA_CONFIG_PERMISSIONS_INVALID");
91
+ if (info.size > 1024 * 1024)
92
+ throw new Error("EXTRA_CONFIG_TOO_LARGE");
93
+ const stored = JSON.parse(await readFile(file, "utf8"));
94
+ assertStoredExtra(stored);
95
+ return stored;
96
+ }
82
97
  function validModule(module) {
83
98
  if (isAbsolute(module))
84
99
  return true;