@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 +27 -15
- package/README.zh-CN.md +27 -15
- package/bin/afu.mjs +23 -9
- package/bin/platform.mjs +7 -2
- package/bin/prebuilt.mjs +2 -2
- package/package.json +2 -6
- package/python/{afu_context_local-0.1.0-py3-none-any.whl → afu_context_local-0.1.2-py3-none-any.whl} +0 -0
- package/release.json +4 -4
- package/src/afu_host_integrations/dsh/dist/index.js +25 -3
- package/src/afu_host_integrations/shared/runtime-client/dist/host-worker.js +14 -4
- package/src/afu_host_integrations/shared/runtime-client/dist/index.js +24 -0
- package/src/afu_host_integrations/shared/runtime-client/dist/protocol.js +16 -3
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@
|
|
24
|
-
| Claude Code | Windows 10/11¹ | `npx.cmd --yes "@liwu1995/afu-context@
|
|
25
|
-
| <a id="afu-codex"></a>Codex CLI | macOS / Linux | `npx --yes @liwu1995/afu-context@
|
|
26
|
-
| <a id="afu-pi"></a>Pi | macOS / Linux | `npx --yes @liwu1995/afu-context@
|
|
27
|
-
| <a id="afu-dsh"></a>DSH | macOS / Linux | `npx --yes @liwu1995/afu-context@
|
|
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.
|
|
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@
|
|
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@
|
|
66
|
-
npx --yes @liwu1995/afu-context@
|
|
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
|
-
|
|
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@
|
|
84
|
-
npx --yes @liwu1995/afu-context@
|
|
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@
|
|
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@
|
|
97
|
-
npx --yes @liwu1995/afu-context@
|
|
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@
|
|
24
|
-
| Claude Code | Windows 10/11¹ | `npx.cmd --yes "@liwu1995/afu-context@
|
|
25
|
-
| <a id="afu-codex"></a>Codex CLI | macOS / Linux | `npx --yes @liwu1995/afu-context@
|
|
26
|
-
| <a id="afu-pi"></a>Pi | macOS / Linux | `npx --yes @liwu1995/afu-context@
|
|
27
|
-
| <a id="afu-dsh"></a>DSH | macOS / Linux | `npx --yes @liwu1995/afu-context@
|
|
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
|
|
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@
|
|
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@
|
|
66
|
-
npx --yes @liwu1995/afu-context@
|
|
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@
|
|
84
|
-
npx --yes @liwu1995/afu-context@
|
|
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@
|
|
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@
|
|
97
|
-
npx --yes @liwu1995/afu-context@
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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": "
|
|
29
|
+
"tag": "latest"
|
|
34
30
|
}
|
|
35
31
|
}
|
package/python/{afu_context_local-0.1.0-py3-none-any.whl → afu_context_local-0.1.2-py3-none-any.whl}
RENAMED
|
Binary file
|
package/release.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.1.
|
|
3
|
-
"build": "
|
|
4
|
-
"wheel": "afu_context_local-0.1.
|
|
5
|
-
"prebuilt":
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
this.entries.
|
|
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
|
|
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) {
|