autowonder 0.3.8 → 0.3.10

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.md CHANGED
@@ -6,10 +6,10 @@ AutoWonder 本地 agent runtime。安装后启动 daemon,持续轮询本地 as
6
6
 
7
7
  ```bash
8
8
  # Qoder CLI(模型使用 provider model ID;Context Window 使用固定档位)
9
- npx -y autowonder@0.3.8 connect --ws-url <wss-endpoint> --token <executor-token> --executor-id <executor-id> --provider qoder --model qmodel_latest --reasoning-effort medium --context-window 260000
9
+ npx -y autowonder@0.3.10 connect --ws-url <wss-endpoint> --token <executor-token> --executor-id <executor-id> --provider qoder --model qmodel_latest --reasoning-effort medium --context-window 260000
10
10
 
11
11
  # Qoder CLI CN
12
- npx -y autowonder@0.3.8 connect --ws-url <wss-endpoint> --token <executor-token> --executor-id <executor-id> --provider qodercn
12
+ npx -y autowonder@0.3.10 connect --ws-url <wss-endpoint> --token <executor-token> --executor-id <executor-id> --provider qodercn
13
13
  ```
14
14
 
15
15
  `connect` 会安装当前 npm 包内置的 daemon,并使用页面生成的 WebSocket endpoint、Token 和执行器 ID 建立连接。客户端只支持通过 Qoder 系列 CLI 启动(`qoder` 对应 `qodercli`,`qodercn` 对应 `qodercn`),其他 `--provider` 取值会在启动 daemon 前直接报错拦截并提示 `Unsupported provider`。选择 Qoder CLI 或 Qoder CLI CN 时需要 Node.js 20+;runtime 会在缺少对应 CLI 时自动通过 npm 安装,并在尚未登录时打开浏览器登录。runtime 会继承当前用户的 HOME、环境变量和 CLI 登录状态。
@@ -35,6 +35,8 @@ Qoder 的 Context Window 只能使用 `1000000`、`400000` 或 `260000`,Reason
35
35
 
36
36
  daemon 会响应服务端的 `QODER_MODEL_CATALOG_REQUEST` 帧,用当前 provider 对应的本机 Qoder CLI 发现可用模型目录,只回传 `{id, name}`,让执行器页面展示这台机器真实可选的模型。该请求仅在 `executorId` 与 `provider` 都与本机一致时处理,发现超时 20 秒,并发请求合并为一次发现,`qoder` 与 `qodercn` 的发现互相隔离、不共享配置根。
37
37
 
38
+ daemon 还在协议能力位里声明 `TASK_MODEL_SELECTION_V1`:服务端可在派单帧中附带任务级模型选择(`modelSelection`,含目标模型、条目计数与阈值、策略版本、原因码)。runtime 只对本次派单应用该模型(覆盖派单原有的 model 取值,不修改执行器保存的默认配置),并在开始执行时把 `dispatchId`、选中模型与原因写入 daemon 日志,便于回溯某个任务因何被升级到另一个模型。该能力对执行机用户无新增参数或环境变量。
39
+
38
40
  daemon 还会响应服务端的 `CONVERSATION_COMMANDS_PROBE` 帧,用独立会话抓取该会话工作目录下可用的斜杠命令,回传 `CONVERSATION_COMMANDS_RESULT` 帧,让会话面板在用户输入 `/` 时列出真实可用的命令。该探测与回合执行完全隔离:任何失败只产生 `FAILED` / `TIMEOUT` 帧,不影响进行中的会话;provider 后端不支持抓取时同样降级为 `FAILED` 帧。帧里的 `executorId` 与本机不一致或非正数时直接忽略,避免在别的执行器工作区里建目录或启动 CLI。daemon 侧给整个探测 25 秒硬上限,刻意长于 agent 侧的 20 秒预算,使 agent 自己的超时先触发并可被归类。
39
41
 
40
42
  给 `connect` 追加 `--token-aware-enable` 可开启 qoder / qodercn 的 token 用量采集:CLI 会以 sidecar 方式启动本地 HTTPS 转发代理(`bin/token-proxy.js`),从 Qoder API 的 SSE 响应流中提取 usage 数据并按 dispatch ID 归属上报。usage 解析失败只丢失计量数据;代理启动失败时仅告警并继续。启用后模型流量经过代理,连接失败由客户端处理重试,不保证自动直连。代理端口可用 `TOKEN_PROXY_PORT` 环境变量调整(默认 19876);CA 证书位于 `~/.autowonder/token-tap/ca-cert.pem`。daemon 退出时 sidecar 一并停止。代理对 TCP 建连和两端 TLS 握手设置 15 秒期限,对无读写活动的连接设置 120 秒空闲期限;超时、报错或任一端关闭时释放两端连接。持续有数据的 SSE 流不受总时长限制,连续静默超过 120 秒的流会断开。
@@ -158,7 +160,7 @@ mv "$queue/.assignment.tmp" "$queue/assignment.json"
158
160
  也可以直接提交到本地 API:
159
161
 
160
162
  ```bash
161
- npx -y autowonder@0.3.8 dispatch ./assignment.json
163
+ npx -y autowonder@0.3.10 dispatch ./assignment.json
162
164
  ```
163
165
 
164
166
  ## 上传工单附件
@@ -166,7 +168,7 @@ npx -y autowonder@0.3.8 dispatch ./assignment.json
166
168
  把本地需求/设计文档直接上传到工单,避免在会话里传递大文件:
167
169
 
168
170
  ```bash
169
- npx -y autowonder@0.3.8 workitem upload --server-url <autowonder-server-url> --workitem-id <id> --file <filepath-1> --file <filepath-2> --json
171
+ npx -y autowonder@0.3.10 workitem upload --server-url <autowonder-server-url> --workitem-id <id> --file <filepath-1> --file <filepath-2> --json
170
172
  ```
171
173
 
172
174
  上传令牌通过 MCP 工具 `autowonder.workitem_cli_upload_token` 签发,经 `--token` 或 `AUTOWONDER_UPLOAD_TOKEN` 环境变量传入。`--file` 可重复指定多个文件;`--json` 输出机器可读结果。失败时按错误类型返回不同退出码(401→3、403→4、404→5、409→6、413→7、其他 4xx→8、网络/重定向→9)。
@@ -176,7 +178,7 @@ npx -y autowonder@0.3.8 workitem upload --server-url <autowonder-server-url> --w
176
178
  把工单上已有的需求/设计文档下载到本地目录,再用本地文件工具阅读,避免把大文件或二进制附件搬进会话上下文:
177
179
 
178
180
  ```bash
179
- npx -y autowonder@0.3.8 workitem download --server-url <autowonder-server-url> --workitem-id <id> --file <name-or-id> --output-dir <dir> --json
181
+ npx -y autowonder@0.3.10 workitem download --server-url <autowonder-server-url> --workitem-id <id> --file <name-or-id> --output-dir <dir> --json
180
182
  ```
181
183
 
182
184
  下载令牌通过 MCP 工具 `autowonder.workitem_cli_download_token` 签发(与上传令牌相互独立),经 `--token` 或 `AUTOWONDER_DOWNLOAD_TOKEN` 环境变量传入。`--file` 可重复指定文档名或 artifact id,省略则下载该工单的全部附件;`--output-dir` 默认为当前目录;`--json` 输出机器可读结果,含每个文件的 `id`、`name`、`path`、`size`。退出码约定与 `workitem upload` 相同(401→3、403→4、404→5、409→6、413→7、其他 4xx→8、网络/重定向→9)。
@@ -186,7 +188,7 @@ npx -y autowonder@0.3.8 workitem download --server-url <autowonder-server-url> -
186
188
  把需求/设计文档上传到 7x24 定时任务,用法与工单附件一致(同一套上传令牌与预检规则):
187
189
 
188
190
  ```bash
189
- npx -y autowonder@0.3.8 scheduled-task upload --server-url <autowonder-server-url> --scheduled-task-id <id> --file <filepath-1> --file <filepath-2> --json
191
+ npx -y autowonder@0.3.10 scheduled-task upload --server-url <autowonder-server-url> --scheduled-task-id <id> --file <filepath-1> --file <filepath-2> --json
190
192
  ```
191
193
 
192
194
  上传令牌同样通过 `autowonder.workitem_cli_upload_token` 签发(令牌为用户级,对当前可修改的任意定时任务生效),经 `--token` 或 `AUTOWONDER_UPLOAD_TOKEN` 传入;退出码约定与 `workitem upload` 相同。
@@ -194,8 +196,8 @@ npx -y autowonder@0.3.8 scheduled-task upload --server-url <autowonder-server-ur
194
196
  ## 管理 daemon
195
197
 
196
198
  ```bash
197
- npx -y autowonder@0.3.8 status
198
- npx -y autowonder@0.3.8 stop
199
+ npx -y autowonder@0.3.10 status
200
+ npx -y autowonder@0.3.10 stop
199
201
  ```
200
202
 
201
203
  默认 API 是 `http://127.0.0.1:34989`,日志位于 `~/.autowonder/daemon.log`。npm 包不包含任何 agent、MCP 或服务端凭证。
@@ -203,7 +205,7 @@ npx -y autowonder@0.3.8 stop
203
205
  ## 调试
204
206
 
205
207
  ```bash
206
- npx -y autowonder@0.3.8 connect ... --debug
208
+ npx -y autowonder@0.3.10 connect ... --debug
207
209
  ```
208
210
 
209
211
  `--debug` 会让 CLI 以 `AUTOWONDER_DEBUG=1` 启动 daemon,把完整的 daemon 与 agent 活动(runtime 事件流、provider 事件解码、重试退避与会话门等待、空闲时的阻塞点)同时输出到控制台和会话调试日志。调试日志不做脱敏,仅用于本机排障;不传 `--debug` 时该环境变量不会注入。
package/bin/cli.js CHANGED
@@ -1051,10 +1051,14 @@ function parseFlags(args, knownFlags) {
1051
1051
 
1052
1052
  function ensureWindowsUTF8() {
1053
1053
  if (process.platform !== "win32") return;
1054
- const result = spawnSync("chcp", [], { shell: true, encoding: "utf8", timeout: 2000 });
1054
+ // chcp is a .com console tool and CreateProcess resolves no PATHEXT, so it is
1055
+ // addressed by full path. shell:true would join the code page into a cmd.exe
1056
+ // command line instead, which is what emits Node's DEP0190 warning.
1057
+ const chcp = path.join(process.env.SystemRoot || "C:\\Windows", "System32", "chcp.com");
1058
+ const result = spawnSync(chcp, [], { encoding: "utf8", timeout: 2000 });
1055
1059
  const currentCP = (result.stdout || "").match(/:\s*(\d+)/)?.[1];
1056
1060
  if (currentCP !== "65001") {
1057
- spawnSync("chcp", ["65001"], { shell: true, stdio: "ignore" });
1061
+ spawnSync(chcp, ["65001"], { stdio: "ignore" });
1058
1062
  }
1059
1063
  process.env.PYTHONIOENCODING = process.env.PYTHONIOENCODING || "utf-8";
1060
1064
  process.env.PYTHONUTF8 = process.env.PYTHONUTF8 || "1";
@@ -340,23 +340,136 @@ function installQoderCLIUnlocked(provider) {
340
340
  const MACHINE_ID_REL_PATH = path.join(".auth", "machine_id");
341
341
  const CREDENTIAL_REL_PATH = path.join(".auth", "user");
342
342
 
343
- function qoderBinaryVersion(executable, options = {}) {
344
- if ((path.isAbsolute(executable) || executable.includes(path.sep)) && !fs.existsSync(executable)) return "missing";
343
+ // Both tree runners report a timeout as exit code 124, so a bound probe stays
344
+ // distinguishable from a CLI that really failed to start.
345
+ const QODER_PROBE_TIMEOUT_STATUS = 124;
346
+ const QODER_PROBE_OUTPUT_LIMIT = 1000;
347
+
348
+ function classifyQoderVersionProbe(result) {
349
+ if (result.error) {
350
+ if (result.error.code === "ENOENT") return "missing";
351
+ if (result.error.code === "ETIMEDOUT") return "timeout";
352
+ return "spawn_failed";
353
+ }
354
+ if (result.signal) return "failed";
355
+ if (result.status === null || result.status === undefined) return "timeout";
356
+ if (result.status === QODER_PROBE_TIMEOUT_STATUS) return "timeout";
357
+ if (result.status !== 0) return "failed";
358
+ return "unparseable";
359
+ }
360
+
361
+ // The probe keeps the raw evidence of the invocation so a permanent failure can
362
+ // be told apart from a transient one, and so the operator sees exit code,
363
+ // signal, spawn error and output instead of a single opaque "unknown".
364
+ function probeQoderBinaryVersion(executable, options = {}) {
365
+ const absent = (path.isAbsolute(executable) || executable.includes(path.sep)) && !fs.existsSync(executable);
366
+ const probe = {
367
+ executable,
368
+ version: absent ? "missing" : "unknown",
369
+ outcome: absent ? "missing" : "failed",
370
+ status: null,
371
+ signal: null,
372
+ spawnError: "",
373
+ stdout: "",
374
+ stderr: "",
375
+ };
376
+ if (absent) return probe;
345
377
  const result = spawnQoderSync(executable, ["--version"], {
346
378
  encoding: "utf8",
347
379
  stdio: ["ignore", "pipe", "pipe"],
348
380
  timeout: options.timeout || 20000,
349
381
  treeTimeout: true,
350
382
  });
383
+ probe.status = result.status === undefined ? null : result.status;
384
+ probe.signal = result.signal || null;
385
+ probe.spawnError = result.error
386
+ ? `${result.error.code ? `${result.error.code}: ` : ""}${result.error.message}`
387
+ : "";
388
+ probe.stdout = result.stdout || "";
389
+ probe.stderr = result.stderr || "";
390
+ probe.outcome = classifyQoderVersionProbe(result);
351
391
  // "missing" means the file itself is absent; "unknown" means the file is
352
392
  // there but produced no version, so the two failure classes never collapse
353
393
  // into one misleading answer.
354
- if (result.error && result.error.code === "ENOENT") return "missing";
355
- if (result.status !== 0) return "unknown";
356
- // Accept only an unambiguous Qoder version line. Runtime warnings and update
357
- // banners often contain unrelated/newer semvers and must never satisfy the
358
- // minimum gate for an old executable.
359
- return qoderVersionLine(`${result.stdout || ""}\n${result.stderr || ""}`) || "unknown";
394
+ if (probe.outcome === "missing") probe.version = "missing";
395
+ // Accept only an unambiguous Qoder version line, and only from a probe that
396
+ // exited zero. Runtime warnings and update banners often contain
397
+ // unrelated/newer semvers and must never satisfy the minimum gate for an old
398
+ // executable.
399
+ if (probe.outcome === "unparseable") {
400
+ const line = qoderVersionLine(`${probe.stdout}\n${probe.stderr}`);
401
+ if (line) {
402
+ probe.version = line;
403
+ probe.outcome = "ok";
404
+ }
405
+ }
406
+ return probe;
407
+ }
408
+
409
+ function qoderBinaryVersion(executable, options = {}) {
410
+ return probeQoderBinaryVersion(executable, options).version;
411
+ }
412
+
413
+ // "missing" is a definitive filesystem answer. Every other non-ok outcome can
414
+ // be a one-off glitch (first run, a short-lived file lock, a slow antivirus),
415
+ // which is exactly the field failure this retry exists for.
416
+ function isTransientQoderVersionProbe(probe) {
417
+ return probe.outcome !== "ok" && probe.outcome !== "missing";
418
+ }
419
+
420
+ // A single probe must not be trusted on its own: a CLI that is already up to
421
+ // date can answer nothing while its installation settles, and a just-updated
422
+ // CLI can need a moment before it reports the version it just installed.
423
+ // Re-probe on this bounded schedule instead of acting on one transient answer.
424
+ const QODER_VERSION_RETRY_DELAYS_MS = [300, 1000, 2000];
425
+
426
+ function qoderVersionRetryDelays() {
427
+ if (process.env.NODE_ENV === "test" && process.env.AUTOWONDER_TEST_QODER_VERSION_RETRY_DELAY_MS) {
428
+ const override = Number.parseInt(process.env.AUTOWONDER_TEST_QODER_VERSION_RETRY_DELAY_MS, 10);
429
+ if (Number.isFinite(override) && override >= 0) return QODER_VERSION_RETRY_DELAYS_MS.map(() => override);
430
+ }
431
+ return QODER_VERSION_RETRY_DELAYS_MS;
432
+ }
433
+
434
+ // The bootstrap runs before any event-loop work, so the wait blocks exactly
435
+ // like the update lock's own queue does.
436
+ function sleepMs(milliseconds) {
437
+ if (milliseconds > 0) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
438
+ }
439
+
440
+ function describeQoderVersionProbe(probe) {
441
+ const status = probe.status === null ? "none" : probe.status;
442
+ return `version ${probe.version}, outcome ${probe.outcome}, exit code ${status}, signal ${probe.signal || "none"}, spawn error ${probe.spawnError || "none"}`;
443
+ }
444
+
445
+ // A chatty or hostile CLI must not be able to flood the operator's log.
446
+ function boundedQoderProbeOutput(text) {
447
+ const value = String(text || "").replace(/\s+/g, " ").trim();
448
+ if (!value) return "<empty>";
449
+ if (value.length <= QODER_PROBE_OUTPUT_LIMIT) return value;
450
+ return `${value.slice(0, QODER_PROBE_OUTPUT_LIMIT)}... (${value.length} characters total)`;
451
+ }
452
+
453
+ function reportQoderVersionProbe(probe) {
454
+ error(`probe outcome : ${probe.outcome}`);
455
+ error(`probe exit code : ${probe.status === null ? "none" : probe.status}`);
456
+ error(`probe signal : ${probe.signal || "none"}`);
457
+ error(`probe spawn err : ${probe.spawnError || "none"}`);
458
+ error(`probe stdout : ${boundedQoderProbeOutput(probe.stdout)}`);
459
+ error(`probe stderr : ${boundedQoderProbeOutput(probe.stderr)}`);
460
+ }
461
+
462
+ // Re-probes until `accept` is satisfied or the bounded schedule is spent,
463
+ // reusing `initialProbe` when the caller has already probed once.
464
+ function probeQoderVersionUntil(profile, executable, accept, initialProbe) {
465
+ let probe = initialProbe || probeQoderBinaryVersion(executable);
466
+ const delays = qoderVersionRetryDelays();
467
+ for (let step = 0; step < delays.length && !accept(probe); step += 1) {
468
+ log(`${profile.displayName} version probe did not settle (${describeQoderVersionProbe(probe)}); re-probing in ${delays[step]}ms...`);
469
+ sleepMs(delays[step]);
470
+ probe = probeQoderBinaryVersion(executable);
471
+ }
472
+ return probe;
360
473
  }
361
474
 
362
475
  function qoderVersionLine(output) {
@@ -568,10 +681,16 @@ function ensureMinimumQoderVersion(profile, provider, executable) {
568
681
  }
569
682
 
570
683
  function ensureMinimumQoderVersionLocked(profile, provider, executable) {
571
- let version = qoderBinaryVersion(executable);
572
- if (qoderVersionMeetsMinimum(version)) return { ok: true, version };
684
+ let probe = probeQoderBinaryVersion(executable);
685
+ if (isTransientQoderVersionProbe(probe)) {
686
+ // An up-to-date CLI answers nothing far more often than it is really too
687
+ // old, so confirm the failure before paying for a download.
688
+ log(`${profile.displayName} did not report a version on the first probe (${describeQoderVersionProbe(probe)}); re-probing before updating...`);
689
+ probe = probeQoderVersionUntil(profile, executable, (candidate) => candidate.outcome === "ok", probe);
690
+ }
691
+ if (qoderVersionMeetsMinimum(probe.version)) return { ok: true, version: probe.version };
573
692
 
574
- log(`${profile.displayName} ${version} is older than required version 1.1.50 (or could not be parsed). Updating the current installation to latest...`);
693
+ log(`${profile.displayName} ${probe.version} is older than required version 1.1.50 (or could not be parsed). Updating the current installation to latest...`);
575
694
  const update = spawnQoderSync(executable, ["update"], { stdio: "inherit", timeout: 5 * 60 * 1000, treeTimeout: true });
576
695
  if (update.status !== 0) {
577
696
  const managedDir = qoderPaths(profile).managedDir;
@@ -579,22 +698,26 @@ function ensureMinimumQoderVersionLocked(profile, provider, executable) {
579
698
  const reinstalled = installQoderCLI(provider, { lockHeld: true });
580
699
  if (reinstalled) {
581
700
  executable = reinstalled;
582
- version = qoderBinaryVersion(executable);
583
- if (qoderVersionMeetsMinimum(version)) return { ok: true, version, executable };
701
+ probe = probeQoderVersionUntil(profile, executable, (candidate) => qoderVersionMeetsMinimum(candidate.version));
702
+ if (qoderVersionMeetsMinimum(probe.version)) return { ok: true, version: probe.version, executable };
584
703
  }
585
704
  }
586
705
  const detail = update.error?.message || (update.signal ? `signal ${update.signal}` : `exit status ${update.status}`);
587
706
  error(`${profile.displayName} requires version 1.1.50 or newer, but updating ${executable} failed: ${detail}`);
588
- return { ok: false, version, executable };
707
+ reportQoderVersionProbe(probe);
708
+ return { ok: false, version: probe.version, executable };
589
709
  }
590
710
 
591
- version = qoderBinaryVersion(executable);
592
- if (!qoderVersionMeetsMinimum(version)) {
593
- error(`${profile.displayName} requires version 1.1.50 or newer, but ${executable} reports ${version} after updating to latest.`);
594
- return { ok: false, version, executable };
711
+ // The update reported success; give the installation the bounded schedule to
712
+ // start answering with the version it just installed.
713
+ probe = probeQoderVersionUntil(profile, executable, (candidate) => qoderVersionMeetsMinimum(candidate.version));
714
+ if (!qoderVersionMeetsMinimum(probe.version)) {
715
+ error(`${profile.displayName} requires version 1.1.50 or newer, but ${executable} reports ${probe.version} after updating to latest.`);
716
+ reportQoderVersionProbe(probe);
717
+ return { ok: false, version: probe.version, executable };
595
718
  }
596
- log(`${profile.displayName} updated in place to ${version}.`);
597
- return { ok: true, version, executable };
719
+ log(`${profile.displayName} updated in place to ${probe.version}.`);
720
+ return { ok: true, version: probe.version, executable };
598
721
  }
599
722
 
600
723
  // A missing or unreadable record means "no history", never a bootstrap failure.
@@ -1088,6 +1211,8 @@ async function ensureQoderReady(provider, options = {}) {
1088
1211
 
1089
1212
  module.exports = {
1090
1213
  QODER_FAMILY_PROFILES,
1214
+ QODER_VERSION_RETRY_DELAYS_MS,
1215
+ classifyQoderVersionProbe,
1091
1216
  ensureMinimumQoderVersion,
1092
1217
  ensureQoderReady,
1093
1218
  findInInstallDirs,
@@ -1096,6 +1221,7 @@ module.exports = {
1096
1221
  inspectQoderHealth,
1097
1222
  installQoderCLIUnlocked,
1098
1223
  isQoderFamilyProvider,
1224
+ probeQoderBinaryVersion,
1099
1225
  qoderBinaryVersion,
1100
1226
  qoderChildEnv,
1101
1227
  qoderUpdateLockPaths,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autowonder",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "description": "AutoWonder local runtime — execute AI agent dispatch packages on your machine",
5
5
  "bin": {
6
6
  "autowonder": "bin/cli.js"