@bitefoundry/retrieviewer 0.0.12 → 0.0.14
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/ATTENTION.md +92 -0
- package/BRAIN.md +115 -0
- package/INSTALLING.md +25 -0
- package/MOTION-TABLES.md +3 -1
- package/README.ko.md +16 -0
- package/README.md +18 -0
- package/RELEASING.md +47 -1
- package/integrations/openrouter/brain.d.mts +22 -0
- package/integrations/openrouter/brain.mjs +74 -0
- package/integrations/stackbite/attention.mjs +47 -0
- package/integrations/windows-overlay/Overlay.cs +22 -6
- package/integrations/windows-overlay/desktop.mjs +20 -5
- package/integrations/windows-overlay/protocol.mjs +2 -0
- package/integrations/windows-overlay/worker.mjs +37 -13
- package/package.json +17 -2
- package/puppy-companion-interactions-v3.json +1 -1
- package/puppy-companion-life.mjs +24 -3
- package/puppy-motion-tables.generated.mjs +1 -1
- package/puppy-paper-actions-v9.json +1 -1
- package/puppy-paper-approach.json +1 -1
- package/puppy-paper-arrival-seat.json +1 -1
- package/puppy-paper-behaviors.json +1 -1
- package/puppy-paper-rear-gallop.json +1 -1
- package/puppy-paper-seat.json +14 -14
- package/puppy-stable-meta.json +1606 -1606
- package/puppy-stride-meta.json +343 -343
- package/retrieviewer-attention.d.mts +33 -0
- package/retrieviewer-attention.mjs +99 -0
- package/retrieviewer-cli.mjs +32 -5
- package/retrieviewer-config.mjs +14 -3
- package/retrieviewer-plugin.d.mts +1 -1
- package/retrieviewer-plugin.mjs +1 -1
- package/retrieviewer.d.mts +6 -0
- package/retrieviewer.mjs +3 -0
package/ATTENTION.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Attention preview
|
|
2
|
+
|
|
3
|
+
Opt-in, metadata-only StackBite notifications. This integration does **not** see
|
|
4
|
+
screens, click controls, answer questions, or approve anything. LLM use is OFF by
|
|
5
|
+
default; see the separate [opt-in paid DeepSeek backend](BRAIN.md).
|
|
6
|
+
The existing puppy art, rendering and physics stay in place.
|
|
7
|
+
|
|
8
|
+
For a local browser motion review with explicitly simulated events:
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
node tools/preview-attention.mjs
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Open its loopback URL. This page does not connect to StackBite or inspect any
|
|
15
|
+
screen; its pending items are fixtures. The preview server exits after ten minutes;
|
|
16
|
+
the loaded page can keep animating locally. Relaunch the command to reload later.
|
|
17
|
+
|
|
18
|
+
## Run the installed package
|
|
19
|
+
|
|
20
|
+
After `npm install -g @bitefoundry/retrieviewer@latest`, run from a live StackBite
|
|
21
|
+
Seat on the same computer as the desktop overlay, from any directory:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
retrieviewer attention --json
|
|
25
|
+
retrieviewer desktop --watch stackbite
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Choose the StackBite window. If a companion is already running, stop it explicitly
|
|
29
|
+
with `retrieviewer desktop-stop`, then start the watcher. An existing
|
|
30
|
+
companion is never silently replaced. Check `desktop-status --json` for
|
|
31
|
+
`watch.source`, `watch.state` and `watch.pendingCount`.
|
|
32
|
+
|
|
33
|
+
Source development commands such as `node retrieviewer-cli.mjs` require the
|
|
34
|
+
repository root as the current directory; installed commands do not.
|
|
35
|
+
Ordinary `desktop` leaves watching OFF. Stop with
|
|
36
|
+
`desktop-stop`; restart without `--watch` to remove observation. No persistent
|
|
37
|
+
watch setting, host patch, hook, auto-start, account, model or new dependency is added.
|
|
38
|
+
|
|
39
|
+
## What it reads
|
|
40
|
+
|
|
41
|
+
Only the fixed read-only command `stackbite agent seats --output json`, at most
|
|
42
|
+
once every five seconds after the previous read finishes. StackBite revalidates
|
|
43
|
+
the inherited caller Seat/generation. The source requires a live Room binding;
|
|
44
|
+
launching from an unrelated terminal is refused rather than guessing credentials.
|
|
45
|
+
No raw database, credential file, terminal transcript, screenshot, microphone,
|
|
46
|
+
clipboard, provider hook or browser session is read by Retrieviewer.
|
|
47
|
+
|
|
48
|
+
The current `seat_status` v1 projection supplies `needs_input`, generation,
|
|
49
|
+
turn/request identity and typed approval/question facts. It outranks legacy
|
|
50
|
+
fields, including when unknown/resolved. Only live, generation-matched Seats
|
|
51
|
+
with a positive turn ID or stable input request ID qualify. Older explicit
|
|
52
|
+
`awaiting_user`/`blocked` lifecycle facts require a positive turn ID. Generic input
|
|
53
|
+
is **not proof of a particular question**; unknown states are never inferred from
|
|
54
|
+
prose. Onboarding and questions attached to stopped Seats are excluded. Other
|
|
55
|
+
Rooms, exact sidebar item positions and pixel detection are not supported yet.
|
|
56
|
+
SSH: install/run the overlay where the visible desktop lives. A Room binding on
|
|
57
|
+
a remote server does not automatically give a local Windows pet remote access.
|
|
58
|
+
|
|
59
|
+
## Behavior and limits
|
|
60
|
+
|
|
61
|
+
- A new exact Room/Seat/generation/turn/request cues one connected sit-and-look-left.
|
|
62
|
+
Left means the sidebar direction, not a detected button coordinate.
|
|
63
|
+
- Explicit ball/treat/tug interactions win. A rejected cue retries only while
|
|
64
|
+
its source fact is fresh. Distinct cues are at least 20 seconds apart.
|
|
65
|
+
- Resolved/disconnected/stale facts clear the cue. Source errors are unavailable,
|
|
66
|
+
not “zero pending.” A hidden/paused pet aborts observation and refreshes on resume.
|
|
67
|
+
- At most 32 pending items and 128 dedupe identities are retained, in memory only.
|
|
68
|
+
Excess/malformed snapshots fail closed. No unlimited history or recording.
|
|
69
|
+
- Each read has a 4-second timeout and 256 KiB output cap. No overlapping reads,
|
|
70
|
+
separate resident watcher, additional timer or shell command interpreter.
|
|
71
|
+
The CLI briefly runs a child process; it is not free in CPU/peak RAM.
|
|
72
|
+
|
|
73
|
+
The first Windows 1280x800 off-screen startup probes measured Node RSS of about
|
|
74
|
+
106 MiB with watch off and 107 MiB after one live metadata read with watch on.
|
|
75
|
+
These are separate short runs, not a steady-state/peak comparison or a guarantee
|
|
76
|
+
of 1 MiB overhead. Native-helper and transient StackBite CLI memory are additional.
|
|
77
|
+
No new image payload, model or runtime package dependency was added.
|
|
78
|
+
|
|
79
|
+
Windows additionally displays a small count/offline label in the existing control
|
|
80
|
+
bar. Non-Windows helpers do not yet display that label; their live StackBite
|
|
81
|
+
integration remains unverified. Capture/overlay support is a separate platform
|
|
82
|
+
question. No new Wayland or AlmaLinux compatibility claim is made here.
|
|
83
|
+
|
|
84
|
+
## Reuse decision
|
|
85
|
+
|
|
86
|
+
The first source already exists in StackBite, so a fixed-argv adapter is smaller
|
|
87
|
+
than importing an agent desktop app. No external source was copied in this patch.
|
|
88
|
+
Future capture permissions can follow
|
|
89
|
+
[AI-Desktop-Pet](https://github.com/AkshitIreddy/AI-Desktop-Pet); pointing/coordinate
|
|
90
|
+
mapping can draw from [Blepper](https://github.com/Ruthheasman/Blepper). Neither
|
|
91
|
+
capture nor a permissive model action parser belongs in this metadata-only slice.
|
|
92
|
+
Observer is AGPL and current Screenpipe is source-available, not an MIT dependency.
|
package/BRAIN.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Optional DeepSeek brain
|
|
2
|
+
|
|
3
|
+
The selected backend is OpenRouter's paid `deepseek/deepseek-v4.1-flash-1017`.
|
|
4
|
+
No local model, SDK, renderer, dependency, image asset, or permanent watcher is added.
|
|
5
|
+
The existing offline pet and rules remain the default.
|
|
6
|
+
|
|
7
|
+
## Install and run
|
|
8
|
+
|
|
9
|
+
Update once: `npm install -g @bitefoundry/retrieviewer@latest`.
|
|
10
|
+
Then use the installed `retrieviewer` command from any directory; no checkout or
|
|
11
|
+
`cd` is required. Node's `node retrieviewer-cli.mjs` instead looks in the current
|
|
12
|
+
directory and fails with `MODULE_NOT_FOUND` outside the source repository.
|
|
13
|
+
|
|
14
|
+
Set `OPENROUTER_API_KEY` in the launching terminal's environment using your secret
|
|
15
|
+
manager or a hidden terminal prompt. Do not paste it in chat, command arguments,
|
|
16
|
+
source files, screenshots, `.env` files or the repository. Nothing saves the key.
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
retrieviewer brain-check --json
|
|
20
|
+
retrieviewer desktop --watch stackbite --brain openrouter
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The first command sends one **synthetic** approval event, never a real Room,
|
|
24
|
+
and incurs API usage charges.
|
|
25
|
+
The second must run inside a live StackBite Seat on the display computer. Stop an
|
|
26
|
+
existing companion explicitly with `desktop-stop` first; it is never replaced
|
|
27
|
+
automatically. `desktop-status --json` exposes sanitized `brain` diagnostics and
|
|
28
|
+
`watch.lastJudgment`. Restart without `--brain` to disable the backend. No key is
|
|
29
|
+
required for help/status, offline mode, or metadata-only watching.
|
|
30
|
+
|
|
31
|
+
For source development only, first change to your Retrieviewer checkout directory
|
|
32
|
+
and substitute `node retrieviewer-cli.mjs` for `retrieviewer`. Do not copy that
|
|
33
|
+
relative source command into an unrelated directory. The installed command does
|
|
34
|
+
not require this machine-specific path.
|
|
35
|
+
|
|
36
|
+
## What leaves the PC
|
|
37
|
+
|
|
38
|
+
Only fixed system instructions and this allowlisted anonymous state:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{"event":"pending_input","kind":"approval","pending_count":1}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
No Room/Seat/request identifiers, labels, task text, terminal output, screenshots,
|
|
45
|
+
clipboard or credentials appear in prompts. The key goes only in the Authorization
|
|
46
|
+
header to the fixed HTTPS OpenRouter URL, never to helper or StackBite child
|
|
47
|
+
processes. Redirects are refused. Provider data collection is denied; if that
|
|
48
|
+
policy leaves no eligible route, it falls back rather than weakening the policy.
|
|
49
|
+
|
|
50
|
+
## Judgment and fallback
|
|
51
|
+
|
|
52
|
+
The model may return only `{"decision":"notify"}`, `ignore`, or `uncertain`.
|
|
53
|
+
JSON mode is not a correctness guarantee: code rejects extra keys, malformed/
|
|
54
|
+
truncated output, tool calls and responses over 32 KiB. No returned text is run,
|
|
55
|
+
typed, used as a coordinate, or displayed as a factual summary.
|
|
56
|
+
|
|
57
|
+
This first connection is **advisory on known pending-input facts**, not screen
|
|
58
|
+
understanding. A confirmed approval/question cannot be suppressed by an LLM:
|
|
59
|
+
`notify` follows the model; `ignore` is overridden by the rule; uncertain/errors
|
|
60
|
+
fall back. Diagnostics distinguish `model_notify`, `rule_override`, and
|
|
61
|
+
`rule_fallback`. With only kind/count facts, the model adds little semantic value;
|
|
62
|
+
this slice establishes a bounded backend before optional richer perception.
|
|
63
|
+
|
|
64
|
+
Each exact pending item is judged once while fresh. Resolution, generation
|
|
65
|
+
replacement, stale source, pause, hide and shutdown cancel its request; late
|
|
66
|
+
answers cannot resurrect a notice. Direct ball/treat/tug still wins. Animation
|
|
67
|
+
ticks never await HTTP. Network timeout is 3 seconds; a host-tick guard falls back
|
|
68
|
+
within 3.5 seconds of active time even if a custom judge hangs. `brain-check`
|
|
69
|
+
allows 8 seconds for diagnosis, not for normal pet reactions.
|
|
70
|
+
|
|
71
|
+
## Resource and request limits
|
|
72
|
+
|
|
73
|
+
- One inference at a time. No automatic network retries or alternate-model fallback.
|
|
74
|
+
- Fixed paid model with provider price ceilings of $0.30 per million input tokens
|
|
75
|
+
and $1.20 per million output tokens, with per-request fees disallowed. These are
|
|
76
|
+
unit-price limits, not an account spending cap. Thinking disabled,
|
|
77
|
+
temperature 0, maximum 96 output tokens; no tools or plugins.
|
|
78
|
+
- Up to 16 anonymous successful judgments cached for 60 seconds. Same state can
|
|
79
|
+
reuse an answer without another request; source identity/freshness stays local.
|
|
80
|
+
- Default 40 HTTP attempts per UTC day **per process lifetime**. Restarts reset
|
|
81
|
+
this guard, and other apps share account limits; it is not a global quota meter.
|
|
82
|
+
- A 429 backs off for 60 seconds. Other request failures back off for 10 seconds.
|
|
83
|
+
No new calls while the pet is hidden/paused or the source has no fresh events.
|
|
84
|
+
- Requests use bounded transient buffers; the API adds network/HTTP runtime costs.
|
|
85
|
+
No claim of zero RAM overhead or guaranteed endpoint latency is made.
|
|
86
|
+
|
|
87
|
+
The catalog price on 2026-10-24 was $0.15 input / $0.60 output per million tokens,
|
|
88
|
+
with scheduled peak pricing at $0.30 / $1.20. Provider prices vary; the request
|
|
89
|
+
ceilings fail closed if no compatible route remains. Check the
|
|
90
|
+
[model and provider prices](https://openrouter.ai/deepseek/deepseek-v4.1-flash-1017/providers)
|
|
91
|
+
before budgeting. Even an aborted/timed-out request can incur provider usage.
|
|
92
|
+
|
|
93
|
+
Reference contracts: [OpenRouter parameters](https://openrouter.ai/docs/api-reference/parameters),
|
|
94
|
+
[provider routing](https://openrouter.ai/docs/guides/routing/provider-selection),
|
|
95
|
+
[reasoning](https://openrouter.ai/docs/guides/best-practices/reasoning-tokens).
|
|
96
|
+
The state/closed-choice design follows the TypeSafe skill's guidance, but no
|
|
97
|
+
TypeSafe service or SDK is installed; the user-selected OpenRouter backend is retained.
|
|
98
|
+
|
|
99
|
+
## Verification — 2026-10-24
|
|
100
|
+
|
|
101
|
+
The user-selected paid DeepSeek endpoint returned a valid `notify` in 1,608 ms
|
|
102
|
+
through `brain-check`. A second real request through the owned Windows worker
|
|
103
|
+
returned `notify` in 1,113 ms, within the normal 3-second deadline. Both requests
|
|
104
|
+
used synthetic anonymous approval facts; no real approval was created or answered.
|
|
105
|
+
|
|
106
|
+
The native test used a disposable Room fixture and reached `model_notify`.
|
|
107
|
+
Ball interaction kept priority, the pet kept rendering, resolution cleared the
|
|
108
|
+
pending event, and worker/painter both exited. Node RSS was about 128 MiB in that
|
|
109
|
+
short run, excluding helper/CLI memory. This is not an incremental or steady-state
|
|
110
|
+
RAM measurement. Existing live companion state was not changed.
|
|
111
|
+
|
|
112
|
+
All 217 applicable tests passed (two macOS-only checks skipped), including mocked
|
|
113
|
+
JSON validation, price ceilings, timeout, cache, budget, cancellation and stale
|
|
114
|
+
results. The earlier free Gemma attempt returned 429 and never produced a verified
|
|
115
|
+
live decision; it is no longer the selected backend. Privacy restrictions remain.
|
package/INSTALLING.md
CHANGED
|
@@ -99,6 +99,31 @@ Inside OpenCode, `/retrieviewer enable`, `/retrieviewer disable`,
|
|
|
99
99
|
`/retrieviewer ball`, `/retrieviewer treat`, `/retrieviewer status` control the native
|
|
100
100
|
pet. The adapter owns no raw keyboard or stdout; the host owns drawing and layout.
|
|
101
101
|
|
|
102
|
+
## Recovering an interrupted settings change
|
|
103
|
+
|
|
104
|
+
Settings writes use an exclusive `settings.lock`. New locks record the owning
|
|
105
|
+
PID, hostname, start time and a random token. A busy error prints the exact lock
|
|
106
|
+
path and PID when known. An old or empty lock may have no owner information.
|
|
107
|
+
Locks are deliberately **not** deleted just because they are old or a PID probe
|
|
108
|
+
fails: a long operation, PID reuse or a shared directory can make that unsafe.
|
|
109
|
+
|
|
110
|
+
If an operation crashed:
|
|
111
|
+
|
|
112
|
+
1. Stop all Retrieviewer enable/disable/disconnect commands and close all host
|
|
113
|
+
agents that can change its settings. If the directory is shared, stop writers
|
|
114
|
+
on every machine. Inspect the exact lock named in the error; an active owner
|
|
115
|
+
must finish first. Do not kill an unrelated process just because its PID matches.
|
|
116
|
+
2. With **all writers stopped**, move only that `settings.lock` aside in the same
|
|
117
|
+
directory (for example to `settings.lock.recovered`). Keep it as a backup;
|
|
118
|
+
do not overwrite an existing backup, touch `settings.json`, remove the whole
|
|
119
|
+
directory, or follow a symlink. Then retry the original command. A fresh lock
|
|
120
|
+
is acquired normally and the existing settings are preserved.
|
|
121
|
+
|
|
122
|
+
Default directory: `%USERPROFILE%\.config\retrieviewer` on Windows,
|
|
123
|
+
`~/.config/retrieviewer` on macOS/Linux. `RETRIEVIEWER_HOME` overrides it, so use
|
|
124
|
+
the path in the error rather than assuming the default. This is an **offline
|
|
125
|
+
manual recovery**, not a command to remove a live lock or automatic stale reaping.
|
|
126
|
+
|
|
102
127
|
## Evidence, not a misleading green check
|
|
103
128
|
|
|
104
129
|
Registration means the config contains our plugin URL. A runtime receipt means
|
package/MOTION-TABLES.md
CHANGED
|
@@ -19,7 +19,9 @@ reject stale output after source metadata or witnessed art changes.
|
|
|
19
19
|
gait-phase/velocity-match requirements. These cover walk entry/exit, rear run
|
|
20
20
|
into the authored impact, impact into rear pickup, carry-gather into arrival
|
|
21
21
|
seat, tug into the canonical seat, and four watch endpoints into that seat.
|
|
22
|
-
- SHA256 of every input metadata/art file used by the bake.
|
|
22
|
+
- SHA256 of every input metadata/art file used by the bake. JSON text is hashed
|
|
23
|
+
with LF line endings; binary artwork is hashed byte-for-byte. Generated text
|
|
24
|
+
checks also accept CRLF checkouts. No source images,
|
|
23
25
|
source hashes or graph search are computed by the running pet.
|
|
24
26
|
|
|
25
27
|
The generated routes preserve the existing approved preparation order. This is
|
package/README.ko.md
CHANGED
|
@@ -106,6 +106,22 @@ npm uninstall -g @bitefoundry/retrieviewer
|
|
|
106
106
|
별도로 [OpenCode 네이티브 등록](INSTALLING.md)을 했다면 해당 등록을 disconnect한
|
|
107
107
|
뒤 제거하세요. Codex/Claude/Cursor 등에 MCP만 연결해 내부 그림을 띄우는 기능은 없어요.
|
|
108
108
|
|
|
109
|
+
## StackBite 알림
|
|
110
|
+
|
|
111
|
+
0.0.14부터 [승인·입력 대기 알림](ATTENTION.md)을 선택해서 켤 수 있어요.
|
|
112
|
+
화면 캡처나 자동 승인 없이 현재 Room의 메타데이터만 읽어요.
|
|
113
|
+
업데이트 후, 화면이 보이는 PC의 실행 중인 StackBite Seat에서 실행하세요.
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
retrieviewer desktop --watch stackbite --brain openrouter
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
[유료 DeepSeek 연결](BRAIN.md)에는 같은 터미널의 `OPENROUTER_API_KEY`가 필요해요.
|
|
120
|
+
`--brain openrouter`를 빼면 API 없이 규칙 알림만 써요. 이미 실행 중이면 먼저
|
|
121
|
+
`retrieviewer desktop-stop`으로 종료하세요. 설치된 `retrieviewer` 명령은 어느
|
|
122
|
+
폴더에서든 쓸 수 있어요. `node retrieviewer-cli.mjs`는 소스 폴더 안에서만 쓰는
|
|
123
|
+
개발용 명령이에요.
|
|
124
|
+
|
|
109
125
|
## 직접 만든 TUI에 붙이기
|
|
110
126
|
|
|
111
127
|
JS 플러그인과 JSONL 브리지를 제공해요. 화면·입력·시간 제어는 호스트가 소유합니다.
|
package/README.md
CHANGED
|
@@ -130,6 +130,24 @@ The native OpenCode pane has Windows/OpenCode 2.1.2 host evidence; Codex, Claude
|
|
|
130
130
|
Cursor, Grok and Antigravity **in-terminal** renderers are not implemented.
|
|
131
131
|
An MCP connection alone cannot insert a persistent picture into those clients.
|
|
132
132
|
|
|
133
|
+
## Optional StackBite alerts
|
|
134
|
+
|
|
135
|
+
Version 0.0.14 adds [metadata-only StackBite alerts](./ATTENTION.md).
|
|
136
|
+
It is opt-in and does not capture screens or operate other apps.
|
|
137
|
+
An [optional paid DeepSeek/OpenRouter backend](./BRAIN.md) can judge anonymous event metadata;
|
|
138
|
+
offline rules remain the default and preserve confirmed pending alerts.
|
|
139
|
+
|
|
140
|
+
After updating, run inside a live StackBite Seat on the display computer:
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
retrieviewer desktop --watch stackbite --brain openrouter
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The paid backend requires `OPENROUTER_API_KEY` in that terminal's environment.
|
|
147
|
+
Omit `--brain openrouter` for rule-only alerts. Quit an existing companion first
|
|
148
|
+
with `retrieviewer desktop-stop`. These installed commands work from any folder;
|
|
149
|
+
`node retrieviewer-cli.mjs` is for developers running in the source checkout.
|
|
150
|
+
|
|
133
151
|
## Build, contribute, be kind
|
|
134
152
|
|
|
135
153
|
```sh
|
package/RELEASING.md
CHANGED
|
@@ -1,4 +1,50 @@
|
|
|
1
|
-
# Retrieviewer 0.0.
|
|
1
|
+
# Retrieviewer 0.0.14 release process
|
|
2
|
+
|
|
3
|
+
Current target: `@bitefoundry/retrieviewer@0.0.14`, public `latest`.
|
|
4
|
+
On 2026-10-24 the owner authorized npm publication of the metadata-only StackBite
|
|
5
|
+
watcher and optional paid DeepSeek V4.1 Flash 1017 backend. Existing artwork and
|
|
6
|
+
physics are unchanged. Watching and remote inference both remain opt-in.
|
|
7
|
+
|
|
8
|
+
The installed `retrieviewer` bin works from any directory; relative source
|
|
9
|
+
commands require the checkout root. Include this unrelated-working-directory
|
|
10
|
+
regression in the fresh installed-consumer check. Keep model identity, provider
|
|
11
|
+
price ceilings, privacy restrictions, daily per-process attempt guard, deadlines,
|
|
12
|
+
cache bounds, cancellation and rule fallback. No API keys ship or are saved.
|
|
13
|
+
|
|
14
|
+
Build/install a fresh exact artifact, compare every shipped byte, run tests and
|
|
15
|
+
an owned native worker check, then publish the same tarball. Verify registry
|
|
16
|
+
integrity and a fresh registry consumer. Earlier versions stay immutable; do not
|
|
17
|
+
restart the user's pet, patch the host, or certify untested macOS/Linux behavior.
|
|
18
|
+
No new runtime dependency or local model is added. Website deployment is outside
|
|
19
|
+
this release request.
|
|
20
|
+
|
|
21
|
+
## Previous 0.0.13 scope (historical)
|
|
22
|
+
|
|
23
|
+
Current target: `@bitefoundry/retrieviewer@0.0.13`, public `latest`.
|
|
24
|
+
The owner authorized npm publication and website deployment on2026-10-24.
|
|
25
|
+
This release contains the reviewed reliability fixes, with unchanged art/motion:
|
|
26
|
+
|
|
27
|
+
- Canonical LF text/hash checks and CRLF regression coverage; binary art stays
|
|
28
|
+
byte-for-byte unchanged. Git attributes and existing JSON text use LF.
|
|
29
|
+
- Exclusive settings locks record the owner PID/host/time/token. Busy errors
|
|
30
|
+
identify the lock and link to the offline manual recovery in INSTALLING.md.
|
|
31
|
+
There is no automatic removal of possibly live locks.
|
|
32
|
+
- Workers require ready plus an acknowledged detach to survive launcher loss.
|
|
33
|
+
Startup disconnect/launcher-death tests cover worker, painter and session cleanup.
|
|
34
|
+
- Oversized Windows grids hide until they fit again; plugin timer delays cannot
|
|
35
|
+
overflow Node's signed32-bit timeout bound into a1ms loop.
|
|
36
|
+
- Windows20 / Linux22 Node CI on push and PR, including installed-consumer checks
|
|
37
|
+
and an owned Windows native diagnostic. Website copy discloses page visit and
|
|
38
|
+
performance analytics separately from the desktop pet.
|
|
39
|
+
|
|
40
|
+
Build and install a fresh exact tarball outside the source tree, compare installed
|
|
41
|
+
bytes and test its native/browser runtimes. Publish that same artifact after the
|
|
42
|
+
new remote CI succeeds; verify registry integrity and matching Pages runtime.
|
|
43
|
+
No new runtime dependency, process, background timer or art cache. No claim of
|
|
44
|
+
new macOS/Linux GUI certification or a material RAM reduction. Preserve all
|
|
45
|
+
earlier npm artifacts, MIT code / CC BY-SA4.0 art and the user's running pet.
|
|
46
|
+
|
|
47
|
+
## Previous 0.0.12 scope (historical)
|
|
2
48
|
|
|
3
49
|
Current target: `@bitefoundry/retrieviewer@0.0.12`, public `latest`.
|
|
4
50
|
On2026-10-24 the owner authorized npm publication and website deployment of the
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const OPENROUTER_MODEL: 'deepseek/deepseek-v4.1-flash-1017';
|
|
2
|
+
export type BrainDecision = 'notify' | 'ignore' | 'uncertain';
|
|
3
|
+
export interface BrainObservation {kind: 'approval' | 'input' | 'none'; pendingCount: number; signal?: AbortSignal;}
|
|
4
|
+
export interface BrainStatus {
|
|
5
|
+
provider: 'openrouter'; model: typeof OPENROUTER_MODEL;
|
|
6
|
+
state: 'ready' | 'requesting' | 'fallback' | 'off';
|
|
7
|
+
calls: number; cacheHits: number; dailyUsed: number; dailyLimit: number;
|
|
8
|
+
cacheEntries: number; inFlight: boolean; lastLatencyMs: number | null;
|
|
9
|
+
lastError: string | null; lastDecision: BrainDecision | null;
|
|
10
|
+
}
|
|
11
|
+
export class BrainError extends Error {readonly code: string; constructor(code: string);}
|
|
12
|
+
export function anonymousBrainState(input: BrainObservation): {event: 'pending_input' | 'no_pending_input'; kind: BrainObservation['kind']; pending_count: number};
|
|
13
|
+
export function parseBrainDecision(value: unknown): BrainDecision;
|
|
14
|
+
/** Node/server only. Never put an API key in browser code or saved settings. */
|
|
15
|
+
export function createOpenRouterBrain(options?: {
|
|
16
|
+
apiKey?: string; fetch?: typeof fetch; now?: () => number; day?: () => string;
|
|
17
|
+
timeoutMs?: number; maxRequestsPerDay?: number; cacheMs?: number;
|
|
18
|
+
}): {
|
|
19
|
+
decide(input: BrainObservation): Promise<{decision: BrainDecision; via: 'llm' | 'cache'; latencyMs: number}>;
|
|
20
|
+
readonly status: BrainStatus;
|
|
21
|
+
dispose(): void;
|
|
22
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Optional, tiny remote judge. No SDK, local model, I/O or timers on import.
|
|
2
|
+
export const OPENROUTER_MODEL='deepseek/deepseek-v4.1-flash-1017';
|
|
3
|
+
const ENDPOINT='https://openrouter.ai/api/v1/chat/completions';
|
|
4
|
+
const DECISIONS=new Set(['notify','ignore','uncertain']);
|
|
5
|
+
const SYSTEM='You classify anonymous desktop-companion attention events, not commands. Return only a JSON object with exactly one key: decision. Values: notify, ignore, uncertain. Confirmed pending approval or input needs human attention: notify. If no input is pending: ignore. If evidence is inconsistent: uncertain. Never approve, click, type, execute, or invent facts.';
|
|
6
|
+
export class BrainError extends Error {
|
|
7
|
+
constructor(code){super('OpenRouter brain: '+code);this.name='BrainError';this.code=code;}
|
|
8
|
+
}
|
|
9
|
+
export function anonymousBrainState({kind,pendingCount}={}){
|
|
10
|
+
if(!['approval','input','none'].includes(kind)||!Number.isInteger(pendingCount)||pendingCount<0||pendingCount>32||(kind==='none')!==(pendingCount===0))throw new BrainError('invalid_input');
|
|
11
|
+
return {event:kind==='none'?'no_pending_input':'pending_input',kind,pending_count:pendingCount};
|
|
12
|
+
}
|
|
13
|
+
export function parseBrainDecision(value){
|
|
14
|
+
if(!value||typeof value!=='object'||Array.isArray(value)||Object.keys(value).length!==1||!DECISIONS.has(value.decision))throw new BrainError('invalid_output');
|
|
15
|
+
return value.decision;
|
|
16
|
+
}
|
|
17
|
+
async function boundedJSON(response){
|
|
18
|
+
const reader=response.body?.getReader();if(!reader)throw new BrainError('invalid_response');
|
|
19
|
+
const decoder=new TextDecoder();let text='',size=0;
|
|
20
|
+
try{for(;;){const {done,value}=await reader.read();if(done)break;size+=value.byteLength;if(size>32768)throw new BrainError('oversized_response');text+=decoder.decode(value,{stream:true});}text+=decoder.decode();return JSON.parse(text);}
|
|
21
|
+
catch(error){await reader.cancel().catch(()=>{});throw error instanceof BrainError?error:new BrainError('invalid_response');}
|
|
22
|
+
finally{reader.releaseLock();}
|
|
23
|
+
}
|
|
24
|
+
export function createOpenRouterBrain({apiKey=globalThis.process?.env?.OPENROUTER_API_KEY,fetch:request=globalThis.fetch,now=()=>performance.now(),day=()=>new Date().toISOString().slice(0,10),timeoutMs=3000,maxRequestsPerDay=40,cacheMs=60000}={}){
|
|
25
|
+
if(typeof apiKey!=='string'||apiKey.length<10||apiKey.length>512||/\s/.test(apiKey))throw new BrainError('missing_key');
|
|
26
|
+
if(typeof request!=='function'||typeof now!=='function'||typeof day!=='function')throw new BrainError('invalid_options');
|
|
27
|
+
if(!Number.isFinite(timeoutMs)||timeoutMs<100||timeoutMs>15000||!Number.isInteger(maxRequestsPerDay)||maxRequestsPerDay<1||maxRequestsPerDay>1000||!Number.isFinite(cacheMs)||cacheMs<0||cacheMs>300000)throw new BrainError('invalid_options');
|
|
28
|
+
const cache=new Map();let disposed=false,flight=null,dayKey=day(),used=0,calls=0,cacheHits=0,nextAllowed=0,lastLatencyMs=null,lastError=null,lastDecision=null,state='ready';
|
|
29
|
+
return {
|
|
30
|
+
async decide(input){
|
|
31
|
+
if(disposed)throw new BrainError('disposed');
|
|
32
|
+
const facts=anonymousBrainState(input),signal=input?.signal,key=JSON.stringify(facts),start=now();
|
|
33
|
+
if(signal?.aborted)throw new BrainError('aborted');
|
|
34
|
+
const stored=cache.get(key);if(stored&&start<stored.expires){cacheHits++;lastDecision=stored.decision;lastError=null;state='ready';return {decision:stored.decision,via:'cache',latencyMs:0};}
|
|
35
|
+
if(flight)throw new BrainError('busy');
|
|
36
|
+
if(start<nextAllowed)throw new BrainError('backoff');
|
|
37
|
+
const today=day();if(today!==dayKey){dayKey=today;used=0;}
|
|
38
|
+
if(used>=maxRequestsPerDay){state='fallback';lastError='daily_budget';throw new BrainError('daily_budget');}
|
|
39
|
+
const operation={controller:new AbortController()};flight=operation;used++;calls++;state='requesting';
|
|
40
|
+
let timer,rejectAbort;
|
|
41
|
+
const interrupted=new Promise((_,reject)=>{rejectAbort=reject;});
|
|
42
|
+
const abort=()=>{rejectAbort(new BrainError('aborted'));operation.controller.abort();};
|
|
43
|
+
signal?.addEventListener('abort',abort,{once:true});
|
|
44
|
+
operation.abort=abort;
|
|
45
|
+
timer=setTimeout(()=>{rejectAbort(new BrainError('timeout'));operation.controller.abort();},timeoutMs);
|
|
46
|
+
try{
|
|
47
|
+
const result=await Promise.race([(async()=>{
|
|
48
|
+
const response=await request(ENDPOINT,{
|
|
49
|
+
method:'POST',redirect:'error',signal:operation.controller.signal,
|
|
50
|
+
headers:{'Authorization':'Bearer '+apiKey,'Content-Type':'application/json'},
|
|
51
|
+
// USD per million tokens; no per-request fee or alternate-model fallback.
|
|
52
|
+
body:JSON.stringify({model:OPENROUTER_MODEL,messages:[{role:'system',content:SYSTEM},{role:'user',content:key}],temperature:0,max_tokens:96,reasoning:{enabled:false},response_format:{type:'json_object'},provider:{require_parameters:true,data_collection:'deny',max_price:{prompt:0.3,completion:1.2,request:0}}}),
|
|
53
|
+
});
|
|
54
|
+
if(!response.ok){await response.body?.cancel().catch(()=>{});throw new BrainError(response.status===429?'rate_limited':[401,403].includes(response.status)?'authorization_failed':response.status===404?'route_unavailable':'service_unavailable');}
|
|
55
|
+
const body=await boundedJSON(response),choice=body.choices?.[0];
|
|
56
|
+
if(choice?.finish_reason!=='stop'||typeof choice.message?.content!=='string'||choice.message.tool_calls?.length)throw new BrainError('invalid_output');
|
|
57
|
+
let value;try{value=JSON.parse(choice.message.content);}catch{throw new BrainError('invalid_output');}
|
|
58
|
+
return {decision:parseBrainDecision(value),via:'llm',latencyMs:Math.max(0,Math.round(now()-start))};
|
|
59
|
+
})(),interrupted]);
|
|
60
|
+
if(disposed||signal?.aborted||flight!==operation)throw new BrainError('aborted');
|
|
61
|
+
lastLatencyMs=result.latencyMs;lastError=null;lastDecision=result.decision;state='ready';
|
|
62
|
+
cache.delete(key);cache.set(key,{decision:result.decision,expires:now()+cacheMs});
|
|
63
|
+
while(cache.size>16)cache.delete(cache.keys().next().value);
|
|
64
|
+
return result;
|
|
65
|
+
}catch(error){
|
|
66
|
+
const safe=error instanceof BrainError?error:new BrainError('network_error');
|
|
67
|
+
if(!disposed){lastError=safe.code;lastLatencyMs=Math.max(0,Math.round(now()-start));state='fallback';if(safe.code!=='aborted')nextAllowed=now()+(safe.code==='rate_limited'?60000:10000);}
|
|
68
|
+
throw safe;
|
|
69
|
+
}finally{clearTimeout(timer);signal?.removeEventListener('abort',abort);if(flight===operation)flight=null;}
|
|
70
|
+
},
|
|
71
|
+
get status(){return {provider:'openrouter',model:OPENROUTER_MODEL,state,calls,cacheHits,dailyUsed:day()===dayKey?used:0,dailyLimit:maxRequestsPerDay,cacheEntries:cache.size,inFlight:flight!==null,lastLatencyMs,lastError,lastDecision};},
|
|
72
|
+
dispose(){if(disposed)return;disposed=true;flight?.abort();flight=null;cache.clear();apiKey='';state='off';},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Narrow projection of `stackbite agent seats --output json`, not terminal text.
|
|
2
|
+
// Uses StackBite's own binding validation; never reads its credential files.
|
|
3
|
+
import {execFile} from 'node:child_process';
|
|
4
|
+
import {promisify} from 'node:util';
|
|
5
|
+
import {validateAttentionSnapshot} from '../../retrieviewer-attention.mjs';
|
|
6
|
+
const execute=promisify(execFile);
|
|
7
|
+
const identifier=value=>typeof value==='string'&&/^[a-zA-Z0-9_.:-]{1,128}$/.test(value);
|
|
8
|
+
|
|
9
|
+
export function stackBiteBinding(env=process.env){
|
|
10
|
+
if(!identifier(env.STACKBITE_ROOM_ID)||!identifier(env.STACKBITE_SEAT_KEY)||!identifier(env.STACKBITE_SESSION_GENERATION)||!env.STACKBITE_STATE_DIR||!env.STACKBITE_ROOM_BRIDGE_URL)throw Error('Start desktop --watch stackbite inside a live StackBite Seat. No Room binding was found.');
|
|
11
|
+
return {roomId:env.STACKBITE_ROOM_ID};
|
|
12
|
+
}
|
|
13
|
+
export function projectStackBiteSeats(value,{roomId}={}){
|
|
14
|
+
if(!identifier(roomId)||value?.room_id!==roomId||!Array.isArray(value.seats)||value.seats.length>256)throw Error('Invalid or mismatched StackBite Room snapshot');
|
|
15
|
+
const items=[];
|
|
16
|
+
for(const seat of value.seats){
|
|
17
|
+
if(!seat||typeof seat!=='object')throw Error('Invalid StackBite Seat');
|
|
18
|
+
if(seat.is_alive!==true||!identifier(seat.seat_key)||!identifier(seat.generation))continue;
|
|
19
|
+
const status=seat.seat_status;
|
|
20
|
+
if(status!==undefined){
|
|
21
|
+
// The current public projection outranks legacy fields. Never revive an
|
|
22
|
+
// unknown/resolved/foreign projection from a stale legacy flag.
|
|
23
|
+
if(status?.schema_version!==1||status.generation!==seat.generation||status.phase!=='needs_input'||status.input_kind==='onboarding')continue;
|
|
24
|
+
const turnId=Number.isSafeInteger(status.turn_id)&&status.turn_id>0?status.turn_id:null;
|
|
25
|
+
const requestId=identifier(status.input_request_id)?status.input_request_id:undefined;
|
|
26
|
+
if(turnId===null&&requestId===undefined)continue;
|
|
27
|
+
items.push({id:seat.seat_key,generation:seat.generation,turnId,...requestId===undefined?{}:{requestId},kind:status.input_kind==='approval'?'approval':'input'});
|
|
28
|
+
}else{
|
|
29
|
+
// Older explicit lifecycle facts are accepted only with an exact turn.
|
|
30
|
+
if(!['awaiting_user','blocked'].includes(seat.lifecycle_state)||!Number.isSafeInteger(seat.turn_id)||seat.turn_id<1)continue;
|
|
31
|
+
items.push({id:seat.seat_key,generation:seat.generation,turnId:seat.turn_id,kind:seat.provider_permission_pending===true||seat.input_kind==='approval'?'approval':'input'});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return validateAttentionSnapshot({source:'stackbite',scope:roomId,items});
|
|
35
|
+
}
|
|
36
|
+
export function createStackBiteSource({env=process.env,exec=execute}={}){
|
|
37
|
+
const binding=stackBiteBinding(env);
|
|
38
|
+
const childEnv={...env};delete childEnv.OPENROUTER_API_KEY;
|
|
39
|
+
return {
|
|
40
|
+
async readSnapshot({signal}={}){
|
|
41
|
+
// Fixed argv, no shell, no input/click/report/approval command. The CLI checks
|
|
42
|
+
// that the inherited caller Seat/generation is still live on every read.
|
|
43
|
+
const {stdout}=await exec('stackbite',['agent','seats','--output','json'],{env:childEnv,signal,encoding:'utf8',windowsHide:true,timeout:4000,maxBuffer:262144});
|
|
44
|
+
return projectStackBiteSeats(JSON.parse(stdout),binding);
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -110,7 +110,7 @@ sealed class Companion : ApplicationContext {
|
|
|
110
110
|
readonly JavaScriptSerializer json=new JavaScriptSerializer();readonly object inputLock=new object();
|
|
111
111
|
readonly Queue<Dictionary<string,object>> messages=new Queue<Dictionary<string,object>>();
|
|
112
112
|
TargetItem target;Form pickerWindow;QuietWindow testTarget;readonly bool testMode;Rectangle canvas;int revision=0,cell=8;bool visible=false,paused=false,loaded=false,quitting=false,picking=false,announced=false;
|
|
113
|
-
Label status=new Label();Mutex mutex;
|
|
113
|
+
Label status=new Label();Mutex mutex;string attentionText=null;
|
|
114
114
|
internal Companion(Mutex single,bool diagnostic){
|
|
115
115
|
mutex=single;testMode=diagnostic;bar.Text="Retrieviewer controls";bar.Size=new Size(538,40);bar.BackColor=Color.FromArgb(28,30,34);bar.ForeColor=Color.White;
|
|
116
116
|
bar.Font=new Font("Segoe UI",9);bar.Controls.Add(MakeButton("Ball",8,()=>Interact("ball")));bar.Controls.Add(MakeButton("Treat",76,()=>Interact("treat")));
|
|
@@ -151,7 +151,13 @@ sealed class Companion : ApplicationContext {
|
|
|
151
151
|
string type=Program.Str(m,"type");
|
|
152
152
|
if(type=="quit"){Quit();return;}
|
|
153
153
|
if(type=="loaded"){string ink=m.ContainsKey("ink")?Program.Str(m,"ink"):"cream";if(ink!="cream"&&ink!="gold"&&ink!="dark")throw new Exception("Invalid ink");pet.Ink=ink=="gold"?Color.FromArgb(245,231,184,103):ink=="dark"?Color.FromArgb(245,67,48,24):Color.FromArgb(245,248,242,232);loaded=true;status.Text="Playing";if(testMode)((Button)bar.Controls[0]).PerformClick();continue;}
|
|
154
|
-
if(type=="interaction"){status.Text=Program.Bool(m,"accepted")?"Ready!":"Please wait";continue;}
|
|
154
|
+
if(type=="interaction"){status.Text=attentionText??(Program.Bool(m,"accepted")?"Ready!":"Please wait");continue;}
|
|
155
|
+
if(type=="attention"){
|
|
156
|
+
int count=Program.Int(m,"count");string state=Program.Str(m,"state");
|
|
157
|
+
if(count<0||count>32)throw new Exception("Invalid attention count");
|
|
158
|
+
attentionText=state=="connected"?(count>0?count+" need input":"Watching"):state=="paused"?"Watch paused":state=="connecting"?"Connecting...":"Watch offline";status.Text=attentionText;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
155
161
|
if(type=="error"){status.Text="Engine error";Quit();return;}
|
|
156
162
|
if(type=="frame"){
|
|
157
163
|
// Always acknowledge discarded frames too, otherwise resizing can deadlock the sender.
|
|
@@ -168,8 +174,10 @@ sealed class Companion : ApplicationContext {
|
|
|
168
174
|
if(target!=null&&!Native.IsIconic(target.Window)&&Native.IsWindowVisible(target.Window)){
|
|
169
175
|
next=Native.ClientBounds(target.Window);IntPtr foreground=Native.GetForegroundWindow();show=!picking&&(testMode||foreground==target.Window||foreground==bar.Handle)&&next.Width>=160&&next.Height>=120;
|
|
170
176
|
}
|
|
171
|
-
int nextCell=
|
|
172
|
-
|
|
177
|
+
int nextCell=Program.GridCell(next.Width,next.Height);
|
|
178
|
+
// A huge target may exceed the transport budget even at the largest cell.
|
|
179
|
+
// Hide until its size fits again; never send an invalid visible grid.
|
|
180
|
+
if(nextCell==0){show=false;nextCell=16;}
|
|
173
181
|
int columns=next.Width/nextCell,rows=next.Height/(nextCell*2);
|
|
174
182
|
if(force||show!=visible||next!=canvas||nextCell!=cell){
|
|
175
183
|
visible=show;canvas=next;cell=nextCell;revision++;pet.Hide();
|
|
@@ -237,6 +245,13 @@ static class Program {
|
|
|
237
245
|
using(Companion context=new Companion(single,ipcTest))Application.Run(context);return 0;
|
|
238
246
|
}catch(Exception error){Emit(new {type="error",message=error.Message});return 1;}
|
|
239
247
|
}
|
|
248
|
+
internal static int GridCell(int width,int height){
|
|
249
|
+
foreach(int size in new int[]{8,10,12,16}){
|
|
250
|
+
long columns=(long)width/size,rows=(long)height/(size*2);
|
|
251
|
+
if(columns>0&&rows>0&&columns*rows<=125000)return size;
|
|
252
|
+
}
|
|
253
|
+
return 0;
|
|
254
|
+
}
|
|
240
255
|
static int SelfTest(string proof){
|
|
241
256
|
// Uses only our own off-screen windows. Does NOT click/type in or capture a user application.
|
|
242
257
|
IntPtr foreground=Native.GetForegroundWindow();
|
|
@@ -253,8 +268,9 @@ static class Program {
|
|
|
253
268
|
Native.ShowWindow(target.Handle,7);Application.DoEvents();bool minimized=Native.IsIconic(target.Handle);pet.Hide();bool hidden=!pet.Visible;
|
|
254
269
|
Native.ShowWindow(target.Handle,4);Application.DoEvents();bool restored=!Native.IsIconic(target.Handle);
|
|
255
270
|
bool focus=Native.GetForegroundWindow()==foreground;
|
|
256
|
-
bool
|
|
257
|
-
|
|
271
|
+
bool gridBounds=GridCell(3840,2160)==8&&GridCell(7680,4320)==12&&GridCell(32000,18000)==0&&GridCell(int.MaxValue,int.MaxValue)==0&&GridCell(0,0)==0;
|
|
272
|
+
bool passed=clickThrough&&noActivate&&layered&&follows&&minimized&&hidden&&restored&&focus&&pet.PaintedDots>0&&gridBounds;
|
|
273
|
+
Emit(new {type="self-test",passed=passed,gridBounds=gridBounds,clickThroughStyle=clickThrough,noActivateStyle=noActivate,layered=layered,followGeometry=follows,expectedX=expectedX,expectedY=expectedY,actualX=actual.Left,actualY=actual.Top,minimize=minimized,hide=hidden,restore=restored,foregroundPreserved=focus,dots=pet.PaintedDots,proof=proof});return passed?0:1;
|
|
258
274
|
}
|
|
259
275
|
}
|
|
260
276
|
}
|
|
@@ -93,18 +93,33 @@ export async function desktopHelper(options={}){
|
|
|
93
93
|
throw Error('Linux desktop needs system Python3.6+ (AlmaLinux8 platform-python is supported). No packages or system settings were installed.');
|
|
94
94
|
}
|
|
95
95
|
export async function startDesktop(options={}){
|
|
96
|
+
const watch=options.watch??'off';if(!['off','stackbite'].includes(watch))throw Error('Invalid desktop watch source');
|
|
97
|
+
const brain=options.brain??'off';if(!['off','openrouter'].includes(brain))throw Error('Invalid desktop brain');
|
|
98
|
+
if(brain==='openrouter'){
|
|
99
|
+
if(watch!=='stackbite')throw Error('--brain openrouter requires --watch stackbite');
|
|
100
|
+
const {createOpenRouterBrain}=await import('../openrouter/brain.mjs');createOpenRouterBrain().dispose();
|
|
101
|
+
}
|
|
102
|
+
if(watch==='stackbite'){const {stackBiteBinding}=await import('../stackbite/attention.mjs');stackBiteBinding();}
|
|
96
103
|
const ink=options.ink??'cream';if(!['cream','gold','dark'].includes(ink))throw Error('Invalid desktop ink');
|
|
97
104
|
const returnStyle=options.returnStyle??'auto';if(!['auto','brake','trot','run-trot'].includes(returnStyle))throw Error('Invalid return style');
|
|
98
105
|
// Fail before creating any cache/session on unsupported desktops (e.g. Wayland).
|
|
99
106
|
desktopPlatform(options);
|
|
100
|
-
const status=await desktopStatus(options);if(status.running)return {...status,alreadyRunning:true,...status.returnStyle!==returnStyle?{requestedReturnStyle:returnStyle,restartRequired:true}:{}};
|
|
107
|
+
const status=await desktopStatus(options);if(status.running)return {...status,alreadyRunning:true,...status.returnStyle!==returnStyle?{requestedReturnStyle:returnStyle,restartRequired:true}:{},...(status.watch?.source??'off')!==watch?{requestedWatch:watch,restartRequired:true}:{},...(status.brain?.provider??'off')!==brain?{requestedBrain:brain,restartRequired:true}:{}};
|
|
101
108
|
if(status.stale)await stopDesktop(options);
|
|
102
109
|
const helper=await desktopHelper(options);
|
|
110
|
+
const workerEnv={...process.env,RETRIEVIEWER_HOME:getConfigPaths(options).root,RETRIEVIEWER_INK:ink,RETRIEVIEWER_RETURN_STYLE:returnStyle,RETRIEVIEWER_WATCH:watch,RETRIEVIEWER_BRAIN:brain};
|
|
111
|
+
if(brain==='off')delete workerEnv.OPENROUTER_API_KEY;
|
|
103
112
|
return new Promise((resolve,reject)=>{
|
|
104
|
-
const child=spawn(process.execPath,[fileURLToPath(new URL('./worker.mjs',import.meta.url)),'--helper-spec',JSON.stringify(helper)],{detached:true,windowsHide:true,stdio:['ignore','ignore','ignore','ipc'],env:
|
|
105
|
-
let settled=false;const finish=(error,message)=>{if(settled)return;settled=true;clearTimeout(timer);if(child.connected)child.disconnect();child.unref();error?reject(error):resolve(message);};
|
|
106
|
-
|
|
107
|
-
|
|
113
|
+
const child=spawn(process.execPath,[fileURLToPath(new URL('./worker.mjs',import.meta.url)),'--helper-spec',JSON.stringify(helper)],{detached:true,windowsHide:true,stdio:['ignore','ignore','ignore','ipc'],env:workerEnv});
|
|
114
|
+
let settled=false,ready=null;const finish=(error,message)=>{if(settled)return;settled=true;clearTimeout(timer);if(child.connected)child.disconnect();child.unref();error?reject(error):resolve(message);};
|
|
115
|
+
// Disconnect on failure aborts startup. Only an acknowledged detach may live on.
|
|
116
|
+
const timer=setTimeout(()=>finish(Error('Desktop startup timed out; no agent settings changed')),20000);
|
|
117
|
+
child.on('error',error=>finish(error));child.on('message',message=>{
|
|
118
|
+
if(settled)return;
|
|
119
|
+
if(message?.error)return finish(Error(message.error));
|
|
120
|
+
if(message?.detached===true&&ready)return finish(null,ready);
|
|
121
|
+
if(message?.running===true&&!ready){ready=message;child.send({command:'detach'},error=>{if(error)finish(error);});}
|
|
122
|
+
});child.on('disconnect',()=>{if(!settled)finish(Error('Desktop disconnected during startup'));});child.on('exit',code=>finish(Error('Desktop exited during startup ('+code+')')));
|
|
108
123
|
});
|
|
109
124
|
}
|
|
110
125
|
export async function cleanDesktop(options={}){
|
|
@@ -42,6 +42,8 @@ export function createFramePump({pet,send,now=()=>performance.now()}){
|
|
|
42
42
|
try{send(frame);lastFrame=frame;}catch(error){inFlight=false;lastFrame=null;nextPaint=0;throw error;}return true;
|
|
43
43
|
},
|
|
44
44
|
interact(kind){alive();if(!['ball','treat','tug'].includes(kind))throw Error('Invalid overlay interaction');const accepted=pet.interact(kind);if(accepted){lastFrame=null;nextPaint=0;}return accepted;},
|
|
45
|
+
notify(notice){alive();const accepted=pet.notify(notice);if(accepted){lastFrame=null;nextPaint=0;}return accepted;},
|
|
46
|
+
clearNotice(){alive();pet.clearNotice();lastFrame=null;nextPaint=0;},
|
|
45
47
|
get hidden(){return !viewport||paused;},
|
|
46
48
|
dispose(){if(!disposed){disposed=true;viewport=lastFrame=null;pet.dispose();}},
|
|
47
49
|
};
|