@liwu1995/afu-context 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,110 +1,104 @@
1
- # AFU Context
1
+ # AFU Context · Local asynchronous context compression
2
2
 
3
- **English** · [简体中文](README.zh-CN.md)
3
+ **English** · [简体中文](README.zh-CN.md) · [npm](https://www.npmjs.com/package/@liwu1995/afu-context) · [Install](#install) · [Results](#results) · [Dashboard](#dashboard)
4
4
 
5
- Local asynchronous context compression for Claude Code, Codex, Pi Coding Agent and DeepSeek Harness.
5
+ In a long session, the same tool output can travel with every model request. AFU uses the host’s selected model to summarize completed history in the background. Once ready, the summary can replace that history on a later request while preserving newer tool calls and messages.
6
6
 
7
- ## Install
7
+ ## How AFU compacts a session
8
8
 
9
- **Release candidate; not yet published.** The npx commands below become available after the main package and platform companions are published.
9
+ | When | What AFU does |
10
+ | --- | --- |
11
+ | A session reaches its threshold | Prepares a summary in the background while the session continues. |
12
+ | The host next needs the history | Checks that the covered prefix still matches before replacing it. |
13
+ | After replacement | Records a successful upstream response or a host commit so the dashboard can distinguish generation from application. |
10
14
 
11
- Requires Node.js 22.19+ (22.x) or 24+, and your host installed and logged in. Platform-pack builds prepare a private Python runtime on supported Mac/Linux systems. Legacy/source and Windows builds still require Python 3.11+. Platform packs must be published and verified before advertising the one-command flow as generally available.
15
+ <a id="install"></a>
12
16
 
13
- ```sh
14
- npx @liwu1995/afu-context install # detects installed hosts; asks when multiple are found
15
- # Non-interactive: npx @liwu1995/afu-context install claude
16
- ```
17
+ ## Quick start
17
18
 
18
- CC/Codex install no Pi/DSH JavaScript dependencies. Selecting Pi or DSH downloads only that platform’s pinned dependencies into a private versioned cache.
19
+ Install and sign in to your host first. You need npm and Node.js **22.19+ (22.x) or 24+**. Pick one row, run the install command in your terminal, then use the launch command.
19
20
 
20
- The first command prepares AFU's CLI. First use creates a private Python environment and installs pinned dependencies, reusing uv when available or using pip. Set `AFU_PYTHON` to select your Python executable. No system Python modifications or separate summary API key are needed.
21
-
22
- | Host | Integration | Launch | Validated compatibility |
21
+ | Host | System | Install | Launch after installation |
23
22
  | --- | --- | --- | --- |
24
- | Claude Code | Local gateway and startup hooks | `claude` | macOS/Linux sandbox; Windows implementation awaits machine validation |
25
- | Codex CLI | Local gateway and startup hooks | `codex` | macOS/Linux; not a desktop App plugin; Windows native integration disabled |
26
- | Pi | Launcher-loaded extension | `afu pi -- --provider PROVIDER --model MODEL` | Pi 0.84.4, macOS/Linux |
27
- | DSH | Temporary profile overlay | `afu dsh -- --profile PROFILE` | DSH 0.1.2-rc.1 / Cordis 4.0.2, macOS/Linux |
28
-
29
- Native services use user launchd on macOS, systemd user services on Linux, and the existing Windows scheduled-task integration for Claude Code. Linux native automatic installation requires a working `systemctl --user`. Codex may require approving its hooks. Host credentials remain owned by the host. Official Claude subscription/OAuth and Windows real-machine acceptance remain pending.
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` |
30
28
 
31
- ## Diagnostics
32
-
33
- ```sh
34
- afu status
35
- afu status claude --json
36
- afu ui --open
37
- ```
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`.
38
30
 
39
- The local read-only dashboard distinguishes installation, observed requests, summary generation and confirmed application. It includes disk/running versions, sessions, timelines, receipts, cache usage and fixed error reasons. Pi/DSH application indicates successful host-history commit; CC/Codex application requires a complete successful upstream response with the replacement. Historical runtime events do not assert that a process is currently alive.
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.
40
32
 
41
- Only allowlisted metadata is logged. Prompts, summary text, credentials, raw provider errors and conversation files are not exposed in the panel. It binds to loopback and requires a random access capability; do not share the printed URL. Cache counters absent from a provider are unknown, not zero. Pi/DSH chat usage and cost analysis are not provided in this version.
33
+ <details>
34
+ <summary>Linux manual startup and Pi / DSH launch options</summary>
42
35
 
43
- ## Remove
36
+ 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.
44
37
 
45
- ```sh
46
- afu remove claude # substitute codex, pi or dsh
47
- ```
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`.
48
39
 
49
- Only settings still owned by AFU are restored. Later user changes and existing sessions are preserved. Native clients still using an old route retain passthrough; Pi/DSH future wrapper launches bypass AFU. Remove the integration before uninstalling the npm package. Versioned runtime caches and diagnostic logs remain available for active sessions and inspection.
40
+ </details>
50
41
 
51
- ## Configuration
42
+ <a id="results"></a>
52
43
 
53
- - `AFU_PYTHON`: Python 3.11+ executable.
54
- - `AFU_RUNTIME_DIR`: isolated Python runtime cache.
55
- - `AFU_STATE_DIR`: integration receipts and metadata logs.
56
- - `afu install claude --context-window 200000`: explicit CC window; substitute Codex where appropriate. A changed build or explicit configuration creates a healthy new generation before switching future sessions; existing sessions keep their original endpoint.
57
- - Pi/DSH follow the selected model's declared window. Default trigger ratio: 40% at 1M or above, 50% otherwise. Host metadata unavailable means AFU yields to native behavior.
58
- - `--config-dir` and `--no-autostart` support disposable native sandbox tests; do not substitute hooks-only tests for production service-manager acceptance.
44
+ ## Benchmark results
59
45
 
60
- This package includes generic context-management code, host adapters, local diagnostics and a public Qwen tokenizer resource with its attribution. It includes no hosted monitoring service or business-specific backend.
46
+ In a LongBench V2 replay of 39 history questions (20 agent histories and 19 dialogue histories), AFU’s asynchronous strategy answered **23 correctly** with an average context of **15.9k tokens**.
61
47
 
62
- Licensed under Apache-2.0. Third-party dependencies retain their respective licenses.
48
+ | Replay strategy | First-answer accuracy | Mean context tokens |
49
+ | --- | ---: | ---: |
50
+ | No compaction | 19/39 (48.72%) | 72.6k |
51
+ | Codex Compact Sync | 20/39 (51.28%) | 21.7k |
52
+ | DSH Compact Sync | 18/39 (46.15%) | 26.1k |
53
+ | Pi Compact Sync | 13/39 (33.33%) | 21.6k |
54
+ | **AFU Compact Async** | **23/39 (58.97%)** | **15.9k** |
63
55
 
64
- ## Upgrade and model windows
56
+ **4.56×** is the ratio of mean context tokens without compaction to those with AFU. The replay used DeepSeek-V3.2 for both answers and summaries, a 131,072-token logical window, and a 20k AFU trigger with a one-turn delay. Synchronous strategies replayed their respective prompts and policies, with at least one compaction for the context-size comparison. This metric excludes answer output and does not measure total API cost or production savings for the npm release.
65
57
 
66
- Run `afu upgrade claude` (or `codex`) after updating the package. New sessions use the new generation; restart is not forced on existing sessions. Old generation directories are retained for those sessions.
58
+ <a id="dashboard"></a>
67
59
 
68
- CC/Codex default to querying the selected model’s provider metadata after a completed request. The lookup is off the chat response path. Default trigger: 40% for windows >= 1M, otherwise 50%. If the provider does not declare a window, new compression is skipped and status reports `manual_window_required`; normal chat continues. Use `afu install claude --context-window 200000` with the actual known capacity to override. An explicit window remains explicit after a model switch until you reconfigure it. OpenAI-compatible model catalogs are not guaranteed to expose context limits.
60
+ ## Web dashboard
69
61
 
70
- ### Recovery and cache maintenance
62
+ For Codex, check the integration status, then open the dashboard and select your session. For another host, replace `codex` with `claude`, `pi` or `dsh`.
71
63
 
72
- Interrupted native upgrades are journaled. The next install/upgrade/remove or native startup completes the switch, or restores the previous configuration if the candidate cannot start. Concurrent user edits stop recovery rather than being overwritten.
64
+ ```sh
65
+ npx --yes @liwu1995/afu-context@next status codex
66
+ npx --yes @liwu1995/afu-context@next ui
67
+ ```
73
68
 
74
- Use `afu gc --dry-run` to preview reclamation and `afu gc` to perform it. Current, busy, pending and unrecognized legacy generations are retained. Retired native services require released client leases and five idle minutes before retirement. Unreferenced managed caches require 24 hours of age; active installers and other registered AFU state directories protect their caches.
69
+ 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.
75
70
 
76
- Use `afu install claude --context-window auto --ratio auto` (or `codex`) to reset manual overrides. Omitted options preserve existing settings. Unknown model capacity pauses new compression; chat continues.
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.
77
72
 
78
- Host dependencies use release lockfiles and `npm ci` with integrity checks. Pi also ships its upstream shrinkwrap. Installation subprocesses have a 180-second deadline; `AFU_INSTALL_TIMEOUT_MS` may set 1–600000 milliseconds. A timeout stops only that installer's process group and leaves existing generations intact. Registry or integrity errors remain failures; integrity validation is never disabled.
73
+ ## Models and thresholds
79
74
 
80
- Pi/DSH also persist `afu install pi --context-window 200000 --ratio 0.2` (substitute `dsh`). Launcher options take precedence over environment variables, then saved settings, then automatic model defaults. Explicit `auto` resets the override. Invalid ratios/windows fail without changing saved settings. After removal, `afu pi -- <host arguments>` and `afu dsh -- <host arguments>` launch the host without preparing AFU runtimes or downloading dependencies.
75
+ All four integrations reuse the conversation’s model and authentication. The default trigger is **40%** for windows of **1,000,000 tokens or more** and **50%** for smaller windows. Pi and DSH follow the host’s selected model and window. Claude Code and Codex query provider metadata after a complete response. When the window is unknown, status reports `manual_window_required` and chat continues.
81
76
 
82
- Upgrade recovery is scoped to the selected host/configuration directory. Management failures report an allowlisted reason code, phase, recovery hint, and a bounded latest-error record under the chosen state directory's `logs/management-error.json`. Raw exception text, credentials and host settings are not included.
77
+ <details>
78
+ <summary>Set a window or ratio, or restore automatic settings</summary>
83
79
 
84
- ## Platform-pack release (candidate; not published)
80
+ For a Codex model with a known 200,000-token window, the first command sets a 30% trigger. The second restores automatic settings.
85
81
 
86
- Build the main package with `scripts/build_distribution.py --prebuilt`. Build each runtime/Pi/DSH companion on its target system using `scripts/build_platform_pack.py`. Runtime sources must be relocatable standalone Python; host-pack sources must be lockfile-verified platform caches. Publish companions before the main package, then test the registry installation from a clean user account. Never label local-pack tests as registry publication verification.
82
+ ```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
85
+ ```
87
86
 
88
- `AFU_BUNDLE_DIR` selects a local directory of companion packages for offline installs and sandbox validation. Each companion must match the main build, OS and architecture; all payload files are hashed and escaping links or extra files are rejected. CC/Codex do not fetch Pi/DSH companions. `install --dry-run` checks host availability, writable state and the user service manager without installing runtimes or editing host settings.
87
+ </details>
89
88
 
90
- After a temporary npx install, use `npx @liwu1995/afu-context status` and `npx @liwu1995/afu-context pi -- ...` / `npx @liwu1995/afu-context dsh -- ...`; native CC/Codex retain their normal launch commands. Successful installation is not proof of request routing or summary application.
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.
91
90
 
92
- ### Linux without a systemd user session
91
+ ## Upgrade and remove
93
92
 
94
- If installation reports `USER_SERVICE_UNAVAILABLE`, host settings have not been
95
- changed. Enable your systemd user service, or explicitly choose manual startup:
93
+ 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.
96
94
 
97
95
  ```sh
98
- afu install codex --no-autostart
99
- # For Claude Code: afu install claude --no-autostart
96
+ npx --yes @liwu1995/afu-context@next upgrade codex
97
+ npx --yes @liwu1995/afu-context@next remove codex
100
98
  ```
101
99
 
102
- Keep the same `--config-dir`, `--state`, upstream and other options if you used
103
- custom settings. Manual mode starts the gateway now but does not register login
104
- or reboot recovery. Before using the host after reboot, rerun the same install
105
- command with `--no-autostart`. Do not expect the gateway to restart automatically.
100
+ Use `remove pi` or `remove dsh` for those integrations. In Windows PowerShell, use `npx.cmd` and double-quote the package name.
101
+
102
+ The dashboard listens on loopback. Logs record usage and receipts, and Pi/DSH also persist summaries in local session state. Generating summaries uses model tokens. Savings depend on subsequent reuse and actual cache hits.
106
103
 
107
- Linux `status` displays `startup.mode` separately from `healthy`. A matching
108
- service file and registration receipt produce `registration_recorded`; this is
109
- not a live systemd enablement check or proof that a reboot has been tested. Missing
110
- or edited service files are reported as `registration_file_changed_or_missing`.
104
+ [Apache-2.0](LICENSE)
package/README.zh-CN.md CHANGED
@@ -1,79 +1,104 @@
1
- # AFU Context
1
+ # AFU Context · 本地异步上下文压缩
2
2
 
3
- [English](README.md) · **简体中文**
3
+ [English](README.md) · **简体中文** · [npm](https://www.npmjs.com/package/@liwu1995/afu-context) · [一键安装](#install) · [评测效果](#results) · [Web 面板](#dashboard)
4
4
 
5
- 为 Claude Code、Codex、Pi Coding Agent 和 DeepSeek Harness 提供本地异步上下文压缩与诊断。后台准备摘要,覆盖范围仍然匹配时再应用。
5
+ 长对话里,同一段工具输出可能随每次请求反复发送。AFU 用宿主当前的模型在后台整理已经结束的对话。摘要就绪后,下一次合适的请求可以用摘要替换旧历史,同时保留之后发生的工具调用和新消息。
6
6
 
7
- ## 安装
7
+ ## AFU 如何压缩对话
8
8
 
9
- **当前为未发布的候选包。** 以下 npx 命令在主包和对应平台配套包正式发布后可用:
9
+ | 时机 | AFU 的行为 |
10
+ | --- | --- |
11
+ | 达到压缩阈值 | 后台生成摘要,当前会话可以继续。 |
12
+ | 下一次使用历史 | 检查摘要覆盖的对话前缀是否仍然匹配,匹配才替换。 |
13
+ | 使用之后 | 记录模型请求成功回执或宿主提交结果,面板区分已生成与已应用。 |
10
14
 
11
- ```sh
12
- npx @liwu1995/afu-context install
13
- # 非交互安装:npx @liwu1995/afu-context install claude
14
- ```
15
+ <a id="install"></a>
15
16
 
16
- 需要 Node.js 22.19+(22.x)或 24+,以及已安装并登录的宿主。支持的平台配套包在 Mac/Linux 准备私有 Python 运行时;源码、旧分发和 Windows 路径仍需要 Python 3.11+。不修改系统 Python,不需要单独配置摘要模型密钥。`AFU_PYTHON` 可指定解释器。
17
+ ## 快速开始
17
18
 
18
- | 框架 | 接入方式 | 启动 | 验证范围 |
19
+ 先安装并登录要使用的宿主,准备 npm 和 Node.js **22.19+(22.x)或 24+**。选一行,在终端执行安装命令,然后用最后一列的命令启动。
20
+
21
+ | 宿主 | 系统 | 一键安装 | 安装后启动 |
19
22
  | --- | --- | --- | --- |
20
- | Claude Code | 本地网关与启动 Hook | `claude` | Mac/Linux 沙箱;Windows 待实机验收 |
21
- | Codex CLI | 本地网关与启动 Hook | `codex` | Mac/Linux;不是桌面 App 插件;Windows 原生接入未启用 |
22
- | Pi | 启动时加载扩展 | `afu pi -- --provider PROVIDER --model MODEL` | Pi 0.84.4,Mac/Linux |
23
- | DSH | 临时 profile 覆盖 | `afu dsh -- --profile PROFILE` | DSH 0.1.2-rc.1 / Cordis 4.0.2,Mac/Linux |
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` |
24
28
 
25
- CC/Codex 不下载 Pi/DSH 依赖;选择 Pi 或 DSH 时才准备对应的锁定版本依赖。临时 npx 安装后使用 `npx @liwu1995/afu-context status` 和 `npx @liwu1995/afu-context pi -- ...` / `npx @liwu1995/afu-context dsh -- ...`;原生 CC/Codex 继续照常启动。
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`。
26
30
 
27
- macOS 使用用户级 launchd,Linux 使用 systemd 用户服务,Windows Claude Code 使用计划任务。Codex 可能需要确认启用 Hook。宿主继续管理认证;官方 Claude 订阅/OAuth 和 Windows 实机验收尚未完成。
31
+ ¹ Windows 使用 PowerShell,需本机 Python **3.11+** 和支持 PowerShell Hook 的 Claude Code。Windows 原生 Claude Code 已实现,待实机验收。Codex 未支持,Pi 和 DSH 未验收。
28
32
 
29
- ## 状态与面板
33
+ <details>
34
+ <summary>Linux 手动启动与 Pi / DSH 启动参数</summary>
30
35
 
31
- ```sh
32
- afu status
33
- afu status claude --json
34
- afu ui --open
35
- ```
36
+ Linux 没有用户级 systemd 时,在 Claude Code 或 Codex 安装命令末尾加 `--no-autostart`,系统重启后重新执行同一命令启动网关。
37
+
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`。
39
+
40
+ </details>
41
+
42
+ <a id="results"></a>
36
43
 
37
- 只读面板分别展示已安装、请求已观测、摘要就绪、应用已确认,以及磁盘/运行版本、时间线、缓存 usage 和错误原因。Pi/DSH 应用回执表示宿主历史提交;CC/Codex 还需包含替换的上游请求完整成功。历史事件不代表进程仍存活。
44
+ ## 评测效果
38
45
 
39
- 仅记录允许的元数据,不展示对话正文、摘要正文、认证、原始模型错误或会话文件。面板只监听回环地址并要求随机访问凭证,不要分享启动时的链接。未返回缓存指标时显示未知,不当作零。当前不提供 Pi/DSH 主请求 usage 或成本分析。
46
+ 在 LongBench V2 的 39 道历史问答回放中(20 道 Agent 历史、19 道对话历史),AFU 异步压缩答对 **23 道**,平均上下文为 **15.9k tokens**。
40
47
 
41
- ## 移除与升级
48
+ | 回放策略 | 首次回答正确率 | 平均上下文 tokens |
49
+ | --- | ---: | ---: |
50
+ | 无压缩 | 19/39 (48.72%) | 72.6k |
51
+ | Codex Compact Sync | 20/39 (51.28%) | 21.7k |
52
+ | DSH Compact Sync | 18/39 (46.15%) | 26.1k |
53
+ | Pi Compact Sync | 13/39 (33.33%) | 21.6k |
54
+ | **AFU Compact Async** | **23/39 (58.97%)** | **15.9k** |
55
+
56
+ **4.56×** 是无压缩与 AFU 的平均上下文 token 之比。回放使用 DeepSeek-V3.2 回答和压缩,逻辑窗口为 131,072 tokens,AFU 在 20k 触发,摘要延迟一轮使用。同步策略按各自提示词和策略回放,比较上下文规模时至少压缩一次。此指标不含回答输出,也不能直接换算成 API 总费用或当前 npm 版本的线上收益。
57
+
58
+ <a id="dashboard"></a>
59
+
60
+ ## Web 面板:查看压缩是否生效
61
+
62
+ 以 Codex 为例,先看接入状态,再打开面板查看刚才的会话。其它宿主将 `codex` 换成 `claude`、`pi` 或 `dsh`。
42
63
 
43
64
  ```sh
44
- afu remove claude # 可换为 codex、pi、dsh
45
- afu upgrade claude # 更新 npm 包后执行;也支持 codex
65
+ npx --yes @liwu1995/afu-context@next status codex
66
+ npx --yes @liwu1995/afu-context@next ui
46
67
  ```
47
68
 
48
- 仅恢复仍由 AFU 拥有的配置;保留用户后续修改和已有会话。旧原生会话保留透传,Pi/DSH 后续启动绕过 AFU。卸载 npm 包前先移除接入。旧运行时目录、缓存和诊断日志保留供已有会话使用与检查。
69
+ 面板分别记录**请求已接入 → 摘要已生成 → 摘要已应用**。Claude Code 和 Codex 的应用回执表示替换历史后的请求已收到完整成功响应。Pi 和 DSH 的回执表示宿主已把摘要写入会话历史。面板也展示摘要耗时和模型返回的缓存用量。
49
70
 
50
- 升级先验证新服务健康,再切换新会话,不强制重启已有会话。恢复操作限定在指定宿主及配置目录。管理错误以允许的原因码、阶段和恢复提示写入状态目录的 `logs/management-error.json`,不写入原始异常或认证信息。
71
+ 在**总览**查看活跃会话、摘要生成耗时和应用数量。点进一个**会话**,沿时间线找到摘要及后续的应用回执,再看对应请求的缓存用量。遇到失败或证据缺失时,打开**高级诊断**查看错误原因与运行版本。
51
72
 
52
- ## 配置与模型窗口
73
+ ## 模型与压缩阈值
53
74
 
54
- - `AFU_RUNTIME_DIR`:版本化运行时缓存;`AFU_STATE_DIR`:接入回执与日志。
55
- - CC/Codex 在主请求结束后异步查询当前模型的供应商元数据;默认窗口 ≥1M 时 40%,其它窗口 50%。未知窗口暂停新压缩,聊天继续。
56
- - `afu install claude --context-window 200000 --ratio 0.2` 可显式覆盖;`--context-window auto --ratio auto` 恢复自动配置。省略选项保留已有设置,非法值不修改配置。
57
- - Pi/DSH 跟随宿主当前模型、认证和声明窗口。窗口缺失时保留原生行为。支持 `afu install pi --context-window 200000 --ratio 0.2`,也可换为 dsh;优先级为启动选项、环境变量、保存的配置、自动默认值。阈值使用可压缩工作集的 token 估算,不等同状态栏总用量。
58
- - `--config-dir`、`--state`、`--no-autostart` 用于独立沙箱或手动管理。不要将 Hook 测试当作系统服务恢复验收。
75
+ 四个平台都复用主对话的模型和认证。模型窗口为 **1,000,000 tokens 或更大** 时,默认在 **40%** 触发,更小的窗口按 **50%** 触发。Pi 和 DSH 跟随宿主选择的模型和窗口。Claude Code 和 Codex 在完整响应后查询提供方的窗口信息,查不到时显示 `manual_window_required`,聊天可以继续。
59
76
 
60
- 依赖使用发行锁文件、`npm ci` 与完整性检查;Pi 还包含上游 shrinkwrap。安装子进程默认超时 180 秒,`AFU_INSTALL_TIMEOUT_MS` 可设为 1–600000 毫秒。超时只停止本次安装进程组,不改变已有运行版本;不会关闭完整性校验来绕过下载错误。
77
+ <details>
78
+ <summary>手动指定窗口、调整比例或恢复自动设置</summary>
61
79
 
62
- ## 平台配套包
80
+ 下面以已知窗口为 200,000 tokens 的 Codex 模型为例。第一条将触发比例设为 30%,第二条恢复自动设置。
63
81
 
64
- 维护者使用 `scripts/build_distribution.py --prebuilt` 构建主包,在目标系统用 `scripts/build_platform_pack.py` 构建 runtime/Pi/DSH 配套包。Python 来源须可迁移,宿主依赖须匹配锁文件。配套包先发布,主包最后发布,再从全新用户环境验证公网安装。
82
+ ```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
85
+ ```
65
86
 
66
- `AFU_BUNDLE_DIR` 可指向本地配套包目录,供离线安装和沙箱验证。配套包须与主包构建、系统、架构一致;逐文件校验,拒绝越界链接或多余文件。`install --dry-run` 只检查宿主、状态目录和服务管理器,不安装运行时、不修改宿主配置。本地归档验收不等于公网发布验收。
87
+ </details>
67
88
 
68
- ### Linux 没有 systemd 用户会话时
89
+ 如果经常使用 AFU,可运行 `npm install -g @liwu1995/afu-context@next`,以后用 `afu` 代替完整的 `npx` 命令。例如,`afu ui` 打开面板,`afu ui --prices ./afu-prices.json` 使用自己的单价文件估算费用。
69
90
 
70
- 若安装返回 `USER_SERVICE_UNAVAILABLE`,宿主配置尚未改变。启用 systemd 用户服务,或明确选择手动启动:
91
+ ## 升级与移除
92
+
93
+ 升级 Claude Code 或 Codex 接入后,新开会话即可使用新版网关。需要移除时,先撤销对应宿主的接入,再重启宿主。以下两条命令分别用于升级和移除。
71
94
 
72
95
  ```sh
73
- afu install codex --no-autostart
74
- # Claude Code:afu install claude --no-autostart
96
+ npx --yes @liwu1995/afu-context@next upgrade codex
97
+ npx --yes @liwu1995/afu-context@next remove codex
75
98
  ```
76
99
 
77
- 保留原来的配置目录、状态目录、上游等参数。手动模式启动当前网关,但不注册登录/重启恢复;重启后需再次执行同一安装命令。`status` 分别显示启动模式与健康状态;`registration_recorded` 表示注册文件和回执匹配,不等于实时 systemd 状态或整机重启已验收。
100
+ Pi 和 DSH 可分别用 `remove pi` 或 `remove dsh` 撤销。Windows PowerShell 请使用 `npx.cmd` 并将包名放在双引号中。
101
+
102
+ 本地面板只监听回环地址。日志记录用量和回执,Pi/DSH 的本地会话状态还会保存摘要。生成摘要本身会调用模型,节省多少取决于后续复用和实际缓存命中。
78
103
 
79
- 本包包含通用上下文管理、宿主适配器、本地诊断及带来源声明的公共 Qwen tokenizer,不依赖托管监控或业务后端。项目使用 Apache-2.0,第三方依赖保留各自许可证。
104
+ [Apache-2.0](LICENSE)
package/bin/afu.mjs CHANGED
@@ -19,7 +19,7 @@ if(['install','upgrade','status','remove','gc','native'].includes(args[0])){
19
19
  let release;
20
20
  try{
21
21
  if(!args.length || (args.includes('--help') && (args.indexOf('--')<0 || args.indexOf('--help')<args.indexOf('--')))){
22
- console.log('AFU Context: install [claude|codex|pi|dsh], status, remove HOST, gc --dry-run, ui.\nRun npx @liwu1995/afu-context install to detect your installed host. Host login remains managed by the host.');
22
+ 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
23
  process.exit(0);
24
24
  }
25
25
  args=await guide(args,env);
@@ -40,14 +40,24 @@ try{
40
40
  child.on('error',()=>{console.error('AFU: HOST_EXECUTABLE_UNAVAILABLE');process.exitCode=1;});
41
41
  child.on('exit',(code,signal)=>{process.exitCode=code??(signal?130:1);});
42
42
  }else{
43
+ 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);
46
+ if (directHost && ['install','upgrade'].includes(args[0])) {
47
+ // Autoload paths must outlive npx eviction and the ordinary launcher cache GC.
48
+ const state = env.AFU_STATE_DIR || join(homedir(), '.local/share/afu-context');
49
+ env.AFU_RUNTIME_DIR = join(state, 'autoload', 'runtimes');
50
+ }
51
+ if (args[0] === 'pi') env.AFU_EXPLICIT_EXTENSION = 'pi';
43
52
  release=await holdCache(manifest.build,env);
44
53
  const location=await prepareRuntime(root,manifest,env);
45
54
  const python=join(location,process.platform==='win32'?'Scripts/python.exe':'bin/python');
46
55
  env.AFU_INSTALL_ROOT=await preparePlatform(root,manifest,args,env);
47
- const child=spawn(python,['-m','afu_context.local_cli',...args],{stdio:'inherit',env});
56
+ const entry = directHost ? [join(root,'bin/host-autoload.py')] : ['-m','afu_context.local_cli'];
57
+ const child=spawn(python,[...entry,...args],{stdio:'inherit',env});
48
58
  // Relay signals only to our own foreground child; never enumerate other sessions.
49
59
  for(const signal of ['SIGINT','SIGTERM'])process.on(signal,()=>child.kill(signal));
50
60
  child.on('error',()=>{release?.();console.error('AFU failed to start');process.exitCode=1;});
51
- 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':'launch with afu '+args[1]}. Waiting for the first observed request; compression is not yet verified.`);process.exitCode=code ?? (signal?130: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);});
52
62
  }
53
63
  }catch(error){release?.();console.error('AFU: '+error.message);process.exitCode=1;}
@@ -0,0 +1,328 @@
1
+ """Install npm-hosted AFU extensions in Pi and DSH's normal discovery paths."""
2
+
3
+ import argparse
4
+ import hashlib
5
+ import json
6
+ import os
7
+ import re
8
+ import shutil
9
+ import subprocess
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ from afu_context import native_install as fs
14
+ from afu_context.diagnostics import state_root
15
+ from afu_context.management import report, validate_policy
16
+
17
+
18
+ def run(*args, stdin=None):
19
+ result = subprocess.run(
20
+ args, input=stdin, capture_output=True, text=True, timeout=30
21
+ )
22
+ if result.returncode:
23
+ raise ValueError("Host configuration could not be loaded")
24
+ return result.stdout
25
+
26
+
27
+ def write_owned(file, content, marker):
28
+ if file.is_symlink():
29
+ raise ValueError("Refusing to replace an extension symlink")
30
+ if file.exists() and file.read_text() == content:
31
+ return
32
+ if file.exists() and not file.read_text().startswith(marker):
33
+ raise ValueError("Existing extension belongs to another installation")
34
+ fs.atomic(file, content)
35
+
36
+
37
+ def install_pi(root, adapter, settings, previous):
38
+ agent = (
39
+ Path(os.environ.get("PI_CODING_AGENT_DIR", "~/.pi/agent"))
40
+ .expanduser()
41
+ .resolve()
42
+ )
43
+ entry = agent / "extensions" / "afu-context.ts"
44
+ if previous and str(entry) != previous["entry"]:
45
+ raise ValueError(
46
+ "Remove the previous Pi agent registration before switching directories"
47
+ )
48
+ if (
49
+ entry.exists()
50
+ and previous
51
+ and hashlib.sha256(entry.read_bytes()).hexdigest() != previous["digest"]
52
+ ):
53
+ raise ValueError("Pi extension changed since installation; user file preserved")
54
+ marker = "// Managed by AFU " + root.as_uri() + "\n"
55
+ # The explicit launcher supplies its own extension; avoid installing hooks twice.
56
+ body = marker + "import register from " + json.dumps(str(adapter)) + ";\n"
57
+ body += "export default function(pi) {\n if (process.env.AFU_EXPLICIT_EXTENSION === 'pi' || (process.env.AFU_CONTEXT_COMPACT_WORKER_COMMAND && process.argv.some(a => a.endsWith('/afu_host_integrations/pi/dist/index.js')))) return;\n"
58
+ body += (
59
+ " Object.assign(process.env, "
60
+ + json.dumps(settings)
61
+ + ");\n register(pi);\n}\n"
62
+ )
63
+ write_owned(entry, body, marker)
64
+ return {"entry": str(entry), "digest": hashlib.sha256(body.encode()).hexdigest()}
65
+
66
+
67
+ def profile_manifest(binary, platform, profile):
68
+ args = [binary, "--profile", profile, "--dump-config"]
69
+ tree = run(*args)
70
+ info = json.loads(
71
+ run("node", platform / "scripts/dsh-web-probe.mjs", binary, profile)
72
+ )
73
+ return Path(info["profileDir"]) / "package.json", tree
74
+
75
+
76
+ def install_dsh(root, platform, adapter, settings, profile, previous):
77
+ binary = shutil.which("dsh")
78
+ manifest, tree = profile_manifest(binary, platform, profile)
79
+ if previous and previous["manifest"] != str(manifest):
80
+ raise ValueError(
81
+ "DSH profile directory changed; existing registration preserved"
82
+ )
83
+ profile_dir = manifest.parent
84
+ key = hashlib.sha256(str(profile_dir).encode()).hexdigest()[:20]
85
+ bundle_name = "afu-context-profile-" + key
86
+ bundle = root / "autoload" / key
87
+ entry = bundle / "provider.mjs"
88
+ link = profile_dir / "node_modules" / bundle_name
89
+ marker = "// Managed by AFU " + root.as_uri() + "\n"
90
+ original = manifest.read_text()
91
+ config = json.loads(original)
92
+ bundles = config["dsh"]["profile"]["bundles"]
93
+ deps = config.setdefault("dependencies", {})
94
+ owned_dep = "file:" + str(bundle)
95
+ if link.parent.is_symlink():
96
+ raise ValueError("Cannot register a bundle in a shared node_modules symlink")
97
+ if (link.is_symlink() and link.resolve() != bundle) or (
98
+ link.exists() and not link.is_symlink()
99
+ ):
100
+ raise ValueError("Existing DSH bundle directory conflicts with AFU")
101
+ if bundle_name in deps and deps[bundle_name] != owned_dep:
102
+ raise ValueError("DSH bundle dependency has been edited")
103
+ if bundle_name in bundles and deps.get(bundle_name) != owned_dep:
104
+ raise ValueError("DSH bundle is not owned by this installation")
105
+ bundle.mkdir(parents=True, exist_ok=True, mode=0o700)
106
+ # Bundle membership is read on host startup. Never edit watched cordis.patch.yml.
107
+ if bundle_name not in bundles:
108
+ patches = run(
109
+ "node",
110
+ platform / "scripts/dsh-overlay.mjs",
111
+ entry,
112
+ bundle,
113
+ binary,
114
+ stdin=tree,
115
+ )
116
+ preset = bundle / "presets.mjs"
117
+ if preset.exists():
118
+ old_arg = ", " + json.dumps(str(bundle)) + ");"
119
+ source = preset.read_text()
120
+ if old_arg not in source:
121
+ raise ValueError("Unsupported preset loader format")
122
+ cleanup = (
123
+ "import {mkdtempSync,rmSync} from 'node:fs';\n"
124
+ "import {tmpdir} from 'node:os';\nimport {join} from 'node:path';\n"
125
+ "const scratch=mkdtempSync(join(tmpdir(),'afu-dsh-presets-'));\n"
126
+ "process.once('exit',()=>rmSync(scratch,{recursive:true,force:true}));\n"
127
+ )
128
+ fs.atomic(preset, cleanup + source.replace(old_arg, ", scratch);"))
129
+ fs.atomic(bundle / "cordis.patch.yml", patches)
130
+ fs.write(
131
+ bundle / "package.json",
132
+ {
133
+ "name": bundle_name,
134
+ "version": "1.0.0",
135
+ "dsh": {"bundle": {"patch": "./cordis.patch.yml"}},
136
+ },
137
+ )
138
+ content = marker + "Object.assign(process.env," + json.dumps(settings) + ");\n"
139
+ content += (
140
+ "export default (await import(" + json.dumps(adapter.as_uri()) + ")).default;\n"
141
+ )
142
+ write_owned(entry, content, marker)
143
+ if not link.exists():
144
+ link.parent.mkdir(parents=True, exist_ok=True)
145
+ link.symlink_to(bundle, target_is_directory=True)
146
+ if bundle_name not in bundles:
147
+ bundles.append(bundle_name)
148
+ deps[bundle_name] = owned_dep
149
+ if manifest.read_text() != original:
150
+ raise ValueError("Concurrent DSH configuration change")
151
+ fs.write(manifest, config)
152
+ try:
153
+ composed = run(binary, "--profile", profile, "--dump-config")
154
+ if (
155
+ str(entry) not in composed
156
+ and str(bundle / "presets.mjs") not in composed
157
+ ):
158
+ raise ValueError("DSH did not resolve the AFU provider")
159
+ except Exception:
160
+ if fs.read(manifest) == config:
161
+ fs.atomic(manifest, original)
162
+ raise
163
+ return {"manifest": str(manifest), "bundle": str(bundle), "name": bundle_name}
164
+
165
+
166
+ def remove_record(host, record):
167
+ if host == "pi":
168
+ entry = Path(record["entry"])
169
+ if entry.exists():
170
+ if (
171
+ entry.is_symlink()
172
+ or hashlib.sha256(entry.read_bytes()).hexdigest() != record["digest"]
173
+ ):
174
+ raise ValueError(
175
+ "Pi extension changed since installation; user file preserved"
176
+ )
177
+ entry.unlink()
178
+ return
179
+ manifest, name = Path(record["manifest"]), record["name"]
180
+ config = fs.read(manifest)
181
+ if not config:
182
+ return
183
+ bundles = config["dsh"]["profile"]["bundles"]
184
+ deps = config.get("dependencies", {})
185
+ if name in deps and deps[name] != "file:" + record["bundle"]:
186
+ raise ValueError(
187
+ "DSH dependency changed since installation; user file preserved"
188
+ )
189
+ if name in bundles and name not in deps:
190
+ raise ValueError("DSH bundle ownership changed; user configuration preserved")
191
+ if name in bundles:
192
+ bundles.remove(name)
193
+ deps.pop(name, None)
194
+ fs.write(manifest, config)
195
+ # Keep bundle files for running sessions (some presets resolve lazily).
196
+ link = manifest.parent / "node_modules" / name
197
+ if link.is_symlink() and link.resolve() == Path(record["bundle"]):
198
+ link.unlink()
199
+
200
+
201
+ def main():
202
+ p = argparse.ArgumentParser()
203
+ p.add_argument("action", choices=["install", "upgrade", "remove", "status"])
204
+ p.add_argument("host", choices=["pi", "dsh"], nargs="?")
205
+ p.add_argument("--profile")
206
+ p.add_argument("--state", type=Path)
207
+ p.add_argument("--ratio")
208
+ p.add_argument("--context-window")
209
+ p.add_argument("--json", action="store_true")
210
+ args = p.parse_args()
211
+ root = (args.state or state_root()).expanduser().resolve()
212
+ host = args.host
213
+ if args.profile and (
214
+ host != "dsh"
215
+ or not re.fullmatch(r"[\w-]+", args.profile)
216
+ or args.profile == "node_modules"
217
+ ):
218
+ p.error("invalid DSH profile")
219
+ key = args.profile or ("web" if host == "dsh" else "user")
220
+ if args.action == "status":
221
+ data = report(host, root=root)
222
+ for install in data["installations"]:
223
+ name = install["host"]
224
+ receipt = fs.read(root / "integrations" / (name + ".json"))
225
+ if receipt.get("autoload"):
226
+ install.update(
227
+ mode="host",
228
+ launch=name if name == "pi" else "dsh --profile PROFILE",
229
+ registrations=sorted(receipt["autoload"]),
230
+ )
231
+ print(json.dumps(data))
232
+ return
233
+ path = root / "integrations" / (host + ".json")
234
+ with fs.lock(path.parent):
235
+ old = fs.read(path)
236
+ registrations = dict(old.get("autoload", {}))
237
+ previous = registrations.get(key, {})
238
+ if args.action == "remove":
239
+ for item in [key] if args.profile or host == "pi" else list(registrations):
240
+ if item in registrations:
241
+ remove_record(host, registrations.pop(item))
242
+ fs.write(
243
+ path,
244
+ {
245
+ **old,
246
+ "host": host,
247
+ "autoload": registrations,
248
+ "removed": not registrations,
249
+ },
250
+ )
251
+ else:
252
+ platform = Path(os.environ["AFU_INSTALL_ROOT"]).resolve()
253
+ adapter = platform / "src/afu_host_integrations" / host / "dist/index.js"
254
+ if not adapter.is_file():
255
+ raise ValueError("Adapter not installed")
256
+
257
+ def policy(raw, field, cast):
258
+ if raw is None:
259
+ return previous.get(
260
+ field, old.get(field) if not registrations else None
261
+ )
262
+ return None if raw == "auto" else cast(raw)
263
+
264
+ ratio = policy(args.ratio, "ratio", float)
265
+ window = policy(args.context_window, "window", int)
266
+ validate_policy(window, ratio)
267
+ if window and ratio and int(window * ratio) < 1:
268
+ raise ValueError("The configured threshold must be at least one token")
269
+ settings = {
270
+ "AFU_CONTEXT_COMPACT_WORKER_COMMAND": sys.executable,
271
+ "AFU_CONTEXT_COMPACT_WORKER_ARGS": json.dumps(
272
+ [
273
+ "-m",
274
+ "afu_context",
275
+ "compact",
276
+ "worker",
277
+ "--factory",
278
+ "afu_context.local_worker:create_plugin",
279
+ ]
280
+ ),
281
+ "AFU_CONTEXT_COMPACT_WORKER_CWD": str(platform),
282
+ "AFU_INSTALL_ROOT": str(platform),
283
+ "AFU_STATE_DIR": str(root),
284
+ "AFU_HOST_CONTEXT_WINDOW_OVERRIDE": str(window or "auto"),
285
+ }
286
+ if ratio is not None:
287
+ settings["AFU_TRIGGER_RATIO"] = str(ratio)
288
+ if host == "pi":
289
+ record = install_pi(root, adapter, settings, previous)
290
+ else:
291
+ record = install_dsh(root, platform, adapter, settings, key, previous)
292
+ registrations[key] = {**record, "window": window, "ratio": ratio}
293
+ fs.write(
294
+ path,
295
+ {
296
+ **old,
297
+ "host": host,
298
+ "mode": "host",
299
+ "removed": False,
300
+ "window": window,
301
+ "ratio": ratio,
302
+ "build": fs.read(platform / "release.json")["build"],
303
+ "autoload": registrations,
304
+ },
305
+ )
306
+ print(
307
+ json.dumps(
308
+ {
309
+ "host": host,
310
+ "installed": bool(registrations),
311
+ "upgraded": args.action == "upgrade",
312
+ "mode": "host",
313
+ "registrations": sorted(registrations),
314
+ "launch": "pi" if host == "pi" else "dsh --profile " + key,
315
+ }
316
+ )
317
+ )
318
+
319
+
320
+ if __name__ == "__main__":
321
+ try:
322
+ main()
323
+ except (OSError, ValueError, KeyError, subprocess.TimeoutExpired) as error:
324
+ sys.exit(
325
+ "AFU registration failed: " + str(error)
326
+ if type(error) is ValueError
327
+ else "AFU registration failed: " + type(error).__name__
328
+ )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liwu1995/afu-context",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "description": "Local asynchronous context compression and diagnostics",