afucontext 0.1.18 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -16
- package/README.zh-CN.md +31 -16
- package/bin/afu.mjs +1 -1
- package/bin/host-autoload.py +13 -0
- package/package.json +1 -1
- package/python/{afu_context_local-0.1.18-py3-none-any.whl → afu_context_local-0.1.20-py3-none-any.whl} +0 -0
- package/release.json +3 -3
- package/src/afu_host_integrations/dsh/dist/index.js +83 -24
- package/src/afu_host_integrations/pi/dist/index.js +228 -50
- package/src/afu_host_integrations/shared/runtime-client/dist/index.js +1 -1
- package/src/afu_host_integrations/shared/runtime-client/dist/telemetry.js +48 -7
package/README.md
CHANGED
|
@@ -20,13 +20,13 @@ Install and sign in to your host first. You need npm, Node.js **22.19+ (22.x) or
|
|
|
20
20
|
|
|
21
21
|
| Host | System | Install | Launch after installation | Tested version |
|
|
22
22
|
| --- | --- | --- | --- | --- |
|
|
23
|
-
| <a id="afu-claude"></a>Claude Code | macOS / Linux | `npx --yes --registry=https://registry.npmjs.org afucontext@0.1.
|
|
24
|
-
| <a id="afu-codex"></a>Codex CLI | macOS / Linux | `npx --yes --registry=https://registry.npmjs.org afucontext@0.1.
|
|
25
|
-
| <a id="afu-pi"></a>Pi | macOS / Linux | `npx --yes --registry=https://registry.npmjs.org afucontext@0.1.
|
|
26
|
-
| <a id="afu-dsh"></a>DSH | macOS / Linux | `npx --yes --registry=https://registry.npmjs.org afucontext@0.1.
|
|
27
|
-
| Codex CLI | Windows 10/11¹ | `npx.cmd --yes --registry=https://registry.npmjs.org "afucontext@0.1.
|
|
28
|
-
| Pi | Windows 10/11¹ | `npx.cmd --yes --registry=https://registry.npmjs.org "afucontext@0.1.
|
|
29
|
-
| DSH | Windows 10/11¹ | `npx.cmd --yes --registry=https://registry.npmjs.org "afucontext@0.1.
|
|
23
|
+
| <a id="afu-claude"></a>Claude Code | macOS / Linux | `npx --yes --registry=https://registry.npmjs.org afucontext@0.1.20 install claude` | `claude` | 2.1.293 |
|
|
24
|
+
| <a id="afu-codex"></a>Codex CLI | macOS / Linux | `npx --yes --registry=https://registry.npmjs.org afucontext@0.1.20 install codex` | `codex` | 0.153.4 |
|
|
25
|
+
| <a id="afu-pi"></a>Pi | macOS / Linux | `npx --yes --registry=https://registry.npmjs.org afucontext@0.1.20 install pi` | `pi` | 0.84.4, 1.5.0 |
|
|
26
|
+
| <a id="afu-dsh"></a>DSH | macOS / Linux | `npx --yes --registry=https://registry.npmjs.org afucontext@0.1.20 install dsh --profile YOUR_PROFILE` | `dsh --profile YOUR_PROFILE` | 0.2.2-rc.1 |
|
|
27
|
+
| Codex CLI | Windows 10/11¹ | `npx.cmd --yes --registry=https://registry.npmjs.org "afucontext@0.1.20" install codex` | `codex` | preview, not yet tested; official in the next release |
|
|
28
|
+
| Pi | Windows 10/11¹ | `npx.cmd --yes --registry=https://registry.npmjs.org "afucontext@0.1.20" install pi` | `pi` | preview, not yet tested; official in the next release |
|
|
29
|
+
| DSH | Windows 10/11¹ | `npx.cmd --yes --registry=https://registry.npmjs.org "afucontext@0.1.20" install dsh --profile YOUR_PROFILE` | `dsh --profile YOUR_PROFILE` | preview, not yet tested; official in the next release |
|
|
30
30
|
|
|
31
31
|
Supported on macOS (Apple Silicon) and Linux x64. Start a new session after installing.
|
|
32
32
|
|
|
@@ -38,7 +38,7 @@ Supported on macOS (Apple Silicon) and Linux x64. Start a new session after inst
|
|
|
38
38
|
|
|
39
39
|
## Commands
|
|
40
40
|
|
|
41
|
-
The terminal commands (`afuctx …`) are placed in `~/.local/bin` by the first install
|
|
41
|
+
The terminal commands (`afuctx …`) are placed in `~/.local/bin` by the first install of any host (the folder is created when missing). If that folder is not on `PATH`, the installer says so: add `export PATH="$HOME/.local/bin:$PATH"` to `~/.bashrc` or `~/.zshrc`, or use `npx --yes --registry=https://registry.npmjs.org afucontext@latest` in place of `afuctx`. Install, upgrade and remove replace the runtime itself, so they always run through npx.
|
|
42
42
|
|
|
43
43
|
| Command | What it does |
|
|
44
44
|
| --- | --- |
|
|
@@ -48,6 +48,8 @@ The terminal commands (`afuctx …`) are placed in `~/.local/bin` by the first i
|
|
|
48
48
|
| `npx --yes --registry=https://registry.npmjs.org afucontext@latest remove HOST` | Remove; host settings return to their state before installation. For DSH, `--profile NAME` removes one profile |
|
|
49
49
|
| `afuctx status` | Whether each host is connected, how far the context is from the trigger, compactions applied; `--json` for the full record |
|
|
50
50
|
| `afuctx dashboard` | Open the local dashboard (same as `afuctx-admin`); `--prices FILE` estimates costs with your prices |
|
|
51
|
+
| `afuctx doctor` | Checks host → AFU → model end to end, fixes what AFU can and gives exact steps for the rest; add a host name to check one |
|
|
52
|
+
| `afuctx off` / `afuctx on` | Pause / resume: while paused the hosts talk to their models directly; the installation, logs and summaries stay. Add a host name for one host |
|
|
51
53
|
| `afuctx repair codex --network` | Find and verify the proxy route to the model again (`repair claude` for Claude Code) |
|
|
52
54
|
| `afuctx gc --dry-run` | List old runtimes and logs that can be removed; without `--dry-run` it removes them |
|
|
53
55
|
| `afuctx --version` / `afuctx --help` | Version / help |
|
|
@@ -60,6 +62,19 @@ Inside a host session:
|
|
|
60
62
|
| Codex | `$afuctx-status` | `$afuctx-dashboard` |
|
|
61
63
|
| Pi / DSH | `/afuctx-status` | `/afuctx-dashboard` |
|
|
62
64
|
|
|
65
|
+
## When something goes wrong
|
|
66
|
+
|
|
67
|
+
| What you see | Do first |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| After installing, the host cannot connect or keeps reconnecting | `afuctx off`: the host talks to its model directly right away. Then `afuctx doctor` to find the cause, and `afuctx on` once fixed |
|
|
70
|
+
| Status says the model service is unreachable | `afuctx doctor` (or, for the network only, `afuctx repair codex --network`) |
|
|
71
|
+
| A proxy app keeps the host from reaching AFU on this machine | Add `127.0.0.1` and `localhost` to the proxy app's bypass/exception list (for example AntFamily); `afuctx doctor` checks this |
|
|
72
|
+
| Settings or hooks were damaged | Run the install command again |
|
|
73
|
+
| No compaction after a reboot | Open a host session once (its hook starts AFU) |
|
|
74
|
+
| You no longer want AFU | `remove`: the host's settings return to how they were before install |
|
|
75
|
+
|
|
76
|
+
When AFU cannot reach the model service several times in a row, it looks for another working route by itself and verifies it (only AFU's own record changes); the dashboard shows it.
|
|
77
|
+
|
|
63
78
|
## Benchmark results
|
|
64
79
|
|
|
65
80
|
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**.
|
|
@@ -89,7 +104,7 @@ In a LongBench V2 replay of 39 history questions (20 agent histories and 19 dial
|
|
|
89
104
|
|
|
90
105
|
- The dashboard runs in the background and does not occupy a terminal; opening it again reuses the same one, and it exits after 30 minutes without an open page.
|
|
91
106
|
- It opens in the dark theme; switch to light, follow-system or English/Chinese in the top-right corner.
|
|
92
|
-
- The terminal commands are placed in `~/.local/bin` by the installer
|
|
107
|
+
- The terminal commands are placed in `~/.local/bin` by the installer for every host, creating the folder when needed. Shell startup files are never edited (the installer prints the line to add when the folder is not on `PATH`) and an existing command of the same name is kept (the installer says so). They are removed with the last integration, whichever host that is.
|
|
93
108
|
|
|
94
109
|
### Check the current session
|
|
95
110
|
|
|
@@ -98,8 +113,8 @@ Type `/afuctx:status` in Claude Code, `$afuctx-status` in Codex, `/afuctx-status
|
|
|
98
113
|
Without the terminal commands (for example when `~/.local/bin` is not on `PATH`), use the full npx commands. For Codex; for another host replace `codex` with `claude`, `pi` or `dsh`:
|
|
99
114
|
|
|
100
115
|
```sh
|
|
101
|
-
npx --yes --registry=https://registry.npmjs.org afucontext@0.1.
|
|
102
|
-
npx --yes --registry=https://registry.npmjs.org afucontext@0.1.
|
|
116
|
+
npx --yes --registry=https://registry.npmjs.org afucontext@0.1.20 status codex --brief
|
|
117
|
+
npx --yes --registry=https://registry.npmjs.org afucontext@0.1.20 ui
|
|
103
118
|
```
|
|
104
119
|
|
|
105
120
|
### What the dashboard shows
|
|
@@ -145,12 +160,12 @@ Run the same install command with options, then start a new session. For Codex;
|
|
|
145
160
|
|
|
146
161
|
```sh
|
|
147
162
|
# Trigger at 30% of a manually set 200,000-token window (60,000 tokens)
|
|
148
|
-
npx --yes --registry=https://registry.npmjs.org afucontext@0.1.
|
|
163
|
+
npx --yes --registry=https://registry.npmjs.org afucontext@0.1.20 install codex --context-window 200000 --ratio 0.3
|
|
149
164
|
# Restore automatic settings
|
|
150
|
-
npx --yes --registry=https://registry.npmjs.org afucontext@0.1.
|
|
165
|
+
npx --yes --registry=https://registry.npmjs.org afucontext@0.1.20 install codex --context-window auto --ratio auto
|
|
151
166
|
```
|
|
152
167
|
|
|
153
|
-
For frequent use, run `npm install -g --registry=https://registry.npmjs.org afucontext@0.1.
|
|
168
|
+
For frequent use, run `npm install -g --registry=https://registry.npmjs.org afucontext@0.1.20` and use `afuctx` in place of the full `npx` invocation. For example, `afuctx dashboard --prices ./afu-prices.json` opens the dashboard with your price file to estimate costs.
|
|
154
169
|
|
|
155
170
|
## Upgrade and remove
|
|
156
171
|
|
|
@@ -172,8 +187,8 @@ npx --yes --registry=https://registry.npmjs.org afucontext@latest status --brief
|
|
|
172
187
|
To upgrade or remove one host:
|
|
173
188
|
|
|
174
189
|
```sh
|
|
175
|
-
npx --yes --registry=https://registry.npmjs.org afucontext@0.1.
|
|
176
|
-
npx --yes --registry=https://registry.npmjs.org afucontext@0.1.
|
|
190
|
+
npx --yes --registry=https://registry.npmjs.org afucontext@0.1.20 upgrade codex
|
|
191
|
+
npx --yes --registry=https://registry.npmjs.org afucontext@0.1.20 remove codex
|
|
177
192
|
```
|
|
178
193
|
|
|
179
194
|
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,13 +20,13 @@
|
|
|
20
20
|
|
|
21
21
|
| 宿主 | 系统 | 一键安装 | 安装后启动 | 实测版本 |
|
|
22
22
|
| --- | --- | --- | --- | --- |
|
|
23
|
-
| <a id="afu-claude"></a>Claude Code | macOS / Linux | `npx --yes --registry=https://registry.npmjs.org afucontext@0.1.
|
|
24
|
-
| <a id="afu-codex"></a>Codex CLI | macOS / Linux | `npx --yes --registry=https://registry.npmjs.org afucontext@0.1.
|
|
25
|
-
| <a id="afu-pi"></a>Pi | macOS / Linux | `npx --yes --registry=https://registry.npmjs.org afucontext@0.1.
|
|
26
|
-
| <a id="afu-dsh"></a>DSH | macOS / Linux | `npx --yes --registry=https://registry.npmjs.org afucontext@0.1.
|
|
27
|
-
| Codex CLI | Windows 10/11¹ | `npx.cmd --yes --registry=https://registry.npmjs.org "afucontext@0.1.
|
|
28
|
-
| Pi | Windows 10/11¹ | `npx.cmd --yes --registry=https://registry.npmjs.org "afucontext@0.1.
|
|
29
|
-
| DSH | Windows 10/11¹ | `npx.cmd --yes --registry=https://registry.npmjs.org "afucontext@0.1.
|
|
23
|
+
| <a id="afu-claude"></a>Claude Code | macOS / Linux | `npx --yes --registry=https://registry.npmjs.org afucontext@0.1.20 install claude` | `claude` | 2.1.293 |
|
|
24
|
+
| <a id="afu-codex"></a>Codex CLI | macOS / Linux | `npx --yes --registry=https://registry.npmjs.org afucontext@0.1.20 install codex` | `codex` | 0.153.4 |
|
|
25
|
+
| <a id="afu-pi"></a>Pi | macOS / Linux | `npx --yes --registry=https://registry.npmjs.org afucontext@0.1.20 install pi` | `pi` | 0.84.4、1.5.0 |
|
|
26
|
+
| <a id="afu-dsh"></a>DSH | macOS / Linux | `npx --yes --registry=https://registry.npmjs.org afucontext@0.1.20 install dsh --profile YOUR_PROFILE` | `dsh --profile YOUR_PROFILE` | 0.2.2-rc.1 |
|
|
27
|
+
| Codex CLI | Windows 10/11¹ | `npx.cmd --yes --registry=https://registry.npmjs.org "afucontext@0.1.20" install codex` | `codex` | 预览,未实测,下一版本正式提供 |
|
|
28
|
+
| Pi | Windows 10/11¹ | `npx.cmd --yes --registry=https://registry.npmjs.org "afucontext@0.1.20" install pi` | `pi` | 预览,未实测,下一版本正式提供 |
|
|
29
|
+
| DSH | Windows 10/11¹ | `npx.cmd --yes --registry=https://registry.npmjs.org "afucontext@0.1.20" install dsh --profile YOUR_PROFILE` | `dsh --profile YOUR_PROFILE` | 预览,未实测,下一版本正式提供 |
|
|
30
30
|
|
|
31
31
|
支持 macOS(Apple Silicon)和 x64 Linux;安装后新开会话即可。
|
|
32
32
|
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
|
|
39
39
|
## 命令一览
|
|
40
40
|
|
|
41
|
-
终端命令(`afuctx
|
|
41
|
+
终端命令(`afuctx …`)在首次安装任一宿主后放到 `~/.local/bin`,目录不存在时会自动创建。该目录不在 `PATH` 时,安装程序会提示:把 `export PATH="$HOME/.local/bin:$PATH"` 加到 `~/.bashrc` 或 `~/.zshrc`,或者把 `afuctx` 换成 `npx --yes --registry=https://registry.npmjs.org afucontext@latest`。安装、升级、移除这类会替换运行时本身的命令,始终用 npx 执行。
|
|
42
42
|
|
|
43
43
|
| 命令 | 作用 |
|
|
44
44
|
| --- | --- |
|
|
@@ -48,6 +48,8 @@
|
|
|
48
48
|
| `npx --yes --registry=https://registry.npmjs.org afucontext@latest remove 宿主` | 移除,宿主配置恢复成安装前的样子;DSH 可加 `--profile 名称` 只移除一个 profile |
|
|
49
49
|
| `afuctx status` | 各宿主是否接入、当前上下文离触发线多远、已压缩几次;加 `--json` 输出完整信息 |
|
|
50
50
|
| `afuctx dashboard` | 打开本地面板(同 `afuctx-admin`);`--prices 文件` 用自己的单价估算费用 |
|
|
51
|
+
| `afuctx doctor` | 检查"宿主 → AFU → 模型"整条链路,能修的直接修,修不了的给出具体步骤;加宿主名只查一个 |
|
|
52
|
+
| `afuctx off` / `afuctx on` | 暂停 / 恢复:暂停时宿主直连模型,AFU 的安装、日志、摘要都保留;加宿主名只作用于一个 |
|
|
51
53
|
| `afuctx repair codex --network` | 重新查找并验证连到模型服务的代理(Claude Code 用 `repair claude`) |
|
|
52
54
|
| `afuctx gc --dry-run` | 查看可清理的旧版本运行时和日志;去掉 `--dry-run` 才会真正清理 |
|
|
53
55
|
| `afuctx --version` / `afuctx --help` | 版本号 / 帮助 |
|
|
@@ -60,6 +62,19 @@
|
|
|
60
62
|
| Codex | `$afuctx-status` | `$afuctx-dashboard` |
|
|
61
63
|
| Pi / DSH | `/afuctx-status` | `/afuctx-dashboard` |
|
|
62
64
|
|
|
65
|
+
## 出问题时怎么办
|
|
66
|
+
|
|
67
|
+
| 现象 | 先做 |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| 装了以后宿主连不上、一直重连 | `afuctx off`,宿主立即恢复直连模型;之后运行 `afuctx doctor` 查原因,修好后 `afuctx on` |
|
|
70
|
+
| 状态提示连不上模型服务 | `afuctx doctor`(只查网络也可以 `afuctx repair codex --network`) |
|
|
71
|
+
| 代理软件导致连不上本机 AFU | 在代理软件(如 AntFamily)的例外 / 绕过列表里加入 `127.0.0.1` 和 `localhost`;`afuctx doctor` 会检查 |
|
|
72
|
+
| 配置或 hook 被改坏 | 重新执行安装命令 |
|
|
73
|
+
| 重启后不压缩 | 开一次宿主会话(hook 会拉起 AFU) |
|
|
74
|
+
| 彻底不用 | `remove`,宿主配置恢复成安装前的样子 |
|
|
75
|
+
|
|
76
|
+
AFU 连续几次连不上模型服务时,会自动重新查找并验证一条能用的网络路线(只改 AFU 自己的记录),面板里能看到。
|
|
77
|
+
|
|
63
78
|
## 评测效果
|
|
64
79
|
|
|
65
80
|
在 LongBench V2 的 39 道历史问答回放中(20 道 Agent 历史、19 道对话历史),AFU 异步压缩答对 **23 道**,平均上下文为 **15.9k tokens**。
|
|
@@ -89,7 +104,7 @@
|
|
|
89
104
|
|
|
90
105
|
- 面板在后台运行,不占用终端;再次打开会复用同一个面板,30 分钟没有页面打开时自动退出。
|
|
91
106
|
- 默认暗色主题,右上角可切换亮色、跟随系统和中英文。
|
|
92
|
-
- 终端命令由安装程序放在 `~/.local/bin
|
|
107
|
+
- 终端命令由安装程序放在 `~/.local/bin`,四个宿主都会放,目录不存在时自动创建。不改 shell 配置(目录不在 `PATH` 时会提示要加的那一行),不覆盖你已有的同名命令(会提示未覆盖)。移除最后一个接入时一并删除,不论是哪个宿主。
|
|
93
108
|
|
|
94
109
|
### 查看当前会话
|
|
95
110
|
|
|
@@ -98,8 +113,8 @@ Claude Code 输入 `/afuctx:status`,Codex 输入 `$afuctx-status`,Pi 和 DSH
|
|
|
98
113
|
没有上面的终端命令时(例如 `~/.local/bin` 不在 `PATH` 中),可以用完整的 npx 命令。以 Codex 为例,其它宿主将 `codex` 换成 `claude`、`pi` 或 `dsh`:
|
|
99
114
|
|
|
100
115
|
```sh
|
|
101
|
-
npx --yes --registry=https://registry.npmjs.org afucontext@0.1.
|
|
102
|
-
npx --yes --registry=https://registry.npmjs.org afucontext@0.1.
|
|
116
|
+
npx --yes --registry=https://registry.npmjs.org afucontext@0.1.20 status codex --brief
|
|
117
|
+
npx --yes --registry=https://registry.npmjs.org afucontext@0.1.20 ui
|
|
103
118
|
```
|
|
104
119
|
|
|
105
120
|
### 面板内容
|
|
@@ -145,12 +160,12 @@ npx --yes --registry=https://registry.npmjs.org afucontext@0.1.18 ui
|
|
|
145
160
|
|
|
146
161
|
```sh
|
|
147
162
|
# 在 30% 触发,并手动指定 200,000 的窗口(即 60,000 触发)
|
|
148
|
-
npx --yes --registry=https://registry.npmjs.org afucontext@0.1.
|
|
163
|
+
npx --yes --registry=https://registry.npmjs.org afucontext@0.1.20 install codex --context-window 200000 --ratio 0.3
|
|
149
164
|
# 恢复自动设置
|
|
150
|
-
npx --yes --registry=https://registry.npmjs.org afucontext@0.1.
|
|
165
|
+
npx --yes --registry=https://registry.npmjs.org afucontext@0.1.20 install codex --context-window auto --ratio auto
|
|
151
166
|
```
|
|
152
167
|
|
|
153
|
-
如果经常使用 AFU,可运行 `npm install -g --registry=https://registry.npmjs.org afucontext@0.1.
|
|
168
|
+
如果经常使用 AFU,可运行 `npm install -g --registry=https://registry.npmjs.org afucontext@0.1.20`,以后用 `afuctx` 代替完整的 `npx` 命令。例如,`afuctx dashboard --prices ./afu-prices.json` 用自己的单价文件打开面板估算费用。
|
|
154
169
|
|
|
155
170
|
## 升级与移除
|
|
156
171
|
|
|
@@ -172,8 +187,8 @@ npx --yes --registry=https://registry.npmjs.org afucontext@latest status --brief
|
|
|
172
187
|
只升级或移除某一个宿主:
|
|
173
188
|
|
|
174
189
|
```sh
|
|
175
|
-
npx --yes --registry=https://registry.npmjs.org afucontext@0.1.
|
|
176
|
-
npx --yes --registry=https://registry.npmjs.org afucontext@0.1.
|
|
190
|
+
npx --yes --registry=https://registry.npmjs.org afucontext@0.1.20 upgrade codex
|
|
191
|
+
npx --yes --registry=https://registry.npmjs.org afucontext@0.1.20 remove codex
|
|
177
192
|
```
|
|
178
193
|
|
|
179
194
|
Pi 和 DSH 可分别用 `remove pi` 或 `remove dsh` 撤销。Windows PowerShell 请使用 `npx.cmd` 并将包名放在双引号中。
|
package/bin/afu.mjs
CHANGED
|
@@ -13,7 +13,7 @@ const root=resolve(dirname(fileURLToPath(import.meta.url)),'..');
|
|
|
13
13
|
const manifest=JSON.parse(readFileSync(join(root,'release.json'),'utf8'));
|
|
14
14
|
const env={...process.env,AFU_INSTALL_ROOT:root};
|
|
15
15
|
let args=process.argv.slice(2);
|
|
16
|
-
if(['install','upgrade','status','remove','gc','native','repair'].includes(args[0])){
|
|
16
|
+
if(['install','upgrade','status','remove','gc','native','repair','off','on','doctor'].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
18
|
const inline=args.find(a=>a.startsWith('--state='));if(inline&&inline.length>8)env.AFU_STATE_DIR=resolve(inline.slice(8));
|
|
19
19
|
}
|
package/bin/host-autoload.py
CHANGED
|
@@ -456,6 +456,8 @@ def main():
|
|
|
456
456
|
"autoload": registrations,
|
|
457
457
|
"plugin_runtime": plugin_runtime,
|
|
458
458
|
"removed": not registrations and not plugin_runtime,
|
|
459
|
+
# A removed integration is not paused: a later install starts active.
|
|
460
|
+
"paused": bool(old.get("paused")) and bool(registrations or plugin_runtime),
|
|
459
461
|
},
|
|
460
462
|
)
|
|
461
463
|
else:
|
|
@@ -512,6 +514,7 @@ def main():
|
|
|
512
514
|
"host": host,
|
|
513
515
|
"mode": "host",
|
|
514
516
|
"removed": False,
|
|
517
|
+
"paused": bool(old.get("paused")) and not old.get("removed"),
|
|
515
518
|
"plugin_runtime": args.runtime_only or bool(old.get("plugin_runtime")),
|
|
516
519
|
**({"host_version": host_version} if host_version else {}),
|
|
517
520
|
"window": window,
|
|
@@ -521,6 +524,11 @@ def main():
|
|
|
521
524
|
"autoload": registrations,
|
|
522
525
|
},
|
|
523
526
|
)
|
|
527
|
+
from afu_context.management import active_integrations, install_shortcut, remove_shortcut, shortcut_notes
|
|
528
|
+
if args.action == "remove":
|
|
529
|
+
shortcut, removed = None, (remove_shortcut() if not active_integrations(root) else False)
|
|
530
|
+
else:
|
|
531
|
+
shortcut, removed = install_shortcut(root), None
|
|
524
532
|
print(
|
|
525
533
|
json.dumps(
|
|
526
534
|
{
|
|
@@ -530,9 +538,14 @@ def main():
|
|
|
530
538
|
"mode": "host",
|
|
531
539
|
"registrations": sorted(registrations),
|
|
532
540
|
"launch": "pi" if host == "pi" else "dsh --profile " + key,
|
|
541
|
+
**({"shortcut": shortcut} if shortcut is not None else {}),
|
|
542
|
+
**({"shortcut_removed": removed} if removed is not None else {}),
|
|
533
543
|
}
|
|
534
544
|
)
|
|
535
545
|
)
|
|
546
|
+
if not args.json:
|
|
547
|
+
for line in shortcut_notes(shortcut):
|
|
548
|
+
print("AFU: " + line, file=sys.stderr)
|
|
536
549
|
|
|
537
550
|
|
|
538
551
|
if __name__ == "__main__":
|
package/package.json
CHANGED
|
Binary file
|
package/release.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.1.
|
|
2
|
+
"version": "0.1.20",
|
|
3
3
|
"name": "afucontext",
|
|
4
|
-
"build": "
|
|
5
|
-
"wheel": "afu_context_local-0.1.
|
|
4
|
+
"build": "7103b588cc60356b806eefbe524a14ee65eb28e04c9536bf6c022892af50c437",
|
|
5
|
+
"wheel": "afu_context_local-0.1.20-py3-none-any.whl",
|
|
6
6
|
"prebuilt": false
|
|
7
7
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BasicCompactionEngine as BundledBasicCompactionEngine, } from "@deepseek-ai/dsh-compaction-basic";
|
|
2
2
|
import { BlockAssembler as BundledBlockAssembler, createUserMessage as bundledCreateUserMessage, } from "@deepseek-ai/dsh-llm";
|
|
3
|
-
import { journal, sessionHash, afuTrigger, summaryPrefix, observationId, HostCompactPool, bindingKey, estimateContentTokens, findAppliedArtifact, normalizeContent, } from "../../shared/runtime-client/dist/index.js";
|
|
3
|
+
import { journal, sessionHash, afuTrigger, afuPaused, summaryPrefix, observationId, HostCompactPool, bindingKey, estimateContentTokens, findAppliedArtifact, normalizeContent, } from "../../shared/runtime-client/dist/index.js";
|
|
4
4
|
const hostModules = (globalThis.__afuDshHost) ?? {};
|
|
5
5
|
const BasicCompactionEngine = typeof hostModules.BasicCompactionEngine === "function" ? hostModules.BasicCompactionEngine : BundledBasicCompactionEngine;
|
|
6
6
|
const BlockAssembler = typeof hostModules.BlockAssembler === "function" ? hostModules.BlockAssembler : BundledBlockAssembler;
|
|
@@ -78,7 +78,7 @@ export class CompactDshEngine extends BasicCompactionEngine {
|
|
|
78
78
|
ctx.logger.warn("compact idle preparation unavailable; native compaction remains active");
|
|
79
79
|
});
|
|
80
80
|
ctx.on("agent/status", async ({ agent, status }) => {
|
|
81
|
-
if (status !== "idle")
|
|
81
|
+
if (status !== "idle" || afuPaused("dsh"))
|
|
82
82
|
return;
|
|
83
83
|
// Context size as DSH's own token meter prices it, plus the AFU trigger for the routed model.
|
|
84
84
|
let tokens, window;
|
|
@@ -140,6 +140,9 @@ export class CompactDshEngine extends BasicCompactionEngine {
|
|
|
140
140
|
return tokens === undefined ? request : { ...request, metadata: { ...request.metadata, host_context_tokens: tokens } };
|
|
141
141
|
}
|
|
142
142
|
async compactIfNeeded(agent, trigger, signal) {
|
|
143
|
+
// afuctx off: DSH's own compaction, unchanged.
|
|
144
|
+
if (afuPaused("dsh"))
|
|
145
|
+
return super.compactIfNeeded(agent, trigger, signal);
|
|
143
146
|
const started = performance.now();
|
|
144
147
|
this.handoffCommitted.delete(agent);
|
|
145
148
|
try {
|
|
@@ -200,6 +203,8 @@ export class CompactDshEngine extends BasicCompactionEngine {
|
|
|
200
203
|
}
|
|
201
204
|
}
|
|
202
205
|
async summarize(input, agent, signal) {
|
|
206
|
+
if (afuPaused("dsh"))
|
|
207
|
+
return super.summarize(input, agent, signal);
|
|
203
208
|
let request;
|
|
204
209
|
try {
|
|
205
210
|
request = await this.boundRequest(agent, input.messages, "sync");
|
|
@@ -220,6 +225,12 @@ export class CompactDshEngine extends BasicCompactionEngine {
|
|
|
220
225
|
this.handoff(agent, prepared);
|
|
221
226
|
return { ...preparedSummary, provider: target.provider, model: target.model };
|
|
222
227
|
}
|
|
228
|
+
if (prepared !== undefined) {
|
|
229
|
+
// Previously only slower; now visible: the advance summary did not fit.
|
|
230
|
+
const artifact = findAppliedArtifact(prepared);
|
|
231
|
+
this.log("summary_skipped", { session: sessionHash(agent.session.id), reason: "host_prepared_unused",
|
|
232
|
+
...(observationId(prepared) ?? artifact?.artifactId ? { artifact_id: observationId(prepared) ?? artifact?.artifactId } : {}) });
|
|
233
|
+
}
|
|
223
234
|
try {
|
|
224
235
|
const result = await this.compactClient.compact(request, signal);
|
|
225
236
|
const summary = dshSummaryFromResult(request, result);
|
|
@@ -400,22 +411,67 @@ export function dshMessagesToRequest(messages, sessionId, mode = "active") {
|
|
|
400
411
|
};
|
|
401
412
|
}
|
|
402
413
|
/**
|
|
403
|
-
*
|
|
404
|
-
*
|
|
414
|
+
* Where a prepared summary's coverage lies in a request, matched loosely.
|
|
415
|
+
*
|
|
416
|
+
* DSH messages have stable ids. The summary's last covered id is the anchor
|
|
417
|
+
* and must be present; the region runs from the first covered id found to the
|
|
418
|
+
* anchor. Items inside it the summary does not know (inserted by DSH or a
|
|
419
|
+
* plugin) are replaced with the rest, as long as they are few; covered ids
|
|
420
|
+
* that are gone do not matter. System items are context, never coverage.
|
|
421
|
+
*/
|
|
422
|
+
export function dshLooseCover(items, replacedIds) {
|
|
423
|
+
const anchor = replacedIds.at(-1);
|
|
424
|
+
let end = -1;
|
|
425
|
+
for (let k = items.length - 1; k >= 0; k--)
|
|
426
|
+
if (items[k].id === anchor) {
|
|
427
|
+
end = k;
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
if (anchor === undefined || end < 0)
|
|
431
|
+
return { reason: "anchor_not_found" };
|
|
432
|
+
const known = new Set(replacedIds);
|
|
433
|
+
const start = items.findIndex((item) => known.has(item.id));
|
|
434
|
+
// Covered ids out of their recorded order, or after the anchor: not the
|
|
435
|
+
// history it summarized.
|
|
436
|
+
for (let k = end + 1; k < items.length; k++)
|
|
437
|
+
if (known.has(items[k].id))
|
|
438
|
+
return { reason: "anchor_not_found" };
|
|
439
|
+
const order = new Map(replacedIds.map((id, k) => [id, k]));
|
|
440
|
+
let last = -1;
|
|
441
|
+
for (let k = start; k <= end; k++) {
|
|
442
|
+
const position = order.get(items[k].id);
|
|
443
|
+
if (position === undefined)
|
|
444
|
+
continue;
|
|
445
|
+
if (position < last)
|
|
446
|
+
return { reason: "anchor_not_found" };
|
|
447
|
+
last = position;
|
|
448
|
+
}
|
|
449
|
+
let unknown = 0, found = 0;
|
|
450
|
+
for (let k = start; k <= end; k++) {
|
|
451
|
+
if (items[k].kind === "system")
|
|
452
|
+
continue;
|
|
453
|
+
if (known.has(items[k].id))
|
|
454
|
+
found++;
|
|
455
|
+
else
|
|
456
|
+
unknown++;
|
|
457
|
+
}
|
|
458
|
+
if (unknown > Math.max(3, Math.floor(0.2 * (unknown + found))) || unknown > found)
|
|
459
|
+
return { reason: "low_overlap" };
|
|
460
|
+
return { start, end, unknown };
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Resolve a prepared Artifact to the DSH surface span it covers (loosely; see
|
|
464
|
+
* dshLooseCover). Anything that cannot be located is rejected without mutation.
|
|
405
465
|
*/
|
|
406
466
|
export function dshPreparedRange(session, request, result) {
|
|
407
467
|
const artifact = findAppliedArtifact(result);
|
|
408
468
|
if (artifact === undefined)
|
|
409
469
|
return undefined;
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
if (startIndex < 0)
|
|
470
|
+
const cover = dshLooseCover(request.items, artifact.replacedItemIds);
|
|
471
|
+
if ("reason" in cover)
|
|
413
472
|
return undefined;
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
coveredIds.some((id, index) => id !== artifact.replacedItemIds[index])) {
|
|
417
|
-
return undefined;
|
|
418
|
-
}
|
|
473
|
+
const startId = request.items[cover.start].id;
|
|
474
|
+
const endId = request.items[cover.end].id;
|
|
419
475
|
const seqByMessageId = new Map();
|
|
420
476
|
for (const seq of session.surface.nodes) {
|
|
421
477
|
const event = session.eventAt(seq);
|
|
@@ -425,26 +481,29 @@ export function dshPreparedRange(session, request, result) {
|
|
|
425
481
|
if (message !== null)
|
|
426
482
|
seqByMessageId.set(String(message.id), seq);
|
|
427
483
|
}
|
|
428
|
-
const start = seqByMessageId.get(
|
|
429
|
-
const end = seqByMessageId.get(
|
|
484
|
+
const start = seqByMessageId.get(startId);
|
|
485
|
+
const end = seqByMessageId.get(endId);
|
|
430
486
|
return start === undefined || end === undefined ? undefined : { start, end };
|
|
431
487
|
}
|
|
432
|
-
/**
|
|
488
|
+
/**
|
|
489
|
+
* A DSH summary when one Artifact covers the region DSH selected (loosely):
|
|
490
|
+
* its anchor must be the region's last conversation item, so nothing DSH
|
|
491
|
+
* keeps after the region is described or lost; a few unknown items inside the
|
|
492
|
+
* region are summarized away with it.
|
|
493
|
+
*/
|
|
433
494
|
export function dshSummaryFromResult(request, result) {
|
|
434
495
|
const artifact = findAppliedArtifact(result);
|
|
435
496
|
if (artifact === undefined || typeof artifact.item.content !== "string")
|
|
436
497
|
return undefined;
|
|
498
|
+
const conversation = request.items.filter((item) => item.kind !== "system");
|
|
499
|
+
if (conversation.at(-1)?.id !== artifact.replacedItemIds.at(-1))
|
|
500
|
+
return undefined;
|
|
437
501
|
// DSH 0.2 replays the system head in front of the region it summarizes;
|
|
438
|
-
// the summary replaces only the region, so
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
first++;
|
|
443
|
-
const requestIds = request.items.slice(first).map((item) => item.id);
|
|
444
|
-
if (artifact.replacedItemIds.length !== requestIds.length ||
|
|
445
|
-
artifact.replacedItemIds.some((id, index) => id !== requestIds[index])) {
|
|
502
|
+
// the summary replaces only the region, so system messages are context.
|
|
503
|
+
const known = new Set(artifact.replacedItemIds);
|
|
504
|
+
const unknown = conversation.filter((item) => !known.has(item.id)).length;
|
|
505
|
+
if (unknown > Math.max(3, Math.floor(0.2 * conversation.length)) || unknown > conversation.length - unknown)
|
|
446
506
|
return undefined;
|
|
447
|
-
}
|
|
448
507
|
const summary = [{ type: "text", text: artifact.item.content }];
|
|
449
508
|
return {
|
|
450
509
|
summary,
|
|
@@ -1,5 +1,29 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { sessionEntryToContextMessages, convertToLlm, estimateTokens, getAgentDir, SettingsManager, DEFAULT_COMPACTION_SETTINGS } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { journal, sessionHash, afuTrigger, summaryPrefix, observationId, afuStatus, afuDashboard, STATUS_COMMAND, DASHBOARD_COMMAND, CompactWorkerClient, HostCompactPool, bindingKey, findAppliedArtifact, normalizeContent, } from "../../shared/runtime-client/dist/index.js";
|
|
3
|
+
import { journal, sessionHash, afuTrigger, afuPaused, summaryPrefix, observationId, afuStatus, afuDashboard, STATUS_COMMAND, DASHBOARD_COMMAND, CompactWorkerClient, HostCompactPool, bindingKey, findAppliedArtifact, normalizeContent, } from "../../shared/runtime-client/dist/index.js";
|
|
4
|
+
/** Requests in a row a ready summary may fail to fit before it is dropped. */
|
|
5
|
+
export const MAX_MISSES = 3;
|
|
6
|
+
/**
|
|
7
|
+
* The conversation as Pi sends it to the model, entry by entry.
|
|
8
|
+
*
|
|
9
|
+
* Pi 1.5 builds requests from buildSessionProjection(): context edits applied,
|
|
10
|
+
* an older compaction kept inside the newest retained range contributes
|
|
11
|
+
* nothing, and a compaction entry contributes its system message and its
|
|
12
|
+
* summary. Pi 0.84 has no projection; its context entries map one to one.
|
|
13
|
+
*/
|
|
14
|
+
export function piProjectedEntries(manager) {
|
|
15
|
+
const view = manager;
|
|
16
|
+
if (typeof view.buildSessionProjection === "function") {
|
|
17
|
+
return view.buildSessionProjection().entries.map((projected) => ({ entry: projected.sourceEntry, messages: projected.messages }));
|
|
18
|
+
}
|
|
19
|
+
return project(view.buildContextEntries());
|
|
20
|
+
}
|
|
21
|
+
function project(entries) {
|
|
22
|
+
return entries.map((item) => isProjected(item) ? item : { entry: item, messages: sessionEntryToContextMessages(item) });
|
|
23
|
+
}
|
|
24
|
+
function isProjected(item) {
|
|
25
|
+
return isRecord(item) && "entry" in item && Array.isArray(item.messages);
|
|
26
|
+
}
|
|
3
27
|
export function createCompactPiExtension(options = {}) {
|
|
4
28
|
return function compactPi(pi) {
|
|
5
29
|
const injected = options.worker instanceof CompactWorkerClient ? options.worker : undefined;
|
|
@@ -17,7 +41,7 @@ export function createCompactPiExtension(options = {}) {
|
|
|
17
41
|
let automatic;
|
|
18
42
|
const fingerprint = (ctx) => bindingKey([
|
|
19
43
|
ctx.sessionManager.getSessionId(), ctx.model,
|
|
20
|
-
ctx.sessionManager.
|
|
44
|
+
piProjectedEntries(ctx.sessionManager).map((item) => [item.entry.id, item.messages]),
|
|
21
45
|
]);
|
|
22
46
|
const invalidate = () => {
|
|
23
47
|
epoch++;
|
|
@@ -47,6 +71,9 @@ export function createCompactPiExtension(options = {}) {
|
|
|
47
71
|
// very next model request (as the Codex/Claude Code gateway and DSH do),
|
|
48
72
|
// whether or not Pi's own compaction later accepts them.
|
|
49
73
|
const ready = new Map();
|
|
74
|
+
// Consecutive requests a ready summary did not fit; after MAX_MISSES it is
|
|
75
|
+
// dropped so the next settled turn prepares one for the current history.
|
|
76
|
+
const misses = new WeakMap();
|
|
50
77
|
// What the next model request carries, recorded when Pi builds it and
|
|
51
78
|
// settled by the assistant message that answers it (request_receipt).
|
|
52
79
|
const outgoing = new Map();
|
|
@@ -55,9 +82,27 @@ export function createCompactPiExtension(options = {}) {
|
|
|
55
82
|
// next compaction; those requests are the ones it made smaller.
|
|
56
83
|
const committed = new Map();
|
|
57
84
|
pi.on("context", async (event, ctx) => {
|
|
85
|
+
// afuctx off: Pi sends its own requests unchanged.
|
|
86
|
+
if (afuPaused("pi"))
|
|
87
|
+
return undefined;
|
|
58
88
|
const sid = ctx.sessionManager.getSessionId();
|
|
59
89
|
const summary = ready.get(sid);
|
|
60
|
-
const
|
|
90
|
+
const match = summary ? matchPreparedSummary(event.messages, summary) : undefined;
|
|
91
|
+
const messages = match && "messages" in match ? match.messages : undefined;
|
|
92
|
+
if (summary && match && "reason" in match) {
|
|
93
|
+
const count = (misses.get(summary) ?? 0) + 1;
|
|
94
|
+
misses.set(summary, count);
|
|
95
|
+
// Structure only (positions, counts, a role); never message text.
|
|
96
|
+
if (count === 1)
|
|
97
|
+
log("summary_skipped", { session: sessionHash(sid), artifact_id: summary.artifactId, reason: match.reason,
|
|
98
|
+
...(match.anchor !== undefined ? { anchor_position: match.anchor } : {}),
|
|
99
|
+
...(match.unknown !== undefined ? { unknown_messages: match.unknown } : {}),
|
|
100
|
+
...(match.firstUnknownRole ? { first_unknown_role: match.firstUnknownRole } : {}) });
|
|
101
|
+
if (count >= MAX_MISSES) {
|
|
102
|
+
ready.delete(sid);
|
|
103
|
+
log("summary_discarded", { session: sessionHash(sid), artifact_id: summary.artifactId, reason: "unmatched" });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
61
106
|
if (!summary || !messages) {
|
|
62
107
|
const carried = committed.get(sid);
|
|
63
108
|
const first = event.messages[0];
|
|
@@ -67,6 +112,7 @@ export function createCompactPiExtension(options = {}) {
|
|
|
67
112
|
return undefined;
|
|
68
113
|
}
|
|
69
114
|
outgoing.set(sid, { applied: true, ...(summary.artifactId ? { artifactId: summary.artifactId } : {}), removed: summary.removedTokens });
|
|
115
|
+
misses.delete(summary);
|
|
70
116
|
log("summary_applied", { session: sessionHash(sid), artifact_id: summary.artifactId, phase: "host_request", removed_tokens_estimate: summary.removedTokens });
|
|
71
117
|
return { messages: messages };
|
|
72
118
|
});
|
|
@@ -110,6 +156,8 @@ export function createCompactPiExtension(options = {}) {
|
|
|
110
156
|
};
|
|
111
157
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
112
158
|
invalidate();
|
|
159
|
+
if (afuPaused("pi"))
|
|
160
|
+
return;
|
|
113
161
|
const generation = epoch;
|
|
114
162
|
// Context size as Pi itself measures it, for the dashboard's progress to the AFU trigger.
|
|
115
163
|
let usage;
|
|
@@ -124,12 +172,12 @@ export function createCompactPiExtension(options = {}) {
|
|
|
124
172
|
...(usage?.tokens == null ? {} : { estimated_input_tokens: usage.tokens }),
|
|
125
173
|
...afuTrigger(usage?.contextWindow),
|
|
126
174
|
});
|
|
127
|
-
const entries = ctx.sessionManager
|
|
175
|
+
const entries = piProjectedEntries(ctx.sessionManager);
|
|
128
176
|
const visible = modelVisibleEntries(entries);
|
|
129
177
|
const latest = visible.at(-1);
|
|
130
178
|
if (latest === undefined)
|
|
131
179
|
return;
|
|
132
|
-
const request = piEntriesToRequest(entries, ctx, new Set([latest.id]));
|
|
180
|
+
const request = piEntriesToRequest(entries, ctx, new Set([latest.entry.id]));
|
|
133
181
|
if (request === undefined)
|
|
134
182
|
return;
|
|
135
183
|
try {
|
|
@@ -189,12 +237,14 @@ export function createCompactPiExtension(options = {}) {
|
|
|
189
237
|
});
|
|
190
238
|
pi.on("session_before_compact", async (event, ctx) => {
|
|
191
239
|
invalidate();
|
|
240
|
+
if (afuPaused("pi"))
|
|
241
|
+
return undefined; // Pi's own compaction, unchanged.
|
|
192
242
|
const started = performance.now();
|
|
193
|
-
const entries = modelVisibleEntries(ctx.sessionManager
|
|
243
|
+
const entries = modelVisibleEntries(piProjectedEntries(ctx.sessionManager));
|
|
194
244
|
const latest = entries.at(-1);
|
|
195
245
|
if (latest === undefined)
|
|
196
246
|
return;
|
|
197
|
-
const requiredIds = new Set([latest.id]);
|
|
247
|
+
const requiredIds = new Set([latest.entry.id]);
|
|
198
248
|
const request = piEntriesToRequest(entries, ctx, requiredIds);
|
|
199
249
|
if (request === undefined)
|
|
200
250
|
return;
|
|
@@ -287,8 +337,8 @@ export async function piHostBinding(ctx, pi) {
|
|
|
287
337
|
contextWindow: model.contextWindow,
|
|
288
338
|
current: async () => key === await identity(),
|
|
289
339
|
complete: async (prompt, signal, ids) => {
|
|
290
|
-
const entries = ids?.length ? modelVisibleEntries(ctx.sessionManager
|
|
291
|
-
const selected = summaryPrefix(entries.map(
|
|
340
|
+
const entries = ids?.length ? modelVisibleEntries(piProjectedEntries(ctx.sessionManager)) : [];
|
|
341
|
+
const selected = summaryPrefix(entries.map(item => ({ id: item.entry.id, messages: convertToLlm(item.messages) })), ids, prompt);
|
|
292
342
|
const definitions = pi?.getAllTools();
|
|
293
343
|
const tools = pi?.getActiveTools().map(name => {
|
|
294
344
|
const tool = definitions?.find(t => t.name === name);
|
|
@@ -322,19 +372,23 @@ export function piEntriesToRequest(entries, ctx, requiredIds) {
|
|
|
322
372
|
if (visible.length < 2)
|
|
323
373
|
return undefined;
|
|
324
374
|
const callGroups = toolCallGroups(visible);
|
|
325
|
-
const items = visible.map((
|
|
326
|
-
const
|
|
375
|
+
const items = visible.map((item) => {
|
|
376
|
+
const entry = item.entry;
|
|
377
|
+
// A compaction entry contributes its system message and its summary; the
|
|
378
|
+
// summary is the conversation content to carry forward.
|
|
379
|
+
const message = conversationMessage(item);
|
|
327
380
|
if (message === undefined)
|
|
328
381
|
throw new Error(`Pi entry ${entry.id} has no model-visible message`);
|
|
329
382
|
const record = message;
|
|
330
383
|
const role = typeof record.role === "string" ? record.role : "user";
|
|
331
384
|
const callId = typeof record.toolCallId === "string" ? record.toolCallId : undefined;
|
|
332
385
|
const atomicGroup = callId === undefined ? callGroups.entryGroups.get(entry.id) : callGroups.calls.get(callId);
|
|
386
|
+
const content = record.content ?? (typeof record.summary === "string" ? record.summary : ("summary" in entry ? entry.summary : ""));
|
|
333
387
|
return {
|
|
334
388
|
schema_version: "1",
|
|
335
389
|
id: entry.id,
|
|
336
390
|
kind: entry.type === "compaction" ? "artifact" : piKind(role),
|
|
337
|
-
content: normalizeContent(
|
|
391
|
+
content: normalizeContent(content),
|
|
338
392
|
source: { schema_version: "1", provider: "pi.session", source_id: entry.id },
|
|
339
393
|
required: requiredIds.has(entry.id),
|
|
340
394
|
...(atomicGroup === undefined ? {} : { atomic_group: atomicGroup }),
|
|
@@ -373,7 +427,7 @@ export function piCompactionFromResult(entries, result, tokensBefore) {
|
|
|
373
427
|
if (artifact === undefined || typeof artifact.item.content !== "string")
|
|
374
428
|
return undefined;
|
|
375
429
|
const visible = modelVisibleEntries(entries);
|
|
376
|
-
const visibleIds = visible.map((
|
|
430
|
+
const visibleIds = visible.map((item) => item.entry.id);
|
|
377
431
|
const start = coveredStart(visibleIds, artifact.replacedItemIds, visible);
|
|
378
432
|
if (start < 0 || start + artifact.replacedItemIds.length >= visibleIds.length)
|
|
379
433
|
return undefined;
|
|
@@ -425,70 +479,188 @@ function piKeepRecentTokens(ctx) {
|
|
|
425
479
|
*/
|
|
426
480
|
export function piCanCompact(ctx, visible, keepRecent = piKeepRecentTokens(ctx)) {
|
|
427
481
|
let total = 0;
|
|
428
|
-
for (const entry of visible) {
|
|
482
|
+
for (const { entry, messages } of project(visible)) {
|
|
429
483
|
if (entry.type === "compaction") {
|
|
430
484
|
total = 0;
|
|
431
485
|
continue;
|
|
432
486
|
}
|
|
433
|
-
for (const message of
|
|
487
|
+
for (const message of messages)
|
|
434
488
|
total += estimateTokens(message);
|
|
435
489
|
}
|
|
436
490
|
return total > keepRecent;
|
|
437
491
|
}
|
|
492
|
+
/**
|
|
493
|
+
* A message's identity in Pi's request: role, timestamp and tool call id.
|
|
494
|
+
* Every Pi message carries a timestamp; parallel tool results can share one
|
|
495
|
+
* millisecond, and their call id tells them apart. Nothing is written into
|
|
496
|
+
* Pi's messages, which end up in the user's session file.
|
|
497
|
+
*/
|
|
498
|
+
export function piFingerprint(message) {
|
|
499
|
+
const m = (message ?? {});
|
|
500
|
+
return hash(JSON.stringify([m.role ?? null, m.timestamp ?? null, m.toolCallId ?? null])).slice(0, 24);
|
|
501
|
+
}
|
|
502
|
+
function contentHash(message) {
|
|
503
|
+
const m = (message ?? {});
|
|
504
|
+
return hash(JSON.stringify(m.content ?? m.summary ?? null)).slice(0, 16);
|
|
505
|
+
}
|
|
506
|
+
function hash(text) {
|
|
507
|
+
return createHash("sha256").update(text).digest("hex");
|
|
508
|
+
}
|
|
438
509
|
/** A ready result that replaces a leading run of the given entries. */
|
|
439
|
-
export function preparedSummary(
|
|
510
|
+
export function preparedSummary(entries, result) {
|
|
440
511
|
const artifact = findAppliedArtifact(result);
|
|
441
512
|
if (artifact === undefined || typeof artifact.item.content !== "string")
|
|
442
513
|
return undefined;
|
|
443
|
-
const
|
|
514
|
+
const visible = modelVisibleEntries(entries);
|
|
515
|
+
const ids = visible.map((item) => item.entry.id);
|
|
444
516
|
const start = coveredStart(ids, artifact.replacedItemIds, visible);
|
|
445
517
|
if (start < 0 || start + artifact.replacedItemIds.length >= ids.length)
|
|
446
518
|
return undefined;
|
|
447
|
-
|
|
448
|
-
|
|
519
|
+
// Everything before the end of the covered range, without system messages:
|
|
520
|
+
// Pi 1.5 drops them from the `context` view, and they are kept verbatim.
|
|
521
|
+
const covered = visible.slice(0, start + artifact.replacedItemIds.length)
|
|
522
|
+
.flatMap((item) => item.messages).filter((message) => !isSystemMessage(message));
|
|
523
|
+
if (covered.length === 0)
|
|
524
|
+
return undefined;
|
|
525
|
+
const replaced = visible.slice(start, start + artifact.replacedItemIds.length).flatMap((item) => item.messages);
|
|
449
526
|
const replacedChars = JSON.stringify(replaced).length;
|
|
450
527
|
// Same id the summary_started/summary_ready events carry, so the dashboard links them.
|
|
451
528
|
const artifactId = observationId(result) ?? artifact.artifactId;
|
|
452
529
|
return {
|
|
453
|
-
|
|
454
|
-
|
|
530
|
+
fingerprints: covered.map(piFingerprint),
|
|
531
|
+
contents: covered.map(contentHash),
|
|
455
532
|
summary: artifact.item.content,
|
|
456
533
|
...(artifactId === undefined ? {} : { artifactId }),
|
|
457
534
|
removedTokens: Math.max(0, Math.round((replacedChars - artifact.item.content.length) / 4)),
|
|
458
535
|
};
|
|
459
536
|
}
|
|
460
537
|
/**
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
538
|
+
* Where a ready summary goes in this request, if anywhere.
|
|
539
|
+
*
|
|
540
|
+
* The summary covers the conversation up to an anchor (its last message). The
|
|
541
|
+
* request is matched loosely: the anchor must be present and its content
|
|
542
|
+
* unchanged; messages before it the summary does not know (inserted by Pi or
|
|
543
|
+
* an extension, of a new kind) are replaced with the rest; messages it knows
|
|
544
|
+
* that are gone (omitted by a context edit) do not matter. Three cases are
|
|
545
|
+
* refused because they lose more than a few messages: the anchor is missing
|
|
546
|
+
* (another branch, rewound history), the anchor is in the current turn (the
|
|
547
|
+
* user's question would be removed), and most messages before the anchor are
|
|
548
|
+
* unknown (the anchor matched by chance). The cut never separates a tool call
|
|
549
|
+
* from its result: it moves back before the call.
|
|
550
|
+
*
|
|
551
|
+
* System messages stay where Pi expects them: those before the cut are kept,
|
|
552
|
+
* in order, ahead of the summary.
|
|
464
553
|
*/
|
|
465
|
-
export function
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
554
|
+
export function matchPreparedSummary(messages, ready) {
|
|
555
|
+
const conversation = [];
|
|
556
|
+
messages.forEach((message, index) => { if (!isSystemMessage(message))
|
|
557
|
+
conversation.push(index); });
|
|
558
|
+
const prints = conversation.map((index) => piFingerprint(messages[index]));
|
|
559
|
+
const anchorPrint = ready.fingerprints.at(-1);
|
|
560
|
+
let anchor = -1;
|
|
561
|
+
for (let k = prints.length - 1; k >= 0; k--)
|
|
562
|
+
if (prints[k] === anchorPrint) {
|
|
563
|
+
anchor = k;
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
if (anchor < 0)
|
|
567
|
+
return { reason: "anchor_not_found" };
|
|
568
|
+
if (contentHash(messages[conversation[anchor]]) !== ready.contents.at(-1))
|
|
569
|
+
return { reason: "content_changed", anchor };
|
|
570
|
+
// The user's current question must stay after the cut.
|
|
571
|
+
const lastUser = prints.reduce((found, _p, k) => roleOf(messages[conversation[k]]) === "user" ? k : found, -1);
|
|
572
|
+
if (anchor >= prints.length - 1 || lastUser <= anchor)
|
|
573
|
+
return { reason: "anchor_in_current_turn", anchor };
|
|
574
|
+
// Most of what lies before the anchor must be what the summary covers.
|
|
575
|
+
const known = new Map(ready.fingerprints.map((print, k) => [print, ready.contents[k]]));
|
|
576
|
+
let unknown = 0, firstUnknownRole;
|
|
577
|
+
for (let k = 0; k < anchor; k++) {
|
|
578
|
+
if (known.has(prints[k]))
|
|
579
|
+
continue;
|
|
580
|
+
unknown++;
|
|
581
|
+
firstUnknownRole ??= roleOf(messages[conversation[k]]);
|
|
582
|
+
}
|
|
583
|
+
if (unknown > Math.max(3, Math.floor(0.2 * anchor)) || unknown > anchor - unknown) {
|
|
584
|
+
return { reason: "low_overlap", anchor, unknown, ...(firstUnknownRole ? { firstUnknownRole } : {}) };
|
|
473
585
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
586
|
+
// A known message whose content changed (a context edit rewrote it) is no
|
|
587
|
+
// longer what the summary describes.
|
|
588
|
+
for (let k = 0; k < anchor; k++) {
|
|
589
|
+
const expected = known.get(prints[k]);
|
|
590
|
+
if (expected !== undefined && expected !== contentHash(messages[conversation[k]]))
|
|
591
|
+
return { reason: "content_changed", anchor };
|
|
592
|
+
}
|
|
593
|
+
// Never separate a tool call from its result.
|
|
594
|
+
let cut = anchor + 1;
|
|
595
|
+
cut = toolSafeCut(messages, conversation, cut);
|
|
596
|
+
if (cut <= 0)
|
|
597
|
+
return { reason: "anchor_in_current_turn", anchor };
|
|
598
|
+
const end = conversation[cut - 1] + 1;
|
|
599
|
+
const first = messages[conversation[0]];
|
|
600
|
+
return {
|
|
601
|
+
unknown,
|
|
602
|
+
messages: [
|
|
603
|
+
...messages.slice(0, end).filter(isSystemMessage),
|
|
604
|
+
{ role: "compactionSummary", summary: ready.summary, tokensBefore: ready.removedTokens, timestamp: first?.timestamp ?? Date.now() },
|
|
605
|
+
...messages.slice(end),
|
|
606
|
+
],
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
/** Earlier cut (in conversation positions) so no tool result is kept without its call. */
|
|
610
|
+
function toolSafeCut(messages, conversation, cut) {
|
|
611
|
+
const callsBefore = (limit) => {
|
|
612
|
+
const ids = new Set();
|
|
613
|
+
for (let k = 0; k < limit; k++)
|
|
614
|
+
for (const id of toolCallIds(messages[conversation[k]]))
|
|
615
|
+
ids.add(id);
|
|
616
|
+
return ids;
|
|
617
|
+
};
|
|
618
|
+
for (let guard = 0; guard < conversation.length && cut > 0; guard++) {
|
|
619
|
+
const before = callsBefore(cut);
|
|
620
|
+
// A result after the cut whose call is before it would lose its call.
|
|
621
|
+
let moved = false;
|
|
622
|
+
for (let k = cut; k < conversation.length; k++) {
|
|
623
|
+
const result = messages[conversation[k]];
|
|
624
|
+
if (result.role === "toolResult" && typeof result.toolCallId === "string" && before.has(result.toolCallId)) {
|
|
625
|
+
// Move the cut before the message that made this call.
|
|
626
|
+
let owner = cut - 1;
|
|
627
|
+
while (owner >= 0 && !toolCallIds(messages[conversation[owner]]).includes(result.toolCallId))
|
|
628
|
+
owner--;
|
|
629
|
+
cut = Math.max(0, owner);
|
|
630
|
+
moved = true;
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (!moved)
|
|
635
|
+
return cut;
|
|
477
636
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
637
|
+
return cut;
|
|
638
|
+
}
|
|
639
|
+
function toolCallIds(message) {
|
|
640
|
+
const content = message?.content;
|
|
641
|
+
if (!Array.isArray(content))
|
|
642
|
+
return [];
|
|
643
|
+
return content.flatMap((block) => isRecord(block) && block.type === "toolCall" && typeof block.id === "string" ? [block.id] : []);
|
|
644
|
+
}
|
|
645
|
+
function roleOf(message) {
|
|
646
|
+
const role = message?.role;
|
|
647
|
+
return typeof role === "string" ? role : "unknown";
|
|
648
|
+
}
|
|
649
|
+
/** The request with the summary applied, or undefined (see matchPreparedSummary). */
|
|
650
|
+
export function applyPreparedSummary(messages, ready) {
|
|
651
|
+
const result = matchPreparedSummary(messages, ready);
|
|
652
|
+
return "messages" in result ? result.messages : undefined;
|
|
653
|
+
}
|
|
654
|
+
function isSystemMessage(message) {
|
|
655
|
+
return message?.role === "system";
|
|
484
656
|
}
|
|
485
657
|
/** Tokens (Pi's estimate) the entries before firstKeptEntryId cost, minus the summary replacing them. */
|
|
486
658
|
function removedBy(entries, firstKeptEntryId, summary) {
|
|
487
659
|
let total = 0;
|
|
488
|
-
for (const entry of entries) {
|
|
660
|
+
for (const { entry, messages } of project(entries)) {
|
|
489
661
|
if (entry.id === firstKeptEntryId)
|
|
490
662
|
break;
|
|
491
|
-
for (const message of
|
|
663
|
+
for (const message of messages)
|
|
492
664
|
total += estimateTokens(message);
|
|
493
665
|
}
|
|
494
666
|
return Math.max(0, total - Math.ceil(summary.length / 4));
|
|
@@ -502,8 +674,9 @@ function removedBy(entries, firstKeptEntryId, summary) {
|
|
|
502
674
|
export function coveredStart(visibleIds, replaced, visible) {
|
|
503
675
|
if (replaced.length === 0)
|
|
504
676
|
return -1;
|
|
677
|
+
const projected = project(visible);
|
|
505
678
|
let start = 0;
|
|
506
|
-
while (start <
|
|
679
|
+
while (start < projected.length && visibleIds[start] !== replaced[0] && isSystemEntry(projected[start]))
|
|
507
680
|
start++;
|
|
508
681
|
if (visibleIds[start] !== replaced[0])
|
|
509
682
|
return -1;
|
|
@@ -513,10 +686,15 @@ export function coveredStart(visibleIds, replaced, visible) {
|
|
|
513
686
|
return start;
|
|
514
687
|
}
|
|
515
688
|
function isSystemEntry(entry) {
|
|
516
|
-
return
|
|
689
|
+
return entry.messages.every(isSystemMessage);
|
|
517
690
|
}
|
|
691
|
+
/** Entries that put at least one message into the model request. */
|
|
518
692
|
function modelVisibleEntries(entries) {
|
|
519
|
-
return entries.filter((
|
|
693
|
+
return project(entries).filter((item) => item.messages.length > 0);
|
|
694
|
+
}
|
|
695
|
+
/** The message an entry contributes as conversation (a compaction's summary, not its system message). */
|
|
696
|
+
function conversationMessage(item) {
|
|
697
|
+
return item.messages.find((message) => !isSystemMessage(message)) ?? item.messages[0];
|
|
520
698
|
}
|
|
521
699
|
function piKind(role) {
|
|
522
700
|
if (role === "assistant")
|
|
@@ -530,8 +708,8 @@ function piKind(role) {
|
|
|
530
708
|
function toolCallGroups(entries) {
|
|
531
709
|
const calls = new Map();
|
|
532
710
|
const entryGroups = new Map();
|
|
533
|
-
for (const
|
|
534
|
-
const message =
|
|
711
|
+
for (const item of entries) {
|
|
712
|
+
const message = conversationMessage(item);
|
|
535
713
|
if (!isRecord(message) || !Array.isArray(message.content))
|
|
536
714
|
continue;
|
|
537
715
|
const callIds = message.content.flatMap((block) => {
|
|
@@ -541,8 +719,8 @@ function toolCallGroups(entries) {
|
|
|
541
719
|
});
|
|
542
720
|
if (callIds.length === 0)
|
|
543
721
|
continue;
|
|
544
|
-
const group = `pi-tool-batch:${entry.id}`;
|
|
545
|
-
entryGroups.set(entry.id, group);
|
|
722
|
+
const group = `pi-tool-batch:${item.entry.id}`;
|
|
723
|
+
entryGroups.set(item.entry.id, group);
|
|
546
724
|
for (const callId of callIds)
|
|
547
725
|
calls.set(callId, group);
|
|
548
726
|
}
|
|
@@ -237,7 +237,7 @@ function isJsonValue(value) {
|
|
|
237
237
|
return isRecord(value) && Object.values(value).every(isJsonValue);
|
|
238
238
|
}
|
|
239
239
|
export { HostCompactPool, bindingKey } from "./host-worker.js";
|
|
240
|
-
export { journal, sessionHash, afuTrigger } from "./telemetry.js";
|
|
240
|
+
export { journal, sessionHash, afuTrigger, afuPaused } from "./telemetry.js";
|
|
241
241
|
export { summaryPrefix } from "./prefix.js";
|
|
242
242
|
export { afuStatus, afuDashboard, STATUS_COMMAND, DASHBOARD_COMMAND } from "./commands.js";
|
|
243
243
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { appendFileSync, openSync, closeSync, mkdirSync, statSync, renameSync, constants, readFileSync } from 'node:fs';
|
|
1
|
+
import { appendFileSync, openSync, closeSync, mkdirSync, statSync, renameSync, constants, readFileSync, existsSync, writeFileSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { gzipSync } from 'node:zlib';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
4
|
import { homedir } from 'node:os';
|
|
4
5
|
import { randomUUID, createHash } from 'node:crypto';
|
|
@@ -36,7 +37,8 @@ export function journal(host) {
|
|
|
36
37
|
function createJournal(host) {
|
|
37
38
|
const root = join(process.env.AFU_STATE_DIR ?? join(homedir(), '.local/share/afu-context'), 'logs');
|
|
38
39
|
const id = randomUUID().replaceAll('-', '');
|
|
39
|
-
const file = join(root, `events-${process.pid}-${id}.jsonl`);
|
|
40
|
+
const file = join(root, `events-${host}-adapter-${process.pid}-${id}.jsonl`);
|
|
41
|
+
let failed = 0;
|
|
40
42
|
let version = '0.1.0', build = 'development';
|
|
41
43
|
try {
|
|
42
44
|
const m = JSON.parse(readFileSync(join(process.env.AFU_INSTALL_ROOT ?? process.cwd(), 'release.json'), 'utf8'));
|
|
@@ -51,9 +53,9 @@ function createJournal(host) {
|
|
|
51
53
|
const row = { event, host, version, build, runtime_id: id, pid: process.pid, time: new Date().toISOString() };
|
|
52
54
|
// COSEC: diagnostics never include raw model IDs, credentials, prompts or errors.
|
|
53
55
|
for (const [k, v] of Object.entries(fields)) {
|
|
54
|
-
if (['session', 'artifact_id', 'phase', 'reason'].includes(k) && typeof v === 'string' && /^[A-Za-z0-9_.:+-]{1,100}$/.test(v))
|
|
56
|
+
if (['session', 'artifact_id', 'phase', 'reason', 'first_unknown_role'].includes(k) && typeof v === 'string' && /^[A-Za-z0-9_.:+-]{1,100}$/.test(v))
|
|
55
57
|
row[k] = v;
|
|
56
|
-
if (['input_tokens', 'output_tokens', 'cache_read_input_tokens', 'cache_creation_input_tokens', 'context_window', 'threshold', 'estimated_input_tokens', 'removed_tokens_estimate', 'duration_ms', 'host_compaction_ms'].includes(k) && typeof v === 'number' && Number.isFinite(v) && v >= 0)
|
|
58
|
+
if (['input_tokens', 'output_tokens', 'cache_read_input_tokens', 'cache_creation_input_tokens', 'context_window', 'threshold', 'estimated_input_tokens', 'removed_tokens_estimate', 'duration_ms', 'host_compaction_ms', 'anchor_position', 'unknown_messages'].includes(k) && typeof v === 'number' && Number.isFinite(v) && v >= 0)
|
|
57
59
|
row[k] = v;
|
|
58
60
|
if (['cache_usage_known', 'applied', 'upstream_success'].includes(k) && typeof v === 'boolean')
|
|
59
61
|
row[k] = v;
|
|
@@ -61,18 +63,57 @@ function createJournal(host) {
|
|
|
61
63
|
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
62
64
|
try {
|
|
63
65
|
if (statSync(file).size > 2 * 1024 * 1024)
|
|
64
|
-
|
|
66
|
+
rotate(file);
|
|
65
67
|
}
|
|
66
68
|
catch { }
|
|
69
|
+
const lines = failed ? [{ event: 'journal_write_failed', host, version, build, runtime_id: id, pid: process.pid, time: row.time, failed_writes: failed }, row] : [row];
|
|
67
70
|
const fd = openSync(file, constants.O_WRONLY | constants.O_CREAT | constants.O_APPEND | (constants.O_NOFOLLOW ?? 0), 0o600);
|
|
68
71
|
try {
|
|
69
|
-
appendFileSync(fd, JSON.stringify(
|
|
72
|
+
appendFileSync(fd, lines.map(l => JSON.stringify(l) + '\n').join(''));
|
|
70
73
|
}
|
|
71
74
|
finally {
|
|
72
75
|
closeSync(fd);
|
|
73
76
|
}
|
|
77
|
+
failed = 0;
|
|
74
78
|
}
|
|
75
|
-
catch {
|
|
79
|
+
catch {
|
|
80
|
+
failed++;
|
|
81
|
+
} // Logging must never break a host request.
|
|
76
82
|
};
|
|
77
83
|
}
|
|
84
|
+
/** Move a full log to the next numbered, gzip-compressed file; nothing is overwritten. */
|
|
85
|
+
function rotate(file) {
|
|
86
|
+
let n = 1;
|
|
87
|
+
const stem = file.replace(/\.jsonl$/, '');
|
|
88
|
+
while (existsSync(`${stem}.${n}.jsonl`) || existsSync(`${stem}.${n}.jsonl.gz`))
|
|
89
|
+
n++;
|
|
90
|
+
const plain = `${stem}.${n}.jsonl`;
|
|
91
|
+
renameSync(file, plain);
|
|
92
|
+
try {
|
|
93
|
+
writeFileSync(plain + '.gz', gzipSync(readFileSync(plain)), { mode: 0o600 });
|
|
94
|
+
unlinkSync(plain);
|
|
95
|
+
}
|
|
96
|
+
catch { }
|
|
97
|
+
}
|
|
98
|
+
const pausedCache = new Map();
|
|
99
|
+
/**
|
|
100
|
+
* Whether the user paused AFU for this host (afuctx off). Read from the
|
|
101
|
+
* host's record in <state>/integrations at most once a second; any read
|
|
102
|
+
* problem counts as not paused, never as a reason to block the host.
|
|
103
|
+
*/
|
|
104
|
+
export function afuPaused(host, now = Date.now()) {
|
|
105
|
+
const cached = pausedCache.get(host);
|
|
106
|
+
if (cached && now - cached.checked < 1000)
|
|
107
|
+
return cached.paused;
|
|
108
|
+
let paused = false;
|
|
109
|
+
try {
|
|
110
|
+
const file = join(process.env.AFU_STATE_DIR ?? join(homedir(), '.local/share/afu-context'), 'integrations', `${host}.json`);
|
|
111
|
+
paused = JSON.parse(readFileSync(file, 'utf8')).paused === true;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
paused = false;
|
|
115
|
+
}
|
|
116
|
+
pausedCache.set(host, { checked: now, paused });
|
|
117
|
+
return paused;
|
|
118
|
+
}
|
|
78
119
|
//# sourceMappingURL=telemetry.js.map
|