autowonder 0.3.18 → 0.3.20

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.18 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.20 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.18 connect --ws-url <wss-endpoint> --token <executor-token> --executor-id <executor-id> --provider qodercn
12
+ npx -y autowonder@0.3.20 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 登录状态。
@@ -27,7 +27,7 @@ runtime 启动时始终只拉取一次 Platform 环境变量快照,即使已
27
27
 
28
28
  出于安全考虑,命令行 token 可能出现在 shell 历史和操作系统进程列表中,建议优先使用环境变量或 Platform 绑定。
29
29
 
30
- `connect` 和 `start` 都要求 Qoder CLI / Qoder CLI CN 版本至少为 `1.1.65`。版本探测保留退出码、信号与有界输出作为证据;单次探测没有拿到版本(超时、启动失败或输出无法解析)时先按有界节奏重试,确认确实不满足后才对当前优先选中的用户 CLI 执行内置 `update`,原位升级到最新版并再次强校验;升级失败或升级后仍不满足最低版本时拒绝启动 daemon,并在日志中打印探测证据。所有 Qoder 系列子进程(普通任务、conversation、warm 会话、恢复回退、命令与模型探测)都会强制注入固定的 `QODER_INTEGRATION_ID`,调用方环境不能覆盖该流量归因标识。
30
+ `connect` 和 `start` 都要求 Qoder CLI / Qoder CLI CN 版本至少为 `1.1.65`。检测到旧版或无法解析的版本时,runtime 会对当前优先选中的用户 CLI 执行内置 `update`,原位升级到最新版并再次强校验;升级失败或升级后仍不满足最低版本时拒绝启动 daemon。所有 Qoder 系列子进程(普通任务、conversation、warm 会话、恢复回退、命令与模型探测)都会强制注入固定的 `QODER_INTEGRATION_ID`,调用方环境不能覆盖该流量归因标识。
31
31
 
32
32
  `qodercn` 是国内版 Qoder CLI。runtime 会在缺少 `qodercn` 时自动把 `@qodercn-ai/qoderclicn` 安装到 `~/.autowonder/qoder-cn-cli`,并在尚未登录时打开浏览器登录。它使用自己的配置根(`QODERCN_CONFIG_DIR`,默认 `~/.qoder-cn`),与国际版 `qoder` 不共享可执行文件、凭证和会话。可用 `AUTOWONDER_QODERCN_CLI` 指定自定义可执行文件路径。
33
33
 
@@ -65,16 +65,18 @@ Qoder 系列执行器的跨派单记忆由平台托管。服务端在派单里
65
65
 
66
66
  ## 工作区保留与 Git 对象复用
67
67
 
68
- daemon 默认启用本地工作区 GC:启动 30 秒后开始检查,之后每小时执行有限量的后台清理。非会话任务仅在终态且最后活动时间超过所配置的任务 TTL(默认 72 小时)后才符合清理条件;会话目录仅在最后活动时间超过所配置的会话 TTL(默认 720 小时)后才符合条件。清理的单位是整个 attempt capsule 或整个 conversation capsule,不会只删除其中部分文件。未知或格式异常、非终态(包括 uploading)、仍在运行、由恢复流程持有、结果仍待确认,或结果状态查询失败的 capsule 都会保留。
68
+ daemon 默认启用本地工作区 GC:启动 30 秒后开始检查,之后每 15 分钟开始一次检查。尚有健康的分片工作时每 250 毫秒继续,错误仍按退避重试;每次操作保持有限的节点和时间预算。非会话任务在终态且最后活动时间超过所配置的任务 TTL(默认 48 小时)后符合清理条件;会话目录的 TTL 仍为 720 小时。清理单位是整个 attempt capsule 或 conversation capsule。未知或格式异常、非终态(包括 uploading)、仍在运行、由恢复流程持有、结果仍待确认,或结果状态查询失败的 capsule 都会保留。只有 package 缓存、没有可信终态证据的历史目录不会按目录年龄删除,恢复扫描也不会为它们凭空创建 assigned 状态。
69
69
 
70
- 磁盘使用率达到 85% 或可用空间低于 10 GiB 时进入压力模式,恢复目标是使用率不高于 75% 且可用空间至少 15 GiB。压力只会调整有限量清理工作的调度和顺序;它不会放宽所配置的任务/会话 TTL(默认 72/720 小时),也不会让较新的 capsule 提前被删除。
70
+ 磁盘使用率达到 85% 或可用空间低于 10 GiB 时进入压力模式,恢复目标是使用率不高于 75% 且可用空间至少 15 GiB。压力模式还允许清理未满 TTL 的 completed attempt;符合条件的 attempt 按最后活动时间从旧到新清理。近期 failed/cancelled 和会话目录的 TTL 不变。每次实际删除后检查恢复目标,隔离重命名不算释放空间。删除前不为统计额外遍历目录;文件数和逻辑字节估算直接从删除操作取得。
71
+
72
+ 工作区 GC 另写独立审计日志 `~/.autowonder/gc/gc_exector_<executorId>.log`(文件名单词拼写如此),每文件上限 50 MiB、共保留 5 个文件,不改变下面托管日志的保留设置。
71
73
 
72
74
  相关环境变量及默认值如下:
73
75
 
74
76
  - `AUTOWONDER_WORKSPACE_GC_ENABLED=true`
75
77
  - `AUTOWONDER_WORKSPACE_GC_INITIAL_DELAY=30s`
76
- - `AUTOWONDER_WORKSPACE_GC_INTERVAL=1h`
77
- - `AUTOWONDER_WORKSPACE_GC_TASK_TTL=72h`
78
+ - `AUTOWONDER_WORKSPACE_GC_INTERVAL=15m`
79
+ - `AUTOWONDER_WORKSPACE_GC_TASK_TTL=48h`
78
80
  - `AUTOWONDER_WORKSPACE_GC_CONVERSATION_TTL=720h`
79
81
  - `AUTOWONDER_WORKSPACE_GC_HIGH_WATERMARK_PERCENT=85`
80
82
  - `AUTOWONDER_WORKSPACE_GC_LOW_WATERMARK_PERCENT=75`
@@ -173,7 +175,7 @@ mv "$queue/.assignment.tmp" "$queue/assignment.json"
173
175
  也可以直接提交到本地 API:
174
176
 
175
177
  ```bash
176
- npx -y autowonder@0.3.18 dispatch ./assignment.json
178
+ npx -y autowonder@0.3.20 dispatch ./assignment.json
177
179
  ```
178
180
 
179
181
  ## 上传工单附件
@@ -181,7 +183,7 @@ npx -y autowonder@0.3.18 dispatch ./assignment.json
181
183
  把本地需求/设计文档直接上传到工单,避免在会话里传递大文件:
182
184
 
183
185
  ```bash
184
- npx -y autowonder@0.3.18 workitem upload --server-url <autowonder-server-url> --workitem-id <id> --file <filepath-1> --file <filepath-2> --json
186
+ npx -y autowonder@0.3.20 workitem upload --server-url <autowonder-server-url> --workitem-id <id> --file <filepath-1> --file <filepath-2> --json
185
187
  ```
186
188
 
187
189
  上传令牌通过 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)。
@@ -191,7 +193,7 @@ npx -y autowonder@0.3.18 workitem upload --server-url <autowonder-server-url> --
191
193
  把工单上已有的需求/设计文档下载到本地目录,再用本地文件工具阅读,避免把大文件或二进制附件搬进会话上下文:
192
194
 
193
195
  ```bash
194
- npx -y autowonder@0.3.18 workitem download --server-url <autowonder-server-url> --workitem-id <id> --file <name-or-id> --output-dir <dir> --json
196
+ npx -y autowonder@0.3.20 workitem download --server-url <autowonder-server-url> --workitem-id <id> --file <name-or-id> --output-dir <dir> --json
195
197
  ```
196
198
 
197
199
  下载令牌通过 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)。
@@ -201,7 +203,7 @@ npx -y autowonder@0.3.18 workitem download --server-url <autowonder-server-url>
201
203
  把需求/设计文档上传到 7x24 定时任务,用法与工单附件一致(同一套上传令牌与预检规则):
202
204
 
203
205
  ```bash
204
- npx -y autowonder@0.3.18 scheduled-task upload --server-url <autowonder-server-url> --scheduled-task-id <id> --file <filepath-1> --file <filepath-2> --json
206
+ npx -y autowonder@0.3.20 scheduled-task upload --server-url <autowonder-server-url> --scheduled-task-id <id> --file <filepath-1> --file <filepath-2> --json
205
207
  ```
206
208
 
207
209
  上传令牌同样通过 `autowonder.workitem_cli_upload_token` 签发(令牌为用户级,对当前可修改的任意定时任务生效),经 `--token` 或 `AUTOWONDER_UPLOAD_TOKEN` 传入;退出码约定与 `workitem upload` 相同。
@@ -209,16 +211,31 @@ npx -y autowonder@0.3.18 scheduled-task upload --server-url <autowonder-server-u
209
211
  ## 管理 daemon
210
212
 
211
213
  ```bash
212
- npx -y autowonder@0.3.18 status
213
- npx -y autowonder@0.3.18 stop
214
+ npx -y autowonder@0.3.20 status
215
+ npx -y autowonder@0.3.20 stop
214
216
  ```
215
217
 
216
218
  默认 API 是 `http://127.0.0.1:34989`,日志位于 `~/.autowonder/daemon.log`。npm 包不包含任何 agent、MCP 或服务端凭证。
217
219
 
220
+ ## 日志轮转与保留
221
+
222
+ daemon 会话日志、`cli start` 重定向的 `~/.autowonder/daemon.log`、service 的 stdout/stderr 日志以及 token-proxy 诊断日志都会按大小与日期轮转,且每类日志只有一个轮转写入方。轮转采用改名后重开同一路径的方式,不会原地截断,也不会删除仍被其他进程打开的文件;改名失败时继续写当前文件,单次失败不会关闭日志。
223
+
224
+ daemon 内置独立于工作区 GC 的保留清扫:默认每小时运行一次,删除修改时间超过 TTL(默认 72h)的已关闭日志。运行中写入方的活动文件始终排除,单个文件处理失败只记录并继续,符号链接与目录不会被跟随。任务 capsule 内的日志、token 计量数据、CA 证书与 `config.json` 不属于托管日志,不会被自动删除。
225
+
226
+ 可用环境变量调整:
227
+
228
+ - `AUTOWONDER_LOG_MAX_BYTES=104857600`(100 MiB):托管日志的轮转阈值
229
+ - `AUTOWONDER_LOG_RETENTION=72h`:保留 TTL,显式设为 `0` 关闭清扫
230
+ - `AUTOWONDER_LOG_SWEEP_INTERVAL=1h`:清扫周期
231
+ - `AUTOWONDER_RAW_LOG_MAX_BYTES=104857600`(100 MiB):`AUTOWONDER_DEBUG=1` 时 token-tap 原始响应抓包的单文件上限,达到上限后停止写入,但不影响隧道继续转发
232
+
233
+ 取值格式非法时回落到默认值。
234
+
218
235
  ## 调试
219
236
 
220
237
  ```bash
221
- npx -y autowonder@0.3.18 connect ... --debug
238
+ npx -y autowonder@0.3.20 connect ... --debug
222
239
  ```
223
240
 
224
241
  `--debug` 会让 CLI 以 `AUTOWONDER_DEBUG=1` 启动 daemon,把完整的 daemon 与 agent 活动(runtime 事件流、provider 事件解码、重试退避与会话门等待、空闲时的阻塞点)同时输出到控制台和会话调试日志。调试日志不做脱敏,仅用于本机排障;不传 `--debug` 时该环境变量不会注入。
package/bin/cli.js CHANGED
@@ -512,15 +512,14 @@ async function cmdConnect(args) {
512
512
  const proxyPort = parseInt(process.env.TOKEN_PROXY_PORT || "19876", 10);
513
513
  const caCertPath = path.join(AUTOWONDER_HOME, "token-tap", "ca-cert.pem");
514
514
  try {
515
- const proxyLogPath = path.join(AUTOWONDER_HOME, "token-tap", "proxy-diag.log");
516
- const proxyStdio = flags.debug
517
- ? ["ignore", fs.openSync(proxyLogPath, "a"), fs.openSync(proxyLogPath, "a")]
518
- : ["ignore", "ignore", "ignore"];
515
+ // The proxy owns its own rotating diagnostic log, so the launcher keeps
516
+ // all three streams on /dev/null and never holds a second unbounded
517
+ // handle on proxy-diag.log.
519
518
  const proxyEnv = { ...process.env };
520
519
  if (flags.debug) proxyEnv.AUTOWONDER_DEBUG = "1";
521
520
  tokenProxyChild = spawn(process.execPath, [tokenProxyScript, "start", "--port", String(proxyPort)], {
522
521
  detached: true,
523
- stdio: proxyStdio,
522
+ stdio: ["ignore", "ignore", "ignore"],
524
523
  env: proxyEnv,
525
524
  });
526
525
  tokenProxyChild.unref();
@@ -537,6 +536,7 @@ async function cmdConnect(args) {
537
536
  // so the proxy can tag token records with the exact dispatch ID.
538
537
  env.AUTOWONDER_TOKEN_PROXY_PORT = String(proxyPort);
539
538
  env.AUTOWONDER_TOKEN_PROXY_CA = caCertPath;
539
+ env.AUTOWONDER_TOKEN_PROXY_DIAG_LOG = path.join(AUTOWONDER_HOME, "token-tap", "proxy-diag.log");
540
540
  env.NODE_EXTRA_CA_CERTS = caCertPath;
541
541
  log(`Token tracking enabled (proxy pid=${tokenProxyChild.pid}, port=${proxyPort})`);
542
542
  } else {
@@ -604,6 +604,9 @@ async function cmdStart(args) {
604
604
  AUTOWONDER_LOCAL_API_ADDR: addr,
605
605
  AUTOWONDER_MAX_CONCURRENT_DISPATCHES: maxTasks,
606
606
  AUTOWONDER_DAEMON_INSTANCE_ID: instanceId,
607
+ // The descriptor keeps the file open before the daemon can log anything;
608
+ // naming the path lets the daemon reopen it and own rotation from here on.
609
+ AUTOWONDER_DAEMON_LOG: LOG_PATH,
607
610
  },
608
611
  });
609
612
  fs.closeSync(logFd);
@@ -31,14 +31,23 @@ const os = require('os');
31
31
 
32
32
  const DEFAULT_PORT = 19876;
33
33
  const MAX_LOG_SIZE = 10 * 1024 * 1024; // 10MB, then rotate
34
+ const DEFAULT_LOG_MAX_BYTES = 100 * 1024 * 1024; // 100MiB, then rotate
35
+ const LOG_MAX_BYTES = intEnv('AUTOWONDER_LOG_MAX_BYTES', DEFAULT_LOG_MAX_BYTES);
36
+ const RAW_LOG_MAX_BYTES = intEnv('AUTOWONDER_RAW_LOG_MAX_BYTES', DEFAULT_LOG_MAX_BYTES);
34
37
  const DATA_DIR = path.join(os.homedir(), '.autowonder', 'token-tap');
35
38
  const CA_KEY_PATH = path.join(DATA_DIR, 'ca-key.pem');
36
39
  const CA_CERT_PATH = path.join(DATA_DIR, 'ca-cert.pem');
37
40
  const USAGE_LOG_PATH = path.join(DATA_DIR, 'token-usage.jsonl');
38
41
  const USAGE_LOG_OLD = path.join(DATA_DIR, 'token-usage.old.jsonl');
39
42
  const RAW_LOG_DIR = path.join(DATA_DIR, 'raw');
43
+ const DIAG_LOG_PATH = path.join(DATA_DIR, 'proxy-diag.log');
40
44
  const DEBUG = process.env.AUTOWONDER_DEBUG === '1';
41
45
 
46
+ function intEnv(name, fallback) {
47
+ const parsed = parseInt(process.env[name], 10);
48
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
49
+ }
50
+
42
51
  // Qoder/QoderCN LLM API hosts to sniff (others are tunneled without inspection).
43
52
  // Only LLM endpoints are listed — non-LLM APIs (openapi.*) are excluded to avoid
44
53
  // unnecessary MITM overhead on high-frequency config/auth calls.
@@ -50,6 +59,93 @@ const SNIFF_HOSTS = new Set([
50
59
  'api3.qodercn.com', 'api2.qodercn.com', 'api.qodercn.com',
51
60
  ]);
52
61
 
62
+ // ─── Managed log rotation (proxy-diag.log) ───────────────────────────────────
63
+
64
+ // Rename-then-reopen: the size trigger closes the live file, archives it under
65
+ // a name carrying the timestamp and pid, and reopens a fresh file at the same
66
+ // path. A live descriptor is never truncated or unlinked, so no diagnostic
67
+ // line is lost mid-write.
68
+ function createRotatingWriter(filePath, { maxBytes = DEFAULT_LOG_MAX_BYTES } = {}) {
69
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
70
+ let fd = fs.openSync(filePath, 'a', 0o600);
71
+ let size = fs.fstatSync(fd).size;
72
+
73
+ const pad = (n) => String(n).padStart(2, '0');
74
+ const archivePath = () => {
75
+ const now = new Date();
76
+ const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`
77
+ + `_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
78
+ const ext = path.extname(filePath);
79
+ const base = `${filePath.slice(0, filePath.length - ext.length)}.${stamp}_${process.pid}`;
80
+ for (let n = 0; n < 10000; n++) {
81
+ const candidate = n === 0 ? `${base}${ext}` : `${base}_${n}${ext}`;
82
+ if (!fs.existsSync(candidate)) return candidate;
83
+ }
84
+ return `${base}_${Date.now()}${ext}`;
85
+ };
86
+
87
+ return {
88
+ filePath,
89
+ get size() { return size; },
90
+ write(chunk) {
91
+ const data = Buffer.from(String(chunk), 'utf8');
92
+ try {
93
+ if (size > 0 && size + data.length > maxBytes) {
94
+ fs.closeSync(fd);
95
+ try {
96
+ fs.renameSync(filePath, archivePath());
97
+ } catch (_) {
98
+ // Archive failure keeps the live file in place; the incoming line
99
+ // still has to land somewhere.
100
+ }
101
+ fd = fs.openSync(filePath, 'a', 0o600);
102
+ size = fs.fstatSync(fd).size;
103
+ }
104
+ fs.writeSync(fd, data);
105
+ size += data.length;
106
+ return true;
107
+ } catch (_) {
108
+ return false;
109
+ }
110
+ },
111
+ };
112
+ }
113
+
114
+ // rawLogFileName keeps the pid in the name so two proxies writing into one raw
115
+ // directory can never collide on the same file.
116
+ function rawLogFileName(seq, hostname, now = new Date(), pid = process.pid) {
117
+ return `${now.toISOString().slice(0, 10)}_${pid}_${seq}_${hostname}.raw`;
118
+ }
119
+
120
+ // rawSliceFor returns the fragment of chunk still allowed under the per-file
121
+ // cap, or null once the cap is reached. Overflow is dropped here only; the
122
+ // tunnel itself keeps forwarding either way.
123
+ function rawSliceFor(chunk, written, cap = RAW_LOG_MAX_BYTES) {
124
+ if (written >= cap) return null;
125
+ const room = cap - written;
126
+ return chunk.length <= room ? chunk : chunk.subarray(0, room);
127
+ }
128
+
129
+ // installDiagLog routes the proxy's own console output into a rotating
130
+ // proxy-diag.log when AUTOWONDER_DEBUG is set. A TTY still echoes, so an
131
+ // interactive `token-proxy.js start` stays readable.
132
+ function installDiagLog(logPath = DIAG_LOG_PATH) {
133
+ if (!DEBUG) return null;
134
+ const { format } = require('util');
135
+ const writer = createRotatingWriter(logPath, { maxBytes: LOG_MAX_BYTES });
136
+ const originalStdout = console.log.bind(console);
137
+ const originalStderr = console.error.bind(console);
138
+ console.log = (...args) => {
139
+ writer.write(format(...args) + '\n');
140
+ if (process.stdout.isTTY) originalStdout(...args);
141
+ };
142
+ console.error = (...args) => {
143
+ writer.write(format(...args) + '\n');
144
+ if (process.stderr.isTTY) originalStderr(...args);
145
+ };
146
+ return writer;
147
+ }
148
+
53
149
  // ─── CA Certificate (auto-generated, only for this proxy) ────────────────────
54
150
 
55
151
  function ensureCA() {
@@ -425,13 +521,15 @@ function createProxyServer({ ca = ensureCA(), hostCertificate = getHostCert,
425
521
  let usageCount = 0;
426
522
  let totalBytes = 0;
427
523
  let rawFd = null;
524
+ let rawWritten = 0;
428
525
 
429
- // Raw response logging: only in debug mode to avoid disk bloat
526
+ // Raw response logging: only in debug mode to avoid disk bloat, and capped
527
+ // per file so one long-lived tunnel cannot fill the disk.
430
528
  if (DEBUG) {
431
529
  try {
432
530
  fs.mkdirSync(RAW_LOG_DIR, { recursive: true });
433
- const rawFile = path.join(RAW_LOG_DIR, `${new Date().toISOString().slice(0,10)}_${seq}_${hostname}.raw`);
434
- rawFd = fs.openSync(rawFile, 'w');
531
+ const rawFile = path.join(RAW_LOG_DIR, rawLogFileName(seq, hostname));
532
+ rawFd = fs.openSync(rawFile, 'w', 0o600);
435
533
  } catch (_) {}
436
534
  }
437
535
 
@@ -465,7 +563,15 @@ function createProxyServer({ ca = ensureCA(), hostCertificate = getHostCert,
465
563
 
466
564
  tlsUpstream.on('data', (chunk) => {
467
565
  totalBytes += chunk.length;
468
- if (rawFd !== null) { try { fs.writeSync(rawFd, chunk); } catch (_) {} }
566
+ if (rawFd !== null) {
567
+ const slice = rawSliceFor(chunk, rawWritten);
568
+ if (slice !== null) {
569
+ try {
570
+ fs.writeSync(rawFd, slice);
571
+ rawWritten += slice.length;
572
+ } catch (_) {}
573
+ }
574
+ }
469
575
  try {
470
576
  const text = lineRemainder + chunk.toString('utf8');
471
577
  const lastNL = text.lastIndexOf('\n');
@@ -508,6 +614,7 @@ function createProxyServer({ ca = ensureCA(), hostCertificate = getHostCert,
508
614
  }
509
615
 
510
616
  function startProxy(port) {
617
+ const diagWriter = installDiagLog();
511
618
  const server = createProxyServer();
512
619
  server.on('error', (err) => {
513
620
  console.error(`[token-proxy] server error: ${err.message}`);
@@ -519,6 +626,7 @@ function startProxy(port) {
519
626
  console.log(`[token-proxy] CA cert: ${CA_CERT_PATH}`);
520
627
  console.log(`[token-proxy] Usage log: ${USAGE_LOG_PATH}`);
521
628
  console.log(`[token-proxy] Sniffing hosts: ${[...SNIFF_HOSTS].join(', ')}`);
629
+ if (diagWriter) console.log(`[token-proxy] Diag log: ${diagWriter.filePath}`);
522
630
  });
523
631
 
524
632
  // Graceful shutdown
@@ -616,9 +724,10 @@ if (require.main === module) {
616
724
 
617
725
  Files:
618
726
  CA cert: ${CA_CERT_PATH}
619
- Usage log: ${USAGE_LOG_PATH}`);
727
+ Usage log: ${USAGE_LOG_PATH}
728
+ Diag log: ${DIAG_LOG_PATH} (written when AUTOWONDER_DEBUG=1)`);
620
729
  }
621
730
  }
622
731
 
623
732
  module.exports = { extractUsageFromResponse, extractDispatchInfo, buildUsageRecord,
624
- guardConnection, createProxyServer };
733
+ guardConnection, createProxyServer, createRotatingWriter, rawLogFileName, rawSliceFor, installDiagLog };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autowonder",
3
- "version": "0.3.18",
3
+ "version": "0.3.20",
4
4
  "description": "AutoWonder local runtime — execute AI agent dispatch packages on your machine",
5
5
  "bin": {
6
6
  "autowonder": "bin/cli.js"