@liwu1995/afu-context 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,22 +20,34 @@ Install and sign in to your host first. You need npm and Node.js **22.19+ (22.x)
20
20
 
21
21
  | Host | System | Install | Launch after installation |
22
22
  | --- | --- | --- | --- |
23
- | <a id="afu-claude"></a>Claude Code | macOS / Linux | `npx --yes @liwu1995/afu-context@next install claude` | `claude` |
24
- | Claude Code | Windows 10/11¹ | `npx.cmd --yes "@liwu1995/afu-context@next" install claude` | `claude` |
25
- | <a id="afu-codex"></a>Codex CLI | macOS / Linux | `npx --yes @liwu1995/afu-context@next install codex` | `codex` |
26
- | <a id="afu-pi"></a>Pi | macOS / Linux | `npx --yes @liwu1995/afu-context@next install pi` | `pi` |
27
- | <a id="afu-dsh"></a>DSH | macOS / Linux | `npx --yes @liwu1995/afu-context@next install dsh --profile YOUR_PROFILE` | `dsh --profile YOUR_PROFILE` |
23
+ | <a id="afu-claude"></a>Claude Code | macOS / Linux | `npx --yes --registry=https://registry.npmjs.org @liwu1995/afu-context@0.1.2 install claude` | `claude` |
24
+ | Claude Code | Windows 10/11¹ | `npx.cmd --yes --registry=https://registry.npmjs.org "@liwu1995/afu-context@0.1.2" install claude` | `claude` |
25
+ | <a id="afu-codex"></a>Codex CLI | macOS / Linux | `npx --yes --registry=https://registry.npmjs.org @liwu1995/afu-context@0.1.2 install codex` | `codex` |
26
+ | <a id="afu-pi"></a>Pi | macOS / Linux | `npx --yes --registry=https://registry.npmjs.org @liwu1995/afu-context@0.1.2 install pi` | `pi` |
27
+ | <a id="afu-dsh"></a>DSH | macOS / Linux | `npx --yes --registry=https://registry.npmjs.org @liwu1995/afu-context@0.1.2 install dsh --profile YOUR_PROFILE` | `dsh --profile YOUR_PROFILE` |
28
28
 
29
- The macOS packages target Apple Silicon (ARM64), and the Linux packages target x64. Both platforms have been tested and include Python. Start a new session for Claude Code or Codex. Pi loads the extension in new sessions. Replace `YOUR_PROFILE` with the DSH profile to integrate, such as `headless` or `web`. For web, you can also launch with `dsh web`. To remove just one profile, run `npx --yes @liwu1995/afu-context@next remove dsh --profile YOUR_PROFILE`.
29
+ The macOS packages target Apple Silicon (ARM64), and the Linux packages target x64, and both include Python. On macOS, Claude Code and Codex were tested end to end with real models (install, chat with compaction, upgrade, remove, launchd autostart). On Linux, Claude Code was tested with a real model and Codex with a local fixture; user-systemd autostart was not tested. Start a new session for Claude Code or Codex. Pi loads the extension in new sessions. Replace `YOUR_PROFILE` with the DSH profile to integrate, such as `headless` or `web`. For web, you can also launch with `dsh web`. To remove just one profile, run `npx --yes --registry=https://registry.npmjs.org @liwu1995/afu-context@0.1.2 remove dsh --profile YOUR_PROFILE`.
30
30
 
31
31
  ¹ On Windows, use PowerShell, Python **3.11+** and a Claude Code version with PowerShell hook support. Native Claude Code is implemented and awaits Windows testing. Native Codex is unsupported, and Pi and DSH are unverified on Windows.
32
32
 
33
+ The commands pin **0.1.2** from the public npm registry so installation does not depend on a mirror carrying the latest release. If npm reports `with a date before`, check `before` and `min-release-age`. This version was published at **PUBLISHED_AT**. Retry after a rolling cooldown expires. For a fixed cutoff, the effective policy must allow that publication date.
34
+
33
35
  <details>
34
36
  <summary>Linux manual startup and Pi / DSH launch options</summary>
35
37
 
36
38
  On Linux without user systemd, append `--no-autostart` to the Claude Code or Codex install command. Run it again after a reboot to start the gateway.
37
39
 
38
- For Pi **0.84.4**, pass `--provider` and `--model` on the Pi command line. DSH was tested with **0.1.2-rc.1** and Cordis **4.0.2**. If a web profile reports an incompatible HMR API, run `npx --yes @liwu1995/afu-context@next dsh --web-compat -- --profile web`.
40
+ For Pi **0.84.4**, pass `--provider` and `--model` on the Pi command line. DSH was tested with **0.1.2-rc.1** and Cordis **4.0.2**. If a web profile reports an incompatible HMR API, run `npx --yes --registry=https://registry.npmjs.org @liwu1995/afu-context@0.1.2 dsh --web-compat -- --profile web`.
41
+
42
+ </details>
43
+
44
+ <details>
45
+ <summary>Known issues and network notes</summary>
46
+
47
+ - npm prints `EBADENGINE` on Node.js 22.18 or older. The requirement is 22.19+ (22.x) or 24+; upgrade Node.js before installing.
48
+ - If the host reaches its model through a proxy, run the install command with the same `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` environment. The autostart service records these variables (and `SSL_CERT_FILE`, `NODE_EXTRA_CA_CERTS`) at install time; rerun the install after they change.
49
+ - Codex through AFU occasionally stalls for about three minutes before reconnecting. The cause is not yet confirmed.
50
+ - Removal restores your original file byte for byte when you made no other change. If you edited the file after installing, removal reverts only the AFU entries and keeps your edits.
39
51
 
40
52
  </details>
41
53
 
@@ -62,13 +74,13 @@ In a LongBench V2 replay of 39 history questions (20 agent histories and 19 dial
62
74
  For Codex, check the integration status, then open the dashboard and select your session. For another host, replace `codex` with `claude`, `pi` or `dsh`.
63
75
 
64
76
  ```sh
65
- npx --yes @liwu1995/afu-context@next status codex
66
- npx --yes @liwu1995/afu-context@next ui
77
+ npx --yes --registry=https://registry.npmjs.org @liwu1995/afu-context@0.1.2 status codex
78
+ npx --yes --registry=https://registry.npmjs.org @liwu1995/afu-context@0.1.2 ui
67
79
  ```
68
80
 
69
81
  The dashboard distinguishes **request observed → summary ready → summary applied**. For Claude Code and Codex, application requires a complete successful response to a request carrying the summary. For Pi and DSH it means the host committed the summary to session history. The dashboard also shows summary duration and provider-reported cache usage.
70
82
 
71
- Start in **Overview** for active sessions, summary duration and application counts. Open a **session** to follow a summary through its timeline to the application receipt and inspect reported cache usage. Open **Diagnostics** to investigate missing evidence, errors or runtime versions.
83
+ **Overview** shows what AFU saved you: estimated tokens saved, recorded conversation wait and how many compactions took effect, followed by your sessions and the state of their latest compaction. Open a **session** to see the context drop after each compaction, how long each one took and what it saved, plus cache hit rates. Integrations, versions and issues live on the **Status** page. The dashboard supports light/dark themes and English/Chinese.
72
84
 
73
85
  ## Models and thresholds
74
86
 
@@ -80,21 +92,21 @@ All four integrations reuse the conversation’s model and authentication. The d
80
92
  For a Codex model with a known 200,000-token window, the first command sets a 30% trigger. The second restores automatic settings.
81
93
 
82
94
  ```sh
83
- npx --yes @liwu1995/afu-context@next install codex --context-window 200000 --ratio 0.3
84
- npx --yes @liwu1995/afu-context@next install codex --context-window auto --ratio auto
95
+ npx --yes --registry=https://registry.npmjs.org @liwu1995/afu-context@0.1.2 install codex --context-window 200000 --ratio 0.3
96
+ npx --yes --registry=https://registry.npmjs.org @liwu1995/afu-context@0.1.2 install codex --context-window auto --ratio auto
85
97
  ```
86
98
 
87
99
  </details>
88
100
 
89
- For frequent use, run `npm install -g @liwu1995/afu-context@next` and use `afu` in place of the full `npx` invocation. For example, `afu ui` opens the dashboard, and `afu ui --prices ./afu-prices.json` uses your price file to estimate costs.
101
+ For frequent use, run `npm install -g --registry=https://registry.npmjs.org @liwu1995/afu-context@0.1.2` and use `afu` in place of the full `npx` invocation. For example, `afu ui` opens the dashboard, and `afu ui --prices ./afu-prices.json` uses your price file to estimate costs.
90
102
 
91
103
  ## Upgrade and remove
92
104
 
93
105
  After upgrading a Claude Code or Codex integration, start a new session to use the new gateway. To uninstall, remove the host integration first, then restart the host. Use the appropriate command below.
94
106
 
95
107
  ```sh
96
- npx --yes @liwu1995/afu-context@next upgrade codex
97
- npx --yes @liwu1995/afu-context@next remove codex
108
+ npx --yes --registry=https://registry.npmjs.org @liwu1995/afu-context@0.1.2 upgrade codex
109
+ npx --yes --registry=https://registry.npmjs.org @liwu1995/afu-context@0.1.2 remove codex
98
110
  ```
99
111
 
100
112
  Use `remove pi` or `remove dsh` for those integrations. In Windows PowerShell, use `npx.cmd` and double-quote the package name.
package/README.zh-CN.md CHANGED
@@ -20,22 +20,34 @@
20
20
 
21
21
  | 宿主 | 系统 | 一键安装 | 安装后启动 |
22
22
  | --- | --- | --- | --- |
23
- | <a id="afu-claude"></a>Claude Code | macOS / Linux | `npx --yes @liwu1995/afu-context@next install claude` | `claude` |
24
- | Claude Code | Windows 10/11¹ | `npx.cmd --yes "@liwu1995/afu-context@next" install claude` | `claude` |
25
- | <a id="afu-codex"></a>Codex CLI | macOS / Linux | `npx --yes @liwu1995/afu-context@next install codex` | `codex` |
26
- | <a id="afu-pi"></a>Pi | macOS / Linux | `npx --yes @liwu1995/afu-context@next install pi` | `pi` |
27
- | <a id="afu-dsh"></a>DSH | macOS / Linux | `npx --yes @liwu1995/afu-context@next install dsh --profile YOUR_PROFILE` | `dsh --profile YOUR_PROFILE` |
23
+ | <a id="afu-claude"></a>Claude Code | macOS / Linux | `npx --yes --registry=https://registry.npmjs.org @liwu1995/afu-context@0.1.2 install claude` | `claude` |
24
+ | Claude Code | Windows 10/11¹ | `npx.cmd --yes --registry=https://registry.npmjs.org "@liwu1995/afu-context@0.1.2" install claude` | `claude` |
25
+ | <a id="afu-codex"></a>Codex CLI | macOS / Linux | `npx --yes --registry=https://registry.npmjs.org @liwu1995/afu-context@0.1.2 install codex` | `codex` |
26
+ | <a id="afu-pi"></a>Pi | macOS / Linux | `npx --yes --registry=https://registry.npmjs.org @liwu1995/afu-context@0.1.2 install pi` | `pi` |
27
+ | <a id="afu-dsh"></a>DSH | macOS / Linux | `npx --yes --registry=https://registry.npmjs.org @liwu1995/afu-context@0.1.2 install dsh --profile YOUR_PROFILE` | `dsh --profile YOUR_PROFILE` |
28
28
 
29
- macOS 指 Apple Silicon(ARM64),Linux 指 x64。这两种平台均已验证,自带 Python 运行时。Claude Code 和 Codex 安装后新开会话。Pi 扩展在新开的会话自动加载。将 DSH 的 `YOUR_PROFILE` 换成要接入的 profile,例如 `headless` 或 `web`。选择 web 后也可以用 `dsh web` 启动。仅移除某个 profile 时运行 `npx --yes @liwu1995/afu-context@next remove dsh --profile YOUR_PROFILE`。
29
+ macOS 指 Apple Silicon(ARM64),Linux 指 x64,均自带 Python 运行时。macOS 上 Claude Code 和 Codex 已用真实模型完成端到端验证(安装、对话压缩、升级、移除、launchd 自启);Linux 上 Claude Code 用真实模型验证,Codex 用本地模拟服务验证,用户级 systemd 自启尚未验证。Claude Code 和 Codex 安装后新开会话。Pi 扩展在新开的会话自动加载。将 DSH 的 `YOUR_PROFILE` 换成要接入的 profile,例如 `headless` 或 `web`。选择 web 后也可以用 `dsh web` 启动。仅移除某个 profile 时运行 `npx --yes --registry=https://registry.npmjs.org @liwu1995/afu-context@0.1.2 remove dsh --profile YOUR_PROFILE`。
30
30
 
31
31
  ¹ Windows 使用 PowerShell,需本机 Python **3.11+** 和支持 PowerShell Hook 的 Claude Code。Windows 原生 Claude Code 已实现,待实机验收。Codex 未支持,Pi 和 DSH 未验收。
32
32
 
33
+ 安装命令固定使用公网 npm 上的 **0.1.2**,避免镜像尚未同步最新版本。如果报错含 `with a date before`,请核对 npm 的 `before` 或 `min-release-age`。此版本发布于 **PUBLISHED_AT**。若使用滚动冷却期,等该版本满足期限后重试;若设置了固定截止日期,需使用允许安装该版本的策略。
34
+
33
35
  <details>
34
36
  <summary>Linux 手动启动与 Pi / DSH 启动参数</summary>
35
37
 
36
38
  Linux 没有用户级 systemd 时,在 Claude Code 或 Codex 安装命令末尾加 `--no-autostart`,系统重启后重新执行同一命令启动网关。
37
39
 
38
- Pi **0.84.4** 可直接传 `--provider` 和 `--model`。DSH 已适配 **0.1.2-rc.1** 和 Cordis **4.0.2**。若 Web profile 提示 HMR API 不兼容,可使用 `npx --yes @liwu1995/afu-context@next dsh --web-compat -- --profile web`。
40
+ Pi **0.84.4** 可直接传 `--provider` 和 `--model`。DSH 已适配 **0.1.2-rc.1** 和 Cordis **4.0.2**。若 Web profile 提示 HMR API 不兼容,可使用 `npx --yes --registry=https://registry.npmjs.org @liwu1995/afu-context@0.1.2 dsh --web-compat -- --profile web`。
41
+
42
+ </details>
43
+
44
+ <details>
45
+ <summary>已知问题与网络说明</summary>
46
+
47
+ - Node.js 22.18 及更早版本安装时 npm 会提示 `EBADENGINE`。要求为 22.19+(22.x)或 24+,请先升级 Node.js。
48
+ - 宿主需要通过代理访问模型时,请在同样设置了 `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` 的终端执行安装命令。自启服务会在安装时记录这些变量(以及 `SSL_CERT_FILE`、`NODE_EXTRA_CA_CERTS`),变更后需重新执行安装。
49
+ - 经 AFU 使用 Codex 时,偶尔会停顿约三分钟后重连,原因尚未确认。
50
+ - 移除时若你没有改过配置文件,会逐字节恢复原文件;若安装后改过,只撤销 AFU 的条目,保留你的修改。
39
51
 
40
52
  </details>
41
53
 
@@ -62,13 +74,13 @@ Pi **0.84.4** 可直接传 `--provider` 和 `--model`。DSH 已适配 **0.1.2-rc
62
74
  以 Codex 为例,先看接入状态,再打开面板查看刚才的会话。其它宿主将 `codex` 换成 `claude`、`pi` 或 `dsh`。
63
75
 
64
76
  ```sh
65
- npx --yes @liwu1995/afu-context@next status codex
66
- npx --yes @liwu1995/afu-context@next ui
77
+ npx --yes --registry=https://registry.npmjs.org @liwu1995/afu-context@0.1.2 status codex
78
+ npx --yes --registry=https://registry.npmjs.org @liwu1995/afu-context@0.1.2 ui
67
79
  ```
68
80
 
69
81
  面板分别记录**请求已接入 → 摘要已生成 → 摘要已应用**。Claude Code 和 Codex 的应用回执表示替换历史后的请求已收到完整成功响应。Pi 和 DSH 的回执表示宿主已把摘要写入会话历史。面板也展示摘要耗时和模型返回的缓存用量。
70
82
 
71
- 在**总览**查看活跃会话、摘要生成耗时和应用数量。点进一个**会话**,沿时间线找到摘要及后续的应用回执,再看对应请求的缓存用量。遇到失败或证据缺失时,打开**高级诊断**查看错误原因与运行版本。
83
+ **总览**先告诉你 AFU 帮你省了多少:节省的 token(估算)、记录到的对话等待时间和生效的压缩次数,下方列出每个会话及其最近一次压缩的状态。点进一个**会话**,可以看到上下文在每次压缩后回落、每次压缩的耗时和节省量,以及缓存命中率。接入情况、版本和异常记录在**运行状态**页查看。面板支持亮色/暗色主题和中英文。
72
84
 
73
85
  ## 模型与压缩阈值
74
86
 
@@ -80,21 +92,21 @@ npx --yes @liwu1995/afu-context@next ui
80
92
  下面以已知窗口为 200,000 tokens 的 Codex 模型为例。第一条将触发比例设为 30%,第二条恢复自动设置。
81
93
 
82
94
  ```sh
83
- npx --yes @liwu1995/afu-context@next install codex --context-window 200000 --ratio 0.3
84
- npx --yes @liwu1995/afu-context@next install codex --context-window auto --ratio auto
95
+ npx --yes --registry=https://registry.npmjs.org @liwu1995/afu-context@0.1.2 install codex --context-window 200000 --ratio 0.3
96
+ npx --yes --registry=https://registry.npmjs.org @liwu1995/afu-context@0.1.2 install codex --context-window auto --ratio auto
85
97
  ```
86
98
 
87
99
  </details>
88
100
 
89
- 如果经常使用 AFU,可运行 `npm install -g @liwu1995/afu-context@next`,以后用 `afu` 代替完整的 `npx` 命令。例如,`afu ui` 打开面板,`afu ui --prices ./afu-prices.json` 使用自己的单价文件估算费用。
101
+ 如果经常使用 AFU,可运行 `npm install -g --registry=https://registry.npmjs.org @liwu1995/afu-context@0.1.2`,以后用 `afu` 代替完整的 `npx` 命令。例如,`afu ui` 打开面板,`afu ui --prices ./afu-prices.json` 使用自己的单价文件估算费用。
90
102
 
91
103
  ## 升级与移除
92
104
 
93
105
  升级 Claude Code 或 Codex 接入后,新开会话即可使用新版网关。需要移除时,先撤销对应宿主的接入,再重启宿主。以下两条命令分别用于升级和移除。
94
106
 
95
107
  ```sh
96
- npx --yes @liwu1995/afu-context@next upgrade codex
97
- npx --yes @liwu1995/afu-context@next remove codex
108
+ npx --yes --registry=https://registry.npmjs.org @liwu1995/afu-context@0.1.2 upgrade codex
109
+ npx --yes --registry=https://registry.npmjs.org @liwu1995/afu-context@0.1.2 remove codex
98
110
  ```
99
111
 
100
112
  Pi 和 DSH 可分别用 `remove pi` 或 `remove dsh` 撤销。Windows PowerShell 请使用 `npx.cmd` 并将包名放在双引号中。
package/bin/afu.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  import {spawn} from 'node:child_process';
4
4
  import {readFileSync} from 'node:fs';
5
5
  import {dirname,join,resolve} from 'node:path';
6
- import {homedir} from 'node:os';
6
+ import {homedir,constants} from 'node:os';
7
7
  import {fileURLToPath} from 'node:url';
8
8
  import {guide} from './install-guide.mjs';
9
9
  import {prepareRuntime} from './runtime.mjs';
@@ -15,10 +15,24 @@ const env={...process.env,AFU_INSTALL_ROOT:root};
15
15
  let args=process.argv.slice(2);
16
16
  if(['install','upgrade','status','remove','gc','native'].includes(args[0])){
17
17
  const at=args.indexOf('--state');if(at>=0&&args[at+1])env.AFU_STATE_DIR=resolve(args[at+1]);
18
+ const inline=args.find(a=>a.startsWith('--state='));if(inline&&inline.length>8)env.AFU_STATE_DIR=resolve(inline.slice(8));
18
19
  }
19
20
  let release;
21
+ // Ctrl-C already reaches the child through the shared foreground process group;
22
+ // relaying SIGINT again would deliver it twice. Only forward signals sent to
23
+ // this launcher alone, and only to our own child.
24
+ function relaySignals(child){
25
+ process.on('SIGINT',()=>{});
26
+ for(const signal of ['SIGTERM','SIGHUP'])process.on(signal,()=>child.kill(signal));
27
+ }
28
+ function exitCode(code,signal){
29
+ if(code!==null&&code!==undefined)return code;
30
+ return signal&&constants.signals[signal]?128+constants.signals[signal]:1;
31
+ }
20
32
  try{
21
- if(!args.length || (args.includes('--help') && (args.indexOf('--')<0 || args.indexOf('--help')<args.indexOf('--')))){
33
+ // Only the bare launcher prints its own banner; "afu ui --help" and
34
+ // "afu claude --help" belong to the subcommand or the host.
35
+ if(!args.length || (['--help','-h'].includes(args[0]))){
22
36
  console.log('AFU Context: install [claude|codex|pi|dsh], status, remove HOST, gc --dry-run, ui.\nRun npx @liwu1995/afu-context@next install to detect your installed host. Host login remains managed by the host.');
23
37
  process.exit(0);
24
38
  }
@@ -36,13 +50,14 @@ try{
36
50
  if(split<0 && args.length>1)throw Error('PLUGIN_REMOVED: pass host arguments after --');
37
51
  // COSEC: fixed host executable, argv array, no shell; no runtime downloads.
38
52
  const child=spawn(args[0],split<0?[]:args.slice(split+1),{stdio:'inherit',env,shell:false});
39
- for(const signal of ['SIGINT','SIGTERM'])process.on(signal,()=>child.kill(signal));
53
+ relaySignals(child);
40
54
  child.on('error',()=>{console.error('AFU: HOST_EXECUTABLE_UNAVAILABLE');process.exitCode=1;});
41
- child.on('exit',(code,signal)=>{process.exitCode=code??(signal?130:1);});
55
+ child.on('exit',(code,signal)=>{process.exitCode=exitCode(code,signal);});
42
56
  }else{
43
57
  const hostAction = ['pi','dsh'].includes(args[1]) && ['install','upgrade','remove','status'].includes(args[0]);
44
- const aggregateStatus = args[0]==='status' && args.slice(1).every(arg=>arg==='--json');
45
- const directHost = process.platform!=='win32' && (hostAction || aggregateStatus);
58
+ // Status always goes through one reporter (it understands autoload receipts,
59
+ // --json and --state); only Pi/DSH install/upgrade/remove need host-autoload.
60
+ const directHost = process.platform!=='win32' && hostAction && args[0]!=='status';
46
61
  if (directHost && ['install','upgrade'].includes(args[0])) {
47
62
  // Autoload paths must outlive npx eviction and the ordinary launcher cache GC.
48
63
  const state = env.AFU_STATE_DIR || join(homedir(), '.local/share/afu-context');
@@ -55,9 +70,8 @@ try{
55
70
  env.AFU_INSTALL_ROOT=await preparePlatform(root,manifest,args,env);
56
71
  const entry = directHost ? [join(root,'bin/host-autoload.py')] : ['-m','afu_context.local_cli'];
57
72
  const child=spawn(python,[...entry,...args],{stdio:'inherit',env});
58
- // Relay signals only to our own foreground child; never enumerate other sessions.
59
- for(const signal of ['SIGINT','SIGTERM'])process.on(signal,()=>child.kill(signal));
73
+ relaySignals(child);
60
74
  child.on('error',()=>{release?.();console.error('AFU failed to start');process.exitCode=1;});
61
- child.on('exit',(code,signal)=>{release?.();if(code===0&&['install','upgrade'].includes(args[0]))console.error(`AFU: installed ${args[1]}; ${['claude','codex'].includes(args[1])?'service checked; open a new '+args[1]+' session':directHost?'open a new host session':'launch with afu '+args[1]}. Waiting for the first observed request; compression is not yet verified.`);process.exitCode=code ?? (signal?130:1);});
75
+ child.on('exit',(code,signal)=>{release?.();if(code===0&&['install','upgrade'].includes(args[0]))console.error(`AFU: installed ${args[1]}; ${['claude','codex'].includes(args[1])?'service checked; open a new '+args[1]+' session':directHost?'open a new host session':'launch with afu '+args[1]}. Waiting for the first observed request; compression is not yet verified.`);process.exitCode=exitCode(code,signal);});
62
76
  }
63
77
  }catch(error){release?.();console.error('AFU: '+error.message);process.exitCode=1;}
package/bin/platform.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {obtainPack} from './prebuilt.mjs';
2
2
  import {runInstall} from './install-process.mjs';
3
3
  /** Host-only dependencies live outside the installed npm package. */
4
- import {mkdirSync,readFileSync,writeFileSync,existsSync,rmSync,cpSync,renameSync} from 'node:fs';
4
+ import {mkdirSync,readFileSync,writeFileSync,existsSync,rmSync,cpSync,renameSync,realpathSync} from 'node:fs';
5
5
  import {join,dirname,delimiter} from 'node:path';
6
6
  import {homedir} from 'node:os';
7
7
 
@@ -56,7 +56,12 @@ export async function preparePlatform(root,manifest,args,env=process.env){
56
56
  if(manifest.prebuilt && process.platform!=='win32'){
57
57
  const payload=await obtainPack(root,manifest,host,env,pid=>writeFileSync(join(lock,'owner.json'),JSON.stringify({pid:process.pid,childPid:pid,token}),{mode:0o600}));
58
58
  cpSync(join(payload,'node_modules'),join(target,'node_modules'),{recursive:true,verbatimSymlinks:true});
59
- }else await runInstall(executable,[...prefix,'ci','--registry=https://registry.npmjs.org','--prefix',target,'--ignore-scripts','--no-audit','--no-fund'],env,pid=>writeFileSync(join(lock,'owner.json'),JSON.stringify({pid:process.pid,childPid:pid,token})));
59
+ }else {
60
+ // npm treats a Windows junction alias as a linked package. Use its real
61
+ // prefix so npm ci validates the shipped lock against the actual root.
62
+ const installTarget=process.platform==='win32'?realpathSync(target):target;
63
+ await runInstall(executable,[...prefix,'ci','--registry=https://registry.npmjs.org','--prefix',installTarget,'--ignore-scripts','--no-audit','--no-fund'],env,pid=>writeFileSync(join(lock,'owner.json'),JSON.stringify({pid:process.pid,childPid:pid,token})));
64
+ }
60
65
  // Import the real adapters: a package.json marker alone misses broken files
61
66
  // and corrupt transitive dependencies. No model request is made by imports.
62
67
  await runInstall(process.execPath,['--input-type=module','-e','await import(process.argv[1])',pathToFileURL(join(target,'src/afu_host_integrations',host,'dist/index.js')).href],env);
package/bin/prebuilt.mjs CHANGED
@@ -8,14 +8,14 @@ import {runInstall} from './install-process.mjs';
8
8
  export function packName(kind){
9
9
  if(!['runtime','pi','dsh'].includes(kind))throw Error('INVALID_PACK_KIND');
10
10
  if(!['darwin','linux','win32'].includes(process.platform)||!['arm64','x64'].includes(process.arch))throw Error('UNSUPPORTED_PLATFORM');
11
- return `@liwu1995/afu-context-${kind}-${process.platform}-${process.arch}`;
11
+ return `afu-context-${kind}-${process.platform}-${process.arch}`;
12
12
  }
13
13
  export function verifyPack(folder,manifest,kind){
14
14
  const info=JSON.parse(readFileSync(join(folder,'pack-manifest.json'),'utf8'));
15
15
  if(info.build!==manifest.build||info.kind!==kind||info.platform!==process.platform||info.arch!==process.arch)throw Error('PLATFORM_PACK_MISMATCH');
16
16
  const root=realpathSync(folder);
17
17
  const listed=new Set(Object.keys(info.files));
18
- function walk(dir){for(const entry of readdirSync(dir,{withFileTypes:true})){const file=join(dir,entry.name);if(entry.isDirectory())walk(file);else {const name=file.slice(root.length+1);if(!listed.has(name))throw Error('PACK_UNLISTED_FILE');if(lstatSync(file).isSymbolicLink()&&lstatSync(realpathSync(file)).isDirectory())throw Error('PACK_DIRECTORY_LINK');}}}
18
+ function walk(dir){for(const entry of readdirSync(dir,{withFileTypes:true})){const file=join(dir,entry.name);if(entry.isDirectory())walk(file);else {const name=file.slice(root.length+1).split(sep).join('/');if(!listed.has(name))throw Error('PACK_UNLISTED_FILE');if(lstatSync(file).isSymbolicLink()&&lstatSync(realpathSync(file)).isDirectory())throw Error('PACK_DIRECTORY_LINK');}}}
19
19
  walk(join(root,'payload'));
20
20
  // COSEC: signed npm integrity plus per-file release hashes; forbid escaping
21
21
  // paths/symlinks before copying any executable into the private runtime.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liwu1995/afu-context",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "description": "Local asynchronous context compression and diagnostics",
@@ -23,13 +23,9 @@
23
23
  "README.zh-CN.md",
24
24
  "LICENSE"
25
25
  ],
26
- "optionalDependencies": {
27
- "@liwu1995/afu-context-runtime-darwin-arm64": "0.1.0",
28
- "@liwu1995/afu-context-runtime-linux-x64": "0.1.0"
29
- },
30
26
  "publishConfig": {
31
27
  "access": "public",
32
28
  "registry": "https://registry.npmjs.org",
33
- "tag": "next"
29
+ "tag": "latest"
34
30
  }
35
31
  }
package/release.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.1.0",
3
- "build": "edb9a28f8ba42bc4b9fa4858fa5438e90570cc442b79c8bc82c6b9bfd5fa3d66",
4
- "wheel": "afu_context_local-0.1.0-py3-none-any.whl",
5
- "prebuilt": true
2
+ "version": "0.1.2",
3
+ "build": "1bd035811a344a7ffffae3515a0027a2e509258eafc6568a1800e380dab2280a",
4
+ "wheel": "afu_context_local-0.1.2-py3-none-any.whl",
5
+ "prebuilt": false
6
6
  }
@@ -11,6 +11,11 @@ export class CompactDshEngine extends BasicCompactionEngine {
11
11
  handed = new WeakMap();
12
12
  handoff(agent, result) {
13
13
  const id = observationId(result);
14
+ const previous = this.handed.get(agent);
15
+ // A retried or manual compactRegion hands off again; the earlier summary
16
+ // was superseded rather than silently dropped.
17
+ if (previous && previous.id !== id)
18
+ this.log("summary_discarded", { session: sessionHash(agent.session.id), artifact_id: previous.id, reason: "superseded" });
14
19
  this.handed.set(agent, { id });
15
20
  this.log("summary_handoff", { session: sessionHash(agent.session.id), artifact_id: id });
16
21
  }
@@ -54,6 +59,9 @@ export class CompactDshEngine extends BasicCompactionEngine {
54
59
  });
55
60
  ctx.effect(() => async () => {
56
61
  this.log("runtime_stopped");
62
+ // Stop scheduling first so jobs cancelled by shutdown are not reported
63
+ // as preparation failures, then stop the host and let them settle.
64
+ this.idlePrecomputer.stop();
57
65
  await this.host.close();
58
66
  await this.idlePrecomputer.close();
59
67
  this.bindings.clear();
@@ -123,7 +131,16 @@ export class CompactDshEngine extends BasicCompactionEngine {
123
131
  }
124
132
  }
125
133
  async summarize(input, agent, signal) {
126
- const request = await this.boundRequest(agent, input.messages, "sync");
134
+ let request;
135
+ try {
136
+ request = await this.boundRequest(agent, input.messages, "sync");
137
+ }
138
+ catch {
139
+ // Model window unknown or binding limit reached: DSH's own summarizer
140
+ // must still run (this path is also used by manual compactNow).
141
+ signal?.throwIfAborted();
142
+ return super.summarize(input, agent, signal);
143
+ }
127
144
  const sessionId = String(request.session_id);
128
145
  const target = dshRoute(agent);
129
146
  const prepared = this.selectedResults.get(sessionId) ?? this.idlePrecomputer.latest(sessionId);
@@ -342,9 +359,13 @@ export class DshIdlePrecomputer {
342
359
  await Promise.all(this.running.values());
343
360
  }
344
361
  }
345
- async close() {
362
+ /** Stop accepting work; in-flight failures after this are expected. */
363
+ stop() {
346
364
  this.closed = true;
347
365
  this.pending.clear();
366
+ }
367
+ async close() {
368
+ this.stop();
348
369
  await this.drain();
349
370
  }
350
371
  start(sessionId) {
@@ -370,7 +391,8 @@ export class DshIdlePrecomputer {
370
391
  this.completed.set(sessionId, result);
371
392
  }
372
393
  catch (error) {
373
- this.onError(sessionId, error);
394
+ if (!this.closed)
395
+ this.onError(sessionId, error);
374
396
  }
375
397
  }
376
398
  }
@@ -44,9 +44,13 @@ export class HostCompactPool {
44
44
  }
45
45
  if (!active) {
46
46
  this.entries.delete(key);
47
- void old.client.close({ drain: false }).catch(() => undefined);
47
+ const closing = old.client.close({ drain: false }).catch(() => undefined);
48
+ if (process.platform === "win32")
49
+ await closing;
48
50
  }
49
51
  }
52
+ if (this.closed)
53
+ throw new Error("Host worker closed");
50
54
  let entry = this.entries.get(binding.key);
51
55
  if (!entry) {
52
56
  // Do not evict or interrupt another active session to admit a new one.
@@ -70,6 +74,8 @@ export class HostCompactPool {
70
74
  this.entries.set(binding.key, entry);
71
75
  }
72
76
  entry.binding = binding;
77
+ if (this.closed)
78
+ throw new Error("Host worker closed");
73
79
  const result = await entry.client.compact({ ...request, ...(binding.contextWindow === undefined ? {} : { token_budget: binding.contextWindow }), session_id: bindingKey([request.session_id, binding.key]) }, signal);
74
80
  if (!await binding.current())
75
81
  throw new Error("Host model changed");
@@ -144,11 +150,15 @@ export class HostCompactPool {
144
150
  }
145
151
  async close() {
146
152
  this.closed = true;
147
- await this.starting;
153
+ await this.starting?.catch(() => undefined);
148
154
  for (const controller of this.controllers)
149
155
  controller.abort();
150
- await Promise.allSettled([...this.entries.values()].map(e => e.client.close({ drain: false })));
151
- this.entries.clear();
156
+ // Entries registered by a compact() racing with close are closed here too.
157
+ while (this.entries.size) {
158
+ const entries = [...this.entries.values()];
159
+ this.entries.clear();
160
+ await Promise.allSettled(entries.map(e => e.client.close({ drain: false })));
161
+ }
152
162
  this.server?.closeAllConnections();
153
163
  if (this.server)
154
164
  await new Promise(resolve => this.server.close(() => resolve()));
@@ -50,6 +50,18 @@ export class CompactWorkerClient {
50
50
  this.child = undefined;
51
51
  this.lines = undefined;
52
52
  this.rejectAll(new CompactWorkerError("worker closed"));
53
+ if (process.platform === "win32" && child.pid !== undefined && child.exitCode === null && child.signalCode === null) {
54
+ // Windows keeps the worker's cwd/files locked until the process exits.
55
+ await new Promise((resolve, reject) => {
56
+ const finished = () => { clearTimeout(kill); clearTimeout(deadline); resolve(); };
57
+ const kill = setTimeout(() => child.kill(), 5000);
58
+ const deadline = setTimeout(() => {
59
+ child.off("exit", finished);
60
+ reject(new CompactWorkerError("worker did not exit after shutdown"));
61
+ }, 10000);
62
+ child.once("exit", finished);
63
+ });
64
+ }
53
65
  }
54
66
  }
55
67
  call(method, params, signal, allowWhileClosing = false) {
@@ -98,6 +110,11 @@ export class CompactWorkerClient {
98
110
  stdio: ["pipe", "pipe", "pipe"],
99
111
  });
100
112
  this.child = child;
113
+ // A worker that dies mid-write emits EPIPE on stdin; without a listener
114
+ // Node treats that as fatal and takes the host process down with it.
115
+ child.stdin.on("error", (error) => {
116
+ this.rejectAll(new CompactWorkerError(`worker input closed: ${error.message}`));
117
+ });
101
118
  this.lines = createInterface({ input: child.stdout });
102
119
  this.lines.on("line", (line) => this.handleLine(line));
103
120
  child.stderr.on("data", (chunk) => {
@@ -108,6 +125,13 @@ export class CompactWorkerClient {
108
125
  process.stderr.write(`[afu_context compact worker] ${text}`);
109
126
  });
110
127
  child.on("error", (error) => {
128
+ // A spawn failure emits "error" without "exit": forget this child so
129
+ // the next call starts a fresh worker instead of writing to a dead pipe.
130
+ if (this.child === child) {
131
+ this.child = undefined;
132
+ this.lines?.close();
133
+ this.lines = undefined;
134
+ }
111
135
  this.rejectAll(new CompactWorkerError(`worker failed to start: ${error.message}`));
112
136
  });
113
137
  child.on("exit", (code, signal) => {
@@ -23,12 +23,25 @@ export function normalizeContent(value) {
23
23
  }
24
24
  return safeStringify(value);
25
25
  }
26
+ /** Same weights as the Python HeuristicTokenEstimator. */
27
+ function characterWeight(code) {
28
+ if ((code >= 0x4e00 && code <= 0x9fff) || (code >= 0x3400 && code <= 0x4dbf))
29
+ return 1;
30
+ if ((code >= 0xac00 && code <= 0xd7af) || (code >= 0x1100 && code <= 0x11ff))
31
+ return 1;
32
+ if (code >= 0x3040 && code <= 0x30ff)
33
+ return 1;
34
+ if (code >= 0x1f000)
35
+ return 1;
36
+ if (code >= 0x0370)
37
+ return 0.5;
38
+ return 0.25;
39
+ }
26
40
  export function estimateContentTokens(content) {
27
41
  const text = typeof content === "string" ? content : JSON.stringify(content);
28
42
  let units = 0;
29
- for (const character of text) {
30
- units += character >= "\u4e00" && character <= "\u9fff" ? 1 : 0.25;
31
- }
43
+ for (const character of text)
44
+ units += characterWeight(character.codePointAt(0) ?? 0);
32
45
  return text.length === 0 ? 0 : Math.max(1, Math.ceil(units));
33
46
  }
34
47
  function isJsonObject(value) {