zigmini 0.9.0-beta.1 → 0.9.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +13 -4
- package/README.md +13 -4
- package/assets/android/zigmini.apk +0 -0
- package/assets/android/zigmini.apk.json +3 -3
- package/assets/build.json +1 -1
- package/docs/manual.ko.md +7 -0
- package/docs/manual.md +7 -0
- package/host/omo-bridge/ascii-json.ts +5 -0
- package/host/omo-bridge/bridge.ts +50 -44
- package/host/omo-check/patches/omo-5.1.48-compaction-port.patch +2629 -0
- package/host/omo-check/patches/omo-5.1.48-gateway-port.manifest.json +355 -0
- package/host/omo-check/patches/omo-5.1.48-provider-port.patch +440 -0
- package/host/omo-check/patches/omo-5.1.48-rpc-port.patch +799 -0
- package/host/omo-check/patches/omo-5.1.48-runtime-compat.patch +31 -0
- package/package.json +1 -1
- package/src/cli/setup.ts +6 -2
- package/src/engine/compat.json +4 -4
- package/src/engine/install.ts +68 -20
- package/src/engine/rpc-socket.ts +36 -0
- package/src/engine/schema-guard.ts +34 -2
- package/src/omo/client.ts +16 -3
- package/src/omo/frame-socket.ts +330 -0
- package/src/update/handoff-system.ts +22 -0
- package/src/update/handoff.ts +10 -2
- package/src/update/helper.ts +11 -1
- package/src/update/request.ts +15 -2
package/README.ko.md
CHANGED
|
@@ -95,9 +95,9 @@ zigmini는 에이전트를 권한 프리셋 `full-access`로 실행합니다.
|
|
|
95
95
|
14. 9번 명령을 다시 실행합니다.
|
|
96
96
|
- 결과가 숫자로 시작하는 버전 한 줄이면 15번으로 갑니다.
|
|
97
97
|
- 결과가 여전히 `zsh: command not found: zigmini`이면 설치를 멈춥니다. `ls "$(bun pm bin -g)"`를 실행합니다. 결과에 `zigmini`가 없으면 8번의 `bun add -g zigmini` 출력에서 `error:`로 시작하는 줄을 확인합니다.
|
|
98
|
-
15. 에이전트 엔진 `omo-ai`를 전역 설치합니다. 필수 단계입니다. `omo-ai` 설치와 모델 계정 로그인이 없으면 게이트웨이가 동작하지 않습니다. 15~18번을 건너뛰면 `zigmini setup`이 전역 `omo`를 직접 설치하고 로그인 단계에서 기다리지만, 로그인은 직접 해야 합니다. 그래도 여기의 단계를 먼저 합니다. 버전은 게이트웨이가 고정한 stock 버전에 맞춥니다(`src/engine/compat.json`의 `omo`, 0.9.0-beta.
|
|
98
|
+
15. 에이전트 엔진 `omo-ai`를 전역 설치합니다. 필수 단계입니다. `omo-ai` 설치와 모델 계정 로그인이 없으면 게이트웨이가 동작하지 않습니다. 15~18번을 건너뛰면 `zigmini setup`이 전역 `omo`를 직접 설치하고 로그인 단계에서 기다리지만, 로그인은 직접 해야 합니다. 그래도 여기의 단계를 먼저 합니다. 버전은 게이트웨이가 고정한 stock 버전에 맞춥니다(`src/engine/compat.json`의 `omo`, 0.9.0-beta.2는 5.1.48이며 설치된 릴리스는 자체 고정값을 유지합니다).
|
|
99
99
|
```sh
|
|
100
|
-
bun add -g omo-ai@5.1.
|
|
100
|
+
bun add -g omo-ai@5.1.48
|
|
101
101
|
```
|
|
102
102
|
16. 셸이 `omo` 명령을 찾는지 확인합니다. 결과는 버전 한 줄이어야 합니다.
|
|
103
103
|
```sh
|
|
@@ -118,7 +118,7 @@ zigmini는 에이전트를 권한 프리셋 `full-access`로 실행합니다.
|
|
|
118
118
|
1. 언어를 묻습니다: `Language / 언어 [1] English [2] 한국어`. 그 뒤의 안내는 모두 고른 언어로 나옵니다. 고른 언어는 `~/.zigmini/language`에 저장되고 `zigmini setup --access`, `zigmini pair`, `zigmini start`·`restart`의 준비 대기도 이 언어를 씁니다. `--lang en|ko`로 묻지 않고 정할 수 있습니다. 묻지 않는 설치(`--non-interactive`)는 저장된 언어, 없으면 시스템 언어(`LANG`이 `ko…`이면 한국어), 그것도 없으면 영어를 씁니다.
|
|
119
119
|
2. 권한 안내를 보여 주고 동의를 받습니다.
|
|
120
120
|
3. 홈 폴더 `~/.zigmini`를 만듭니다. 이 설치의 규칙 파일(`~/.zigmini/rules.md`)이 없으면 빈 파일을 만듭니다. 아래 "에이전트 규칙"을 보세요.
|
|
121
|
-
4. 에이전트 엔진(omo-ai)의 고정 버전을 내려받아 해시를 확인하고 패치합니다. 버전은 패키지가 정합니다(`src/engine/compat.json`, 이 문서를 쓸 때 엔진 5.1.
|
|
121
|
+
4. 에이전트 엔진(omo-ai)의 고정 버전을 내려받아 해시를 확인하고 패치합니다. 버전은 패키지가 정합니다(`src/engine/compat.json`, 이 문서를 쓸 때 엔진 5.1.48). 엔진 라이선스 요약을 보여 주고 동의(`y`)를 받습니다.
|
|
122
122
|
5. 전역 `omo` 명령을 `bun add -g omo-ai@<버전>`으로 설치합니다. 버전은 고정 엔진과 같은 stock 버전입니다. 전역 `omo`가 이미 그 버전이면 설치를 건너뜁니다. 다른 버전이면 `<버전>으로 덮어쓸까요? (y/N)`를 묻습니다. 묻지 않는 설치는 있는 버전을 유지합니다. 설치에 실패하면 고정 엔진의 긴 명령을 로그인 명령으로 보여 줍니다.
|
|
123
123
|
6. 에이전트 폴더와 모델 계정 로그인 상태를 확인합니다. 기본 에이전트 폴더는 `~/.omo/agent`이고, 같은 폴더를 쓰는 다른 OMO와 로그인 정보를 같이 씁니다. 로그인하지 않았으면 아래 상자를 보이고 기다립니다.
|
|
124
124
|
```text
|
|
@@ -195,6 +195,15 @@ F0:AB:B3:B2:DC:70:1A:45:19:85:69:7D:58:68:F5:FB:4F:AC:33:4F:D8:6A:68:DD:04:5C:53
|
|
|
195
195
|
|
|
196
196
|
사용자 매뉴얼([한국어](docs/manual.ko.md), [English](docs/manual.md))은 웹 설정 창 위쪽의 "매뉴얼" 버튼과, 앱 헤더의 상태 태그를 짧게 눌러 여는 연결 화면의 "매뉴얼" 버튼으로 봅니다.
|
|
197
197
|
|
|
198
|
+
## 0.9.0-beta.2의 새 기능
|
|
199
|
+
|
|
200
|
+
0.9.0-beta.2(2026-10-29 KST, Android `versionCode` 28)는 0.8.0-beta.7에서 업데이트하기 위한 핫픽스입니다. 엔진 `5.1.48-zgw`(OMO 5.1.48, 패치 개정 2, senpi 2026.10.25-2, 대화 스키마 10)를 선택합니다. 전체 노트는 [docs/releases/0.9.0-beta.2.ko.md](docs/releases/0.9.0-beta.2.ko.md)에 있습니다.
|
|
201
|
+
|
|
202
|
+
- **0.8.0-beta.7에서 업데이트 복구:** 0.9.0-beta.1은 이미 스키마 10으로 바뀐 대화 저장소에서 시작하지 못했습니다. 이 릴리스는 스키마 10을 읽고 세션, 등록, 설정, 기록을 유지합니다.
|
|
203
|
+
- **시간 초과 대신 분명한 거부:** 엔진이 공유 대화 저장소를 읽지 못하는 업데이트는 아무것도 설치하기 전에 거부합니다. 강제 업데이트도 거부하며 웹 설정에 오류를 보여 줍니다.
|
|
204
|
+
- **더 안정적인 엔진 연결:** 게이트웨이가 엔진 브리지 연결에 Bun WebSocket 클라이언트를 더 쓰지 않습니다. 이 클라이언트는 긴 세션에서 연결을 끊을 수 있었습니다.
|
|
205
|
+
- **앱:** 동작 변화가 없습니다. `versionCode` 28은 이 릴리스 표시입니다.
|
|
206
|
+
|
|
198
207
|
## 0.9.0-beta.1의 새 기능
|
|
199
208
|
|
|
200
209
|
0.9.0-beta.1(2026-10-28 KST, Android `versionCode` 27)은 엔진 `5.1.44-zgw`(OMO 5.1.44, senpi 2026.10.21-3, 대화 스키마 7)를 선택합니다. 전체 노트는 [docs/releases/0.9.0-beta.1.ko.md](docs/releases/0.9.0-beta.1.ko.md)에 있습니다.
|
|
@@ -312,7 +321,7 @@ zigmini update <버전> # 지정한 버전으로 업데이트
|
|
|
312
321
|
```sh
|
|
313
322
|
zigmini setup --access lan # 또는 tailscale, domain --domain https://<도메인>
|
|
314
323
|
```
|
|
315
|
-
- **설치 뒤 에이전트가 답하지 않습니다.** 모델 계정에 로그인하지 않았습니다(예: 로그인 단계에서 `s`를 입력함). 설치 15~18번(`bun add -g omo-ai@5.1.
|
|
324
|
+
- **설치 뒤 에이전트가 답하지 않습니다.** 모델 계정에 로그인하지 않았습니다(예: 로그인 단계에서 `s`를 입력함). 설치 15~18번(`bun add -g omo-ai@5.1.48`, `omo`, `/login`)을 합니다. 그 뒤 `zigmini setup`을 다시 실행합니다. "모델 계정: 로그인됨"이 나오고, 이미 끝난 단계는 유지됩니다. `--agent-dir`을 쓰면 `OMO_CODING_AGENT_DIR=<경로> omo`로 실행해야 그 폴더에 로그인됩니다.
|
|
316
325
|
- 오래된 DB 백업, 쓰지 않는 엔진 버전, 업데이트에서 남은 파일은 웹 화면 설정의 "정리"에서 확인하고, 고른 항목만 지웁니다.
|
|
317
326
|
|
|
318
327
|
## 제거
|
package/README.md
CHANGED
|
@@ -95,9 +95,9 @@ The gateway runs the agent with the permission preset `full-access`. The agent d
|
|
|
95
95
|
14. Run the command of step 9 again.
|
|
96
96
|
- If the result is one version line that starts with a number, go to step 15.
|
|
97
97
|
- If the result is still `zsh: command not found: zigmini`, stop the installation. Run `ls "$(bun pm bin -g)"`. If the result does not contain `zigmini`, look for lines that start with `error:` in the output of `bun add -g zigmini` in step 8.
|
|
98
|
-
15. Install the agent engine `omo-ai` globally. This step is required: without the `omo-ai` installation and the model account login, the gateway does not work. If you skip steps 15 to 18, `zigmini setup` installs the global `omo` itself and stops at the login step, but you must still log in. Do the steps here anyway. Use the stock version that the gateway pins (`omo` in `src/engine/compat.json`; 5.1.
|
|
98
|
+
15. Install the agent engine `omo-ai` globally. This step is required: without the `omo-ai` installation and the model account login, the gateway does not work. If you skip steps 15 to 18, `zigmini setup` installs the global `omo` itself and stops at the login step, but you must still log in. Do the steps here anyway. Use the stock version that the gateway pins (`omo` in `src/engine/compat.json`; 5.1.48 in 0.9.0-beta.2; installed releases keep their own pin).
|
|
99
99
|
```sh
|
|
100
|
-
bun add -g omo-ai@5.1.
|
|
100
|
+
bun add -g omo-ai@5.1.48
|
|
101
101
|
```
|
|
102
102
|
16. Check that the shell finds the `omo` command. The result must be one version line.
|
|
103
103
|
```sh
|
|
@@ -118,7 +118,7 @@ The gateway runs the agent with the permission preset `full-access`. The agent d
|
|
|
118
118
|
1. It asks for the language: `Language / 언어 [1] English [2] 한국어`. All later messages use this language. The choice is saved in `~/.zigmini/language` and used by `zigmini setup --access`, `zigmini pair` and the ready wait of `zigmini start` and `restart`. `--lang en|ko` picks it without the question. Without a question (`--non-interactive`) the saved language is used, else the system language (`LANG`, `ko…` means Korean), else English.
|
|
119
119
|
2. It shows the permission notice and asks for your consent.
|
|
120
120
|
3. It creates the home folder `~/.zigmini`. If the rules file of this installation (`~/.zigmini/rules.md`) does not exist, it creates an empty one. See "Agent rules" below.
|
|
121
|
-
4. It downloads the pinned version of the agent engine (omo-ai), checks its hash and patches it. The package decides the version (`src/engine/compat.json`; engine 5.1.
|
|
121
|
+
4. It downloads the pinned version of the agent engine (omo-ai), checks its hash and patches it. The package decides the version (`src/engine/compat.json`; engine 5.1.48 at the time of writing). It shows a summary of the engine license and asks for your consent (`y`).
|
|
122
122
|
5. It installs the global `omo` command with `bun add -g omo-ai@<version>`, the same stock version as the pinned engine. If the global `omo` already has this version, it skips the installation. If it has another version, it asks `Replace it with <version>? (y/N)`; without a question it keeps the installed one. If the installation fails, it shows the long command of the pinned engine for the login instead.
|
|
123
123
|
6. It checks the agent folder and the login state of the model account. The default agent folder is `~/.omo/agent`. Other OMO installations that use the same folder share the login data. If you are not logged in, it shows this box and waits:
|
|
124
124
|
```text
|
|
@@ -195,6 +195,15 @@ F0:AB:B3:B2:DC:70:1A:45:19:85:69:7D:58:68:F5:FB:4F:AC:33:4F:D8:6A:68:DD:04:5C:53
|
|
|
195
195
|
|
|
196
196
|
The user manual ([English](docs/manual.md), [한국어](docs/manual.ko.md)) opens from the "매뉴얼" (Manual) button at the top of the web settings dialog, and in the app from the "매뉴얼" button on the connection screen that a short press on the header status tag opens.
|
|
197
197
|
|
|
198
|
+
## What's new in 0.9.0-beta.2
|
|
199
|
+
|
|
200
|
+
Release 0.9.0-beta.2 (2026-10-29 KST, Android `versionCode` 28) is a hotfix for updating from 0.8.0-beta.7. It selects engine `5.1.48-zgw` (OMO 5.1.48, patch revision 2, senpi 2026.10.25-2, thread schema 10). Full notes: [docs/releases/0.9.0-beta.2.md](docs/releases/0.9.0-beta.2.md).
|
|
201
|
+
|
|
202
|
+
- **Update from 0.8.0-beta.7 works again:** 0.9.0-beta.1 could not start against a thread store that had already moved to schema 10. This release reads schema 10 and keeps sessions, registrations, settings and history.
|
|
203
|
+
- **Clear refusal instead of a timeout:** an update whose engine can't read the shared thread store is refused before anything is installed, with an error in the web settings, even with force.
|
|
204
|
+
- **Steadier engine connection:** the gateway no longer uses Bun's WebSocket client for the engine bridge, which could drop the connection in long sessions.
|
|
205
|
+
- **App:** no behavior change; `versionCode` 28 only marks this release.
|
|
206
|
+
|
|
198
207
|
## What's new in 0.9.0-beta.1
|
|
199
208
|
|
|
200
209
|
Release 0.9.0-beta.1 (2026-10-28 KST, Android `versionCode` 27) selects engine `5.1.44-zgw` (OMO 5.1.44, senpi 2026.10.21-3, thread schema 7). Full notes: [docs/releases/0.9.0-beta.1.md](docs/releases/0.9.0-beta.1.md).
|
|
@@ -313,7 +322,7 @@ Use these workarounds.
|
|
|
313
322
|
```sh
|
|
314
323
|
zigmini setup --access lan # or tailscale, or domain --domain https://<도메인>
|
|
315
324
|
```
|
|
316
|
-
- **After the installation the agent does not answer.** The model account is not logged in (for example you typed `s` at the login step). Do install steps 15 to 18 (`bun add -g omo-ai@5.1.
|
|
325
|
+
- **After the installation the agent does not answer.** The model account is not logged in (for example you typed `s` at the login step). Do install steps 15 to 18 (`bun add -g omo-ai@5.1.48`, `omo`, `/login`). Then run `zigmini setup` again. It shows "Model accounts: logged in" and keeps the steps that are already done. If you use `--agent-dir`, start `omo` with `OMO_CODING_AGENT_DIR=<경로> omo` so that it logs in to that folder.
|
|
317
326
|
- Old DB backups, unused engine versions and files left by updates appear in "정리" (Clean up) in the web screen settings. Only the items you select are deleted.
|
|
318
327
|
|
|
319
328
|
## Uninstall
|
|
Binary file
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"versionCode":
|
|
3
|
-
"versionName": "0.9.0-beta.
|
|
4
|
-
"sha256": "
|
|
2
|
+
"versionCode": 28,
|
|
3
|
+
"versionName": "0.9.0-beta.2",
|
|
4
|
+
"sha256": "3588a9b0124a4d483d84038edf86f68cf3d96e0d071d5eb93ff556908507e012"
|
|
5
5
|
}
|
package/assets/build.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"commit":"
|
|
1
|
+
{"commit":"78cb593c5a3f2599a4196e877f2d07d3e90fd506"}
|
package/docs/manual.ko.md
CHANGED
|
@@ -239,10 +239,17 @@ README의 '명령' 절에 따르면 채팅에서 `/usage`를 보내면 게이트
|
|
|
239
239
|
| 확인함에 답을 기다리는 질문이 있음 | 버튼이 꺼집니다. "답을 기다리는 질문이 N개 있습니다" 안내와 "확인함 열기" 버튼이 보입니다. 질문에 답한 뒤 업데이트할 수 있습니다. |
|
|
240
240
|
| 권한 안내가 바뀜 | 새 안내문이 보입니다. 입력란에 동의를 입력해야 진행합니다. |
|
|
241
241
|
| 공유 데이터 형식 변경 | 변경 내용을 보여 주고 확인을 받습니다. |
|
|
242
|
+
| 저장된 데이터 형식이 대상 엔진의 지원 범위보다 새로움 | 설치 전에 업데이트를 멈춥니다. 오류에 저장 형식과 지원 형식이 나옵니다. 확인하거나 강제로 실행해도 이 거부를 우회할 수 없습니다. |
|
|
242
243
|
| 진행 중인 작업이 있음 | 게이트웨이가 바쁘다는 안내가 보입니다. |
|
|
243
244
|
|
|
244
245
|
업데이트하면 게이트웨이와 브리지가 다시 시작됩니다. 그동안 연결과 진행 중인 대화가 잠시 끊길 수 있습니다. 진행 상황이 영역 아래에 보입니다. 끝나면 페이지를 새로 고칩니다.
|
|
245
246
|
|
|
247
|
+
공유 데이터 형식이 더 새로우면 그 형식을 지원하는 엔진 릴리스가 필요합니다.
|
|
248
|
+
저장 형식 번호를 낮추거나 에이전트 폴더를 초기화하지 마세요.
|
|
249
|
+
새 세대가 시작을 거부하면 업데이트가 그 이유를 표시합니다.
|
|
250
|
+
기존 실행을 아직 바꾸지 않았다면 기존 세대를 유지합니다.
|
|
251
|
+
에이전트가 연결됐다는 사실만으로 공유 thread 저장소의 정상 동작을 판단할 수는 없습니다.
|
|
252
|
+
|
|
246
253
|
#### 기기 등록
|
|
247
254
|
|
|
248
255
|
- 폰 등록 QR이 보입니다. QR 하나는 10분 동안 한 번만 쓸 수 있습니다.
|
package/docs/manual.md
CHANGED
|
@@ -237,10 +237,17 @@ The update button is held, or asks for more, in these cases.
|
|
|
237
237
|
| Inbox questions are waiting for you | The button is disabled. A line says N questions are waiting, with a 확인함 열기 (open inbox) button. Answer them, then update. |
|
|
238
238
|
| The permission notice changed | The new notice shows. Type the consent word (동의) in the field to go on. |
|
|
239
239
|
| Shared data format change | The changes are shown and you confirm them. |
|
|
240
|
+
| Stored data is newer than the target engine supports | The update stops before installation. The error names the stored and supported formats. Confirmation or forcing the update cannot bypass this refusal. |
|
|
240
241
|
| Work is running | A notice says the gateway is busy. |
|
|
241
242
|
|
|
242
243
|
An update restarts the gateway and the bridge. Connections and running chats may drop for a moment. Progress shows below. When it's done, reload the page.
|
|
243
244
|
|
|
245
|
+
A newer shared data format needs a compatible engine release. Do not lower the
|
|
246
|
+
stored format number or reset the agent folder. If a new generation refuses to
|
|
247
|
+
start, the update reports that reason and keeps the old running generation when
|
|
248
|
+
it has not been changed. A connected agent alone does not prove that its shared
|
|
249
|
+
thread storage works.
|
|
250
|
+
|
|
244
251
|
#### Device registration (기기 등록)
|
|
245
252
|
|
|
246
253
|
- A phone registration QR shows. Each QR works once, for 10 minutes.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Bug 220: Bun 1.4.2 rejects large multibyte JSON frames in the RPC regression.
|
|
2
|
+
// JSON escapes preserve every UTF-16 code unit while keeping wire text ASCII.
|
|
3
|
+
export function asciiJson(line: string): string {
|
|
4
|
+
return line.replace(/[^\x00-\x7f]/g, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
5
|
+
}
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
3
3
|
import { basename, dirname, join } from "node:path";
|
|
4
|
-
import type { ServerWebSocket,
|
|
4
|
+
import type { ServerWebSocket, Subprocess } from "bun";
|
|
5
|
+
import type { Socket } from "node:net";
|
|
5
6
|
import { bounded, maintenanceAuth, MaintenanceError } from "../../src/maintenance.ts";
|
|
6
7
|
import { runtimePaths } from "../../src/runtime/paths.ts";
|
|
7
8
|
import { installKind } from "../../src/runtime/install-kind.ts";
|
|
8
9
|
import { loadNotice, startRefusal } from "../../src/consent/consent.ts";
|
|
9
10
|
import { engine, engineSearchPath } from "../../src/engine/index.ts";
|
|
11
|
+
import { connectEngineSocket } from "../../src/engine/rpc-socket.ts";
|
|
10
12
|
import { bunInstallDir, guardMode, guardStart } from "../../src/engine/schema-guard.ts";
|
|
11
13
|
import { bridgeMayRun, readRegistry } from "../../src/runtime/slots.ts";
|
|
12
14
|
import { BridgeMaintenance } from "./maintenance.ts";
|
|
15
|
+
import { asciiJson } from "./ascii-json.ts";
|
|
13
16
|
|
|
14
17
|
const paths = runtimePaths();
|
|
15
18
|
const home = paths.home;
|
|
@@ -39,6 +42,7 @@ if (slotMode) {
|
|
|
39
42
|
const agentDir = process.env.ZGW_OMO_AGENT_DIR ?? join(process.env.HOME ?? ".", ".omo", "agent");
|
|
40
43
|
// Bug 73: a shared store newer than the pinned engine keeps the bridge down (exit 0: launchd does not restart it).
|
|
41
44
|
const schemaGuard = guardStart({ agentDir, run: paths.run, mode: guardMode(process.env),
|
|
45
|
+
origin: { slot: slotMode ? paths.slot : null, engine: engineRoot ? basename(engineRoot) : null },
|
|
42
46
|
supported: () => engine.supportedThreadSchema(home, engineRoot), bunInstall: bunInstallDir(process.env), now: () => new Date() });
|
|
43
47
|
if (schemaGuard.log !== null) console.error(`${new Date().toISOString()} ${schemaGuard.log}`);
|
|
44
48
|
if (schemaGuard.refuse) process.exit(0);
|
|
@@ -124,7 +128,8 @@ type Link = {
|
|
|
124
128
|
readonly identity: string | null;
|
|
125
129
|
readonly epoch: string;
|
|
126
130
|
readonly previousFailure: string | null;
|
|
127
|
-
upstream: Socket
|
|
131
|
+
upstream: Socket | null;
|
|
132
|
+
upstreamBlocked: boolean;
|
|
128
133
|
frontend: ServerWebSocket<Link> | null;
|
|
129
134
|
connecting: boolean;
|
|
130
135
|
ready: boolean;
|
|
@@ -150,7 +155,7 @@ const links = new Set<Link>();
|
|
|
150
155
|
maintenance = new BridgeMaintenance(paths.maintenanceFile,
|
|
151
156
|
() => ({ generation: nativeGeneration, binary: nativeBinary }),
|
|
152
157
|
() => omo.exitCode === null && [...links].filter((link) => link.nativeGeneration === nativeGeneration).every((link) =>
|
|
153
|
-
!link.connecting && !link.activeOut && link.out.length === 0 && link.pending.size === 0 &&
|
|
158
|
+
!link.connecting && !link.upstreamBlocked && !link.activeOut && link.out.length === 0 && link.pending.size === 0 &&
|
|
154
159
|
!link.frontendBlocked && !link.frontendPending && link.frontendOut.length === 0 &&
|
|
155
160
|
link.pendingUiWrites.size === 0 && !link.untrackedWrite && link.ui.size === 0),
|
|
156
161
|
async () => {
|
|
@@ -191,8 +196,9 @@ function fail(link: Link, reason: string): void {
|
|
|
191
196
|
link.failure = reason;
|
|
192
197
|
logBridgeError(`omo bridge: requester ${link.epoch} lost: ${reason}`);
|
|
193
198
|
link.frontend?.close(1011, reason);
|
|
194
|
-
link.upstream?.
|
|
199
|
+
link.upstream?.destroy();
|
|
195
200
|
link.upstream = null;
|
|
201
|
+
link.upstreamBlocked = false;
|
|
196
202
|
link.activeOut = null;
|
|
197
203
|
link.out = [];
|
|
198
204
|
link.outBytes = 0;
|
|
@@ -234,6 +240,7 @@ function flushFrontend(link: Link): void {
|
|
|
234
240
|
}
|
|
235
241
|
|
|
236
242
|
function deliver(link: Link, line: string): void {
|
|
243
|
+
line = asciiJson(line);
|
|
237
244
|
const ws = link.frontend;
|
|
238
245
|
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
239
246
|
if (link.frontendBlocked || link.frontendPending || link.frontendOut.length > 0) {
|
|
@@ -314,17 +321,20 @@ function receive(link: Link, line: string): void {
|
|
|
314
321
|
|
|
315
322
|
const encoder = new TextEncoder();
|
|
316
323
|
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
// cannot parse it, and the next command glued to the cut-off tail is lost too.
|
|
324
|
+
// net.Socket accepts the complete chunk; false means wait for drain before writing the next one.
|
|
325
|
+
// Keep later commands in the bounded bridge queue instead of the socket's unbounded writable queue.
|
|
320
326
|
function flush(link: Link): void {
|
|
321
327
|
const upstream = link.upstream;
|
|
322
|
-
if (!upstream) return;
|
|
328
|
+
if (!upstream || link.upstreamBlocked) return;
|
|
329
|
+
if (!link.activeOut) {
|
|
330
|
+
link.activeOut = link.out.shift() ?? null;
|
|
331
|
+
if (link.activeOut) link.outBytes -= link.activeOut.length;
|
|
332
|
+
}
|
|
323
333
|
while (link.activeOut) {
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
if (
|
|
327
|
-
link.
|
|
334
|
+
const writable = upstream.write(link.activeOut);
|
|
335
|
+
link.activeOut = null;
|
|
336
|
+
if (!writable) {
|
|
337
|
+
link.upstreamBlocked = true;
|
|
328
338
|
return;
|
|
329
339
|
}
|
|
330
340
|
link.activeOut = link.out.shift() ?? null;
|
|
@@ -334,7 +344,7 @@ function flush(link: Link): void {
|
|
|
334
344
|
|
|
335
345
|
function forward(link: Link, line: string): void {
|
|
336
346
|
const chunk = encoder.encode(`${line}\n`);
|
|
337
|
-
if (!link.activeOut) {
|
|
347
|
+
if (!link.activeOut && !link.upstreamBlocked) {
|
|
338
348
|
link.activeOut = chunk;
|
|
339
349
|
flush(link);
|
|
340
350
|
return;
|
|
@@ -401,6 +411,7 @@ const server = Bun.serve<Link>({
|
|
|
401
411
|
epoch: randomUUID(),
|
|
402
412
|
previousFailure: existing?.failure ?? null,
|
|
403
413
|
upstream: null,
|
|
414
|
+
upstreamBlocked: false,
|
|
404
415
|
frontend: null,
|
|
405
416
|
connecting: false,
|
|
406
417
|
ready: false,
|
|
@@ -440,42 +451,37 @@ const server = Bun.serve<Link>({
|
|
|
440
451
|
if (link.upstream || link.connecting) return;
|
|
441
452
|
link.connecting = true;
|
|
442
453
|
try {
|
|
443
|
-
const upstream = await
|
|
444
|
-
unix: socketPath,
|
|
445
|
-
socket: {
|
|
446
|
-
data(_s, chunk) {
|
|
447
|
-
if (link.failure) return;
|
|
448
|
-
const text = link.decoder.decode(chunk, { stream: true });
|
|
449
|
-
let start = 0;
|
|
450
|
-
let at = text.indexOf("\n");
|
|
451
|
-
while (at >= 0) {
|
|
452
|
-
const line = `${link.buffer.join("")}${text.slice(start, at)}`.replace(/\r$/, "");
|
|
453
|
-
link.buffer = [];
|
|
454
|
-
if (line) receive(link, line);
|
|
455
|
-
start = at + 1;
|
|
456
|
-
at = text.indexOf("\n", start);
|
|
457
|
-
}
|
|
458
|
-
if (start < text.length) link.buffer.push(text.slice(start));
|
|
459
|
-
},
|
|
460
|
-
drain() {
|
|
461
|
-
flush(link);
|
|
462
|
-
},
|
|
463
|
-
close() {
|
|
464
|
-
fail(link, "omo_requester_disconnected");
|
|
465
|
-
},
|
|
466
|
-
error() {
|
|
467
|
-
fail(link, "omo_requester_error");
|
|
468
|
-
},
|
|
469
|
-
},
|
|
470
|
-
});
|
|
454
|
+
const upstream = await connectEngineSocket({ socketPath, binary: nativeBinary, agentDir });
|
|
471
455
|
if (link.failure || shuttingDown) {
|
|
472
|
-
upstream.
|
|
456
|
+
upstream.destroy();
|
|
473
457
|
return;
|
|
474
458
|
}
|
|
459
|
+
upstream.on("data", (chunk: Buffer) => {
|
|
460
|
+
if (link.failure) return;
|
|
461
|
+
const text = link.decoder.decode(chunk, { stream: true });
|
|
462
|
+
let start = 0;
|
|
463
|
+
let at = text.indexOf("\n");
|
|
464
|
+
while (at >= 0) {
|
|
465
|
+
const line = `${link.buffer.join("")}${text.slice(start, at)}`.replace(/\r$/, "");
|
|
466
|
+
link.buffer = [];
|
|
467
|
+
if (line) receive(link, line);
|
|
468
|
+
start = at + 1;
|
|
469
|
+
at = text.indexOf("\n", start);
|
|
470
|
+
}
|
|
471
|
+
if (start < text.length) link.buffer.push(text.slice(start));
|
|
472
|
+
});
|
|
473
|
+
upstream.on("drain", () => {
|
|
474
|
+
link.upstreamBlocked = false;
|
|
475
|
+
flush(link);
|
|
476
|
+
});
|
|
477
|
+
upstream.on("close", () => fail(link, "omo_requester_disconnected"));
|
|
478
|
+
upstream.on("error", () => fail(link, "omo_requester_error"));
|
|
475
479
|
link.upstream = upstream;
|
|
476
480
|
flush(link);
|
|
477
|
-
} catch {
|
|
478
|
-
|
|
481
|
+
} catch (error) {
|
|
482
|
+
const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : null;
|
|
483
|
+
fail(link, code === "auth_failed" || code === "auth_required" || code === "unsafe_key_record" ||
|
|
484
|
+
code === "host_identity_unproven" ? code : "omo_requester_unavailable");
|
|
479
485
|
} finally {
|
|
480
486
|
link.connecting = false;
|
|
481
487
|
}
|