@bitefoundry/retrieviewer 0.0.13 → 0.0.15
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 +122 -0
- package/BRAIN.md +125 -0
- package/README.ko.md +18 -0
- package/README.md +22 -0
- package/RELEASING.md +39 -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 +24 -6
- package/integrations/windows-overlay/desktop.mjs +11 -2
- package/integrations/windows-overlay/protocol.mjs +2 -0
- package/integrations/windows-overlay/worker.mjs +21 -4
- package/package.json +16 -1
- package/puppy-companion-life.mjs +31 -3
- package/retrieviewer-attention.d.mts +35 -0
- package/retrieviewer-attention.mjs +107 -0
- package/retrieviewer-cli.mjs +32 -5
- package/retrieviewer.d.mts +6 -0
- package/retrieviewer.mjs +3 -0
package/ATTENTION.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
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 attention episode:
|
|
62
|
+
brake/recover and face the user, approach using the existing world path if more
|
|
63
|
+
than 0.8 m from the near check-in point, then sit and look left. Near pets stay
|
|
64
|
+
in place. Resolution during approach brakes the obsolete journey naturally.
|
|
65
|
+
Left means the sidebar direction, not a detected button coordinate.
|
|
66
|
+
- The rule cue is accepted before optional inference starts. LLM latency, failure
|
|
67
|
+
or disagreement never delays, repeats or retracts a confirmed pending alert.
|
|
68
|
+
This does not remove the existing five-second source polling interval.
|
|
69
|
+
- Explicit ball/treat/tug interactions win. A rejected cue retries only while
|
|
70
|
+
its source fact is fresh. Distinct cues are at least 20 seconds apart.
|
|
71
|
+
- Resolved/disconnected/stale facts clear the cue. Source errors are unavailable,
|
|
72
|
+
not “zero pending.” A hidden/paused pet aborts observation and refreshes on resume.
|
|
73
|
+
- At most 32 pending items and 128 dedupe identities are retained, in memory only.
|
|
74
|
+
Excess/malformed snapshots fail closed. No unlimited history or recording.
|
|
75
|
+
- Each read has a 4-second timeout and 256 KiB output cap. No overlapping reads,
|
|
76
|
+
separate resident watcher, additional timer or shell command interpreter.
|
|
77
|
+
The CLI briefly runs a child process; it is not free in CPU/peak RAM.
|
|
78
|
+
|
|
79
|
+
The first Windows 1280x800 off-screen startup probes measured Node RSS of about
|
|
80
|
+
106 MiB with watch off and 107 MiB after one live metadata read with watch on.
|
|
81
|
+
These are separate short runs, not a steady-state/peak comparison or a guarantee
|
|
82
|
+
of 1 MiB overhead. Native-helper and transient StackBite CLI memory are additional.
|
|
83
|
+
No new image payload, model or runtime package dependency was added.
|
|
84
|
+
|
|
85
|
+
Windows displays the pending Seat's bounded `role_title` and approval/input kind
|
|
86
|
+
in the existing control bar, with ellipsis and a full-text hover tooltip. The
|
|
87
|
+
local `watch.target` and `attentionLabel` diagnostics expose that same target.
|
|
88
|
+
Names are at most 64 UTF-16 code units, control/directional characters are removed,
|
|
89
|
+
and no task text is retained. Labels never enter model input or saved settings.
|
|
90
|
+
Renaming/replacing a target refreshes the label even if the count is unchanged;
|
|
91
|
+
renaming alone is not a new alert. Missing names use the Seat ID as a fallback.
|
|
92
|
+
Non-Windows helpers do not yet display that label; their live StackBite
|
|
93
|
+
integration remains unverified. Capture/overlay support is a separate platform
|
|
94
|
+
question. No new Wayland or AlmaLinux compatibility claim is made here.
|
|
95
|
+
|
|
96
|
+
Version 0.0.15 includes these rule-first/target/approach improvements after the
|
|
97
|
+
0.0.14 baseline. A simulated frozen
|
|
98
|
+
LLM and a time-skipped distant notification are available in the local preview.
|
|
99
|
+
They never write StackBite state or make model requests. Real provider events
|
|
100
|
+
still depend on StackBite publishing trustworthy `needs_input` metadata.
|
|
101
|
+
|
|
102
|
+
Working-copy verification on 2026-10-24: focused tests cover immediate delivery
|
|
103
|
+
with hung/disagreeing advice, local-only bounded labels, same-count renaming,
|
|
104
|
+
late-result cancellation, far-to-near motion, and resolution during approach.
|
|
105
|
+
An owned Windows worker/native-helper run displayed and renamed the actual label,
|
|
106
|
+
cleared it to `Watching`, preserved direct ball priority and exited both children.
|
|
107
|
+
The fixture run used about 112 MiB Node RSS (no LLM), excluding the helper/CLI.
|
|
108
|
+
Matched short watch-off native startup probes of published 0.0.14 and the working
|
|
109
|
+
copy both rounded to 106 MiB Node RSS. These are not steady-state or peak-memory
|
|
110
|
+
guarantees. All nine packaged art archives are byte-identical; no dependency,
|
|
111
|
+
additional resident process or observer timer was added. Labels are bounded
|
|
112
|
+
metadata, not a promise of literally zero additional bytes or allocations.
|
|
113
|
+
|
|
114
|
+
## Reuse decision
|
|
115
|
+
|
|
116
|
+
The first source already exists in StackBite, so a fixed-argv adapter is smaller
|
|
117
|
+
than importing an agent desktop app. No external source was copied in this patch.
|
|
118
|
+
Future capture permissions can follow
|
|
119
|
+
[AI-Desktop-Pet](https://github.com/AkshitIreddy/AI-Desktop-Pet); pointing/coordinate
|
|
120
|
+
mapping can draw from [Blepper](https://github.com/Ruthheasman/Blepper). Neither
|
|
121
|
+
capture nor a permissive model action parser belongs in this metadata-only slice.
|
|
122
|
+
Observer is AGPL and current Screenpipe is source-available, not an MIT dependency.
|
package/BRAIN.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
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. Since 0.0.15, a confirmed approval/question is
|
|
59
|
+
delivered by the rule first; optional model work starts only after the pet accepts
|
|
60
|
+
the cue. It cannot delay, suppress, repeat or retract the cue. Diagnostics show
|
|
61
|
+
`rule_first` while advice is pending, then `model_notify` for agreement,
|
|
62
|
+
`rule_override` for disagreement, or `rule_fallback` for uncertain/errors.
|
|
63
|
+
The 0.0.14 baseline waited for advice. With only kind/count facts, the model adds little semantic value;
|
|
64
|
+
this slice establishes a bounded backend before optional richer perception.
|
|
65
|
+
|
|
66
|
+
Each accepted exact pending item is judged once while fresh; rejected cues during
|
|
67
|
+
direct play do not spend requests. Seat display names stay local. Resolution, generation
|
|
68
|
+
replacement, stale source, pause, hide and shutdown cancel its request; late
|
|
69
|
+
answers cannot resurrect a notice. Direct ball/treat/tug still wins. Animation
|
|
70
|
+
ticks never await HTTP. Network timeout is 3 seconds; a host-tick guard discards
|
|
71
|
+
hung advice within 3.5 seconds of active time, without delaying the already-issued
|
|
72
|
+
rule alert. `brain-check`
|
|
73
|
+
allows 8 seconds for diagnosis, not for normal pet reactions.
|
|
74
|
+
|
|
75
|
+
## Resource and request limits
|
|
76
|
+
|
|
77
|
+
- One inference at a time. No automatic network retries or alternate-model fallback.
|
|
78
|
+
- Fixed paid model with provider price ceilings of $0.30 per million input tokens
|
|
79
|
+
and $1.20 per million output tokens, with per-request fees disallowed. These are
|
|
80
|
+
unit-price limits, not an account spending cap. Thinking disabled,
|
|
81
|
+
temperature 0, maximum 96 output tokens; no tools or plugins.
|
|
82
|
+
- Up to 16 anonymous successful judgments cached for 60 seconds. Same state can
|
|
83
|
+
reuse an answer without another request; source identity/freshness stays local.
|
|
84
|
+
- Default 40 HTTP attempts per UTC day **per process lifetime**. Restarts reset
|
|
85
|
+
this guard, and other apps share account limits; it is not a global quota meter.
|
|
86
|
+
- A 429 backs off for 60 seconds. Other request failures back off for 10 seconds.
|
|
87
|
+
No new calls while the pet is hidden/paused or the source has no fresh events.
|
|
88
|
+
- Requests use bounded transient buffers; the API adds network/HTTP runtime costs.
|
|
89
|
+
No claim of zero RAM overhead or guaranteed endpoint latency is made.
|
|
90
|
+
|
|
91
|
+
The catalog price on 2026-10-24 was $0.15 input / $0.60 output per million tokens,
|
|
92
|
+
with scheduled peak pricing at $0.30 / $1.20. Provider prices vary; the request
|
|
93
|
+
ceilings fail closed if no compatible route remains. Check the
|
|
94
|
+
[model and provider prices](https://openrouter.ai/deepseek/deepseek-v4.1-flash-1017/providers)
|
|
95
|
+
before budgeting. Even an aborted/timed-out request can incur provider usage.
|
|
96
|
+
|
|
97
|
+
Reference contracts: [OpenRouter parameters](https://openrouter.ai/docs/api-reference/parameters),
|
|
98
|
+
[provider routing](https://openrouter.ai/docs/guides/routing/provider-selection),
|
|
99
|
+
[reasoning](https://openrouter.ai/docs/guides/best-practices/reasoning-tokens).
|
|
100
|
+
The state/closed-choice design follows the TypeSafe skill's guidance, but no
|
|
101
|
+
TypeSafe service or SDK is installed; the user-selected OpenRouter backend is retained.
|
|
102
|
+
|
|
103
|
+
## Verification — 2026-10-24
|
|
104
|
+
|
|
105
|
+
The user-selected paid DeepSeek endpoint returned a valid `notify` in 1,608 ms
|
|
106
|
+
through `brain-check`. A second real request through the owned Windows worker
|
|
107
|
+
returned `notify` in 1,113 ms, within the normal 3-second deadline. Both requests
|
|
108
|
+
used synthetic anonymous approval facts; no real approval was created or answered.
|
|
109
|
+
|
|
110
|
+
The native test used a disposable Room fixture and reached `model_notify`.
|
|
111
|
+
Ball interaction kept priority, the pet kept rendering, resolution cleared the
|
|
112
|
+
pending event, and worker/painter both exited. Node RSS was about 128 MiB in that
|
|
113
|
+
short run, excluding helper/CLI memory. This is not an incremental or steady-state
|
|
114
|
+
RAM measurement. Existing live companion state was not changed.
|
|
115
|
+
|
|
116
|
+
For the 0.0.14 baseline, all 217 applicable tests passed (two macOS-only checks skipped), including mocked
|
|
117
|
+
JSON validation, price ceilings, timeout, cache, budget, cancellation and stale
|
|
118
|
+
results. The earlier free Gemma attempt returned 429 and never produced a verified
|
|
119
|
+
live decision; it is no longer the selected backend. Privacy restrictions remain.
|
|
120
|
+
|
|
121
|
+
The 0.0.15 rule-first candidate passed 223 applicable tests (two macOS-only checks
|
|
122
|
+
skipped), fresh installed-consumer checks and an owned Windows fixture run with
|
|
123
|
+
actual native target-label renaming/clearing. Hung/disagreeing advice is tested
|
|
124
|
+
with mock judges: those tests do not make paid requests. The model request body
|
|
125
|
+
and privacy/price restrictions are unchanged.
|
package/README.ko.md
CHANGED
|
@@ -106,6 +106,24 @@ 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
|
+
0.0.15는 LLM을 기다리지 않고 알리고, Windows에 Seat 이름과 대기 종류를 표시해요.
|
|
113
|
+
강아지도 기존 그림으로 가까이 와서 알림 방향을 봐요. 이름은 외부로 보내지 않아요.
|
|
114
|
+
화면 캡처나 자동 승인 없이 현재 Room의 메타데이터만 읽어요.
|
|
115
|
+
업데이트 후, 화면이 보이는 PC의 실행 중인 StackBite Seat에서 실행하세요.
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
retrieviewer desktop --watch stackbite --brain openrouter
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
[유료 DeepSeek 연결](BRAIN.md)에는 같은 터미널의 `OPENROUTER_API_KEY`가 필요해요.
|
|
122
|
+
`--brain openrouter`를 빼면 API 없이 규칙 알림만 써요. 이미 실행 중이면 먼저
|
|
123
|
+
`retrieviewer desktop-stop`으로 종료하세요. 설치된 `retrieviewer` 명령은 어느
|
|
124
|
+
폴더에서든 쓸 수 있어요. `node retrieviewer-cli.mjs`는 소스 폴더 안에서만 쓰는
|
|
125
|
+
개발용 명령이에요.
|
|
126
|
+
|
|
109
127
|
## 직접 만든 TUI에 붙이기
|
|
110
128
|
|
|
111
129
|
JS 플러그인과 JSONL 브리지를 제공해요. 화면·입력·시간 제어는 호스트가 소유합니다.
|
package/README.md
CHANGED
|
@@ -130,6 +130,28 @@ 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
|
+
Version 0.0.15 alerts without waiting for the model, shows the Seat name and
|
|
141
|
+
pending kind locally on Windows, and lets the puppy approach before looking
|
|
142
|
+
toward the sidebar. It reuses existing artwork; names never enter model input.
|
|
143
|
+
|
|
144
|
+
After updating, run inside a live StackBite Seat on the display computer:
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
retrieviewer desktop --watch stackbite --brain openrouter
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The paid backend requires `OPENROUTER_API_KEY` in that terminal's environment.
|
|
151
|
+
Omit `--brain openrouter` for rule-only alerts. Quit an existing companion first
|
|
152
|
+
with `retrieviewer desktop-stop`. These installed commands work from any folder;
|
|
153
|
+
`node retrieviewer-cli.mjs` is for developers running in the source checkout.
|
|
154
|
+
|
|
133
155
|
## Build, contribute, be kind
|
|
134
156
|
|
|
135
157
|
```sh
|
package/RELEASING.md
CHANGED
|
@@ -1,4 +1,42 @@
|
|
|
1
|
-
# Retrieviewer 0.0.
|
|
1
|
+
# Retrieviewer 0.0.15 release process
|
|
2
|
+
|
|
3
|
+
Current target: `@bitefoundry/retrieviewer@0.0.15`, public `latest`.
|
|
4
|
+
On 2026-10-24 the owner authorized npm publication of the reviewed rule-first
|
|
5
|
+
attention update. The optional model now runs after a confirmed rule cue is
|
|
6
|
+
accepted; no model response can delay, retract or repeat that cue. Windows
|
|
7
|
+
displays a bounded local Seat name and approval/input kind with a hover tooltip.
|
|
8
|
+
The pet reuses existing front contact, travel and whole-body look material;
|
|
9
|
+
resolved approaches brake naturally and direct play keeps priority.
|
|
10
|
+
|
|
11
|
+
Keep all nine art archives, runtime dependencies, provider/privacy restrictions,
|
|
12
|
+
price ceilings and request bounds unchanged. Build/install a fresh exact tarball,
|
|
13
|
+
compare every shipped byte, run the full suite and owned native checks, then
|
|
14
|
+
publish that same artifact. Verify registry integrity and a fresh public-registry
|
|
15
|
+
consumer. No StackBite changes, real approval actions, screen capture, host patch,
|
|
16
|
+
live pet restart or website deployment are authorized by this release request.
|
|
17
|
+
macOS/Linux live notification behavior remains unverified.
|
|
18
|
+
|
|
19
|
+
## Previous 0.0.14 scope (historical)
|
|
20
|
+
|
|
21
|
+
Current target: `@bitefoundry/retrieviewer@0.0.14`, public `latest`.
|
|
22
|
+
On 2026-10-24 the owner authorized npm publication of the metadata-only StackBite
|
|
23
|
+
watcher and optional paid DeepSeek V4.1 Flash 1017 backend. Existing artwork and
|
|
24
|
+
physics are unchanged. Watching and remote inference both remain opt-in.
|
|
25
|
+
|
|
26
|
+
The installed `retrieviewer` bin works from any directory; relative source
|
|
27
|
+
commands require the checkout root. Include this unrelated-working-directory
|
|
28
|
+
regression in the fresh installed-consumer check. Keep model identity, provider
|
|
29
|
+
price ceilings, privacy restrictions, daily per-process attempt guard, deadlines,
|
|
30
|
+
cache bounds, cancellation and rule fallback. No API keys ship or are saved.
|
|
31
|
+
|
|
32
|
+
Build/install a fresh exact artifact, compare every shipped byte, run tests and
|
|
33
|
+
an owned native worker check, then publish the same tarball. Verify registry
|
|
34
|
+
integrity and a fresh registry consumer. Earlier versions stay immutable; do not
|
|
35
|
+
restart the user's pet, patch the host, or certify untested macOS/Linux behavior.
|
|
36
|
+
No new runtime dependency or local model is added. Website deployment is outside
|
|
37
|
+
this release request.
|
|
38
|
+
|
|
39
|
+
## Previous 0.0.13 scope (historical)
|
|
2
40
|
|
|
3
41
|
Current target: `@bitefoundry/retrieviewer@0.0.13`, public `latest`.
|
|
4
42
|
The owner authorized npm publication and website deployment on2026-10-24.
|
|
@@ -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',label:seat.role_title});
|
|
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',label:seat.role_title});
|
|
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,12 +110,12 @@ 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();readonly ToolTip attentionTip=new ToolTip();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")));
|
|
117
117
|
bar.Controls.Add(MakeButton("Tug",144,()=>Interact("tug")));bar.Controls.Add(MakeButton("Window",212,ChooseTarget));bar.Controls.Add(MakeButton("Pause",280,TogglePause));bar.Controls.Add(MakeButton("Quit",348,Quit));
|
|
118
|
-
status.AutoSize=false;status.Location=new Point(422,12);status.Size=new Size(112,22);status.Text="Loading...";bar.Controls.Add(status);
|
|
118
|
+
status.AutoSize=false;status.AutoEllipsis=true;status.UseMnemonic=false;status.Location=new Point(422,12);status.Size=new Size(112,22);status.Text="Loading...";bar.Controls.Add(status);
|
|
119
119
|
ContextMenuStrip menu=new ContextMenuStrip();menu.Items.Add("Choose window",null,(s,e)=>ChooseTarget());menu.Items.Add("Hide / resume",null,(s,e)=>TogglePause());menu.Items.Add("Quit",null,(s,e)=>Quit());
|
|
120
120
|
tray.Icon=SystemIcons.Application;tray.Text="Retrieviewer · Choose window";tray.ContextMenuStrip=menu;tray.Visible=true;tray.DoubleClick+=(s,e)=>ChooseTarget();
|
|
121
121
|
// Handles are created on the UI thread before the reader can signal EOF.
|
|
@@ -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
|
+
attentionText=Program.AttentionText(state,count,m.ContainsKey("label")?Program.Str(m,"label"):"",m.ContainsKey("kind")?Program.Str(m,"kind"):"");status.Text=attentionText;attentionTip.SetToolTip(status,attentionText);
|
|
158
|
+
Program.Emit(new {type="attention-label",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.
|
|
@@ -210,7 +216,7 @@ sealed class Companion : ApplicationContext {
|
|
|
210
216
|
}
|
|
211
217
|
}
|
|
212
218
|
void Quit(){if(quitting)return;quitting=true;clock.Stop();if(pickerWindow!=null)pickerWindow.Close();pet.Close();bar.Close();tray.Visible=false;tray.Dispose();Program.Emit(new {type="quit"});ExitThread();}
|
|
213
|
-
protected override void Dispose(bool disposing){if(disposing){clock.Dispose();pet.Dispose();bar.Dispose();tray.Dispose();if(testTarget!=null)testTarget.Dispose();if(mutex!=null){mutex.ReleaseMutex();mutex.Dispose();mutex=null;}}base.Dispose(disposing);}
|
|
219
|
+
protected override void Dispose(bool disposing){if(disposing){clock.Dispose();attentionTip.Dispose();pet.Dispose();bar.Dispose();tray.Dispose();if(testTarget!=null)testTarget.Dispose();if(mutex!=null){mutex.ReleaseMutex();mutex.Dispose();mutex=null;}}base.Dispose(disposing);}
|
|
214
220
|
}
|
|
215
221
|
|
|
216
222
|
static class Program {
|
|
@@ -220,6 +226,17 @@ static class Program {
|
|
|
220
226
|
internal static int Int(Dictionary<string,object> m,string key){return Convert.ToInt32(m[key]);}
|
|
221
227
|
internal static string Str(Dictionary<string,object> m,string key){return Convert.ToString(m[key]);}
|
|
222
228
|
internal static bool Bool(Dictionary<string,object> m,string key){return Convert.ToBoolean(m[key]);}
|
|
229
|
+
internal static string AttentionText(string state,int count,string label,string kind){
|
|
230
|
+
if(count<0||count>32||label.Length>64)throw new Exception("Invalid attention metadata");
|
|
231
|
+
foreach(char c in label)if(char.IsControl(c)||(c>=0x200b&&c<=0x200f)||(c>=0x202a&&c<=0x202e)||(c>=0x2060&&c<=0x206f))throw new Exception("Invalid attention label");
|
|
232
|
+
if(state=="connected"){
|
|
233
|
+
if(count==0)return "Watching";
|
|
234
|
+
if(label.Length==0)return count+" need input";
|
|
235
|
+
if(kind!="approval"&&kind!="input")throw new Exception("Invalid attention kind");
|
|
236
|
+
return label+" - "+(kind=="approval"?"Approval pending":"Input pending")+(count>1?" (+"+(count-1)+")":"");
|
|
237
|
+
}
|
|
238
|
+
return state=="paused"?"Watch paused":state=="connecting"?"Connecting...":"Watch offline";
|
|
239
|
+
}
|
|
223
240
|
[STAThread] static int Main(string[] args){
|
|
224
241
|
try{
|
|
225
242
|
// GUI-subsystem processes have redirected handles but no console code
|
|
@@ -263,8 +280,9 @@ static class Program {
|
|
|
263
280
|
Native.ShowWindow(target.Handle,4);Application.DoEvents();bool restored=!Native.IsIconic(target.Handle);
|
|
264
281
|
bool focus=Native.GetForegroundWindow()==foreground;
|
|
265
282
|
bool gridBounds=GridCell(3840,2160)==8&&GridCell(7680,4320)==12&&GridCell(32000,18000)==0&&GridCell(int.MaxValue,int.MaxValue)==0&&GridCell(0,0)==0;
|
|
266
|
-
bool
|
|
267
|
-
|
|
283
|
+
bool labels=AttentionText("connected",2,"Worker & Review","approval")=="Worker & Review - Approval pending (+1)"&&AttentionText("connected",0,"","")=="Watching";
|
|
284
|
+
bool passed=clickThrough&&noActivate&&layered&&follows&&minimized&&hidden&&restored&&focus&&pet.PaintedDots>0&&gridBounds&&labels;
|
|
285
|
+
Emit(new {type="self-test",passed=passed,attentionLabels=labels,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;
|
|
268
286
|
}
|
|
269
287
|
}
|
|
270
288
|
}
|
|
@@ -93,15 +93,24 @@ 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:
|
|
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});
|
|
105
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);};
|
|
106
115
|
// Disconnect on failure aborts startup. Only an acknowledged detach may live on.
|
|
107
116
|
const timer=setTimeout(()=>finish(Error('Desktop startup timed out; no agent settings changed')),20000);
|
|
@@ -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
|
};
|
|
@@ -10,12 +10,15 @@ import {controlAddress,parseHelperSpec} from '../desktop/platform.mjs';
|
|
|
10
10
|
|
|
11
11
|
let helper,pump,timer,server,session,paths,owned=false,stopping=false,frames=0,paintedFrames=0,visible=false,selected=false,pickerVisible=false,pendingViewport=null,lastInteraction=null;
|
|
12
12
|
let nativeError=null,ready=false,detached=false;
|
|
13
|
+
let attention=null,brain=null,attentionLabel=null;
|
|
13
14
|
const returnStyle=process.env.RETRIEVIEWER_RETURN_STYLE??'auto';
|
|
15
|
+
const watchSource=process.env.RETRIEVIEWER_WATCH??'off';
|
|
16
|
+
const brainSource=process.env.RETRIEVIEWER_BRAIN??'off';
|
|
14
17
|
let nativeStderr='';
|
|
15
18
|
const sendParent=message=>{if(process.connected)try{process.send(message,error=>{if(error&&!detached)stop();});}catch{if(!detached)stop();}};
|
|
16
19
|
const starting=()=>{if(stopping||(!process.connected&&!detached))throw Error('Desktop launcher closed during startup');};
|
|
17
20
|
async function stop(){
|
|
18
|
-
const first=!stopping;stopping=true;clearInterval(timer);pump?.dispose();
|
|
21
|
+
const first=!stopping;stopping=true;clearInterval(timer);attention?.dispose();brain?.dispose();pump?.dispose();
|
|
19
22
|
if(helper&&!helper.killed){try{helper.stdin.end(JSON.stringify({type:'quit'})+'\n');}catch{}}
|
|
20
23
|
if(server?.listening)server.close();
|
|
21
24
|
if(owned)try{const current=JSON.parse(await readFile(paths.session,'utf8'));if(current.token===session.token)await unlink(paths.session);}catch{}
|
|
@@ -41,7 +44,7 @@ try{
|
|
|
41
44
|
body+=bytes.toString('utf8');if(body.length>2048)return socket.destroy();if(!body.includes('\n'))return;
|
|
42
45
|
let command;try{command=JSON.parse(body.split('\n')[0]);}catch{return socket.destroy();}
|
|
43
46
|
if(command.token!==session.token||!['status','stop'].includes(command.command))return socket.destroy();
|
|
44
|
-
socket.end(JSON.stringify(command.command==='stop'?{stopped:true}:{running:true,reachable:true,selected,visible,pickerVisible,frames,paintedFrames,lastInteraction,returnStyle,pid:process.pid,painterPid:helper?.pid,rssBytes:process.memoryUsage().rss})+'\n');
|
|
47
|
+
socket.end(JSON.stringify(command.command==='stop'?{stopped:true}:{running:true,reachable:true,selected,visible,pickerVisible,frames,paintedFrames,lastInteraction,returnStyle,attentionLabel,watch:{source:watchSource,...attention?.status??{state:'off',pendingCount:0}},brain:brain?.status??{provider:'off',state:'off'},pid:process.pid,painterPid:helper?.pid,rssBytes:process.memoryUsage().rss})+'\n');
|
|
45
48
|
if(command.command==='stop')stop();
|
|
46
49
|
});
|
|
47
50
|
});
|
|
@@ -49,7 +52,8 @@ try{
|
|
|
49
52
|
if(process.platform!=='win32')await chmod(session.pipe,0o600);
|
|
50
53
|
starting();
|
|
51
54
|
// GUI-subsystem exe: no console to hide. SW_HIDE here also hides its first UI.
|
|
52
|
-
|
|
55
|
+
const helperEnv={...process.env};delete helperEnv.OPENROUTER_API_KEY;
|
|
56
|
+
helper=spawn(spec.command,spec.args,{windowsHide:false,stdio:['pipe','pipe','pipe'],env:helperEnv});
|
|
53
57
|
helper.stdin.on('error',()=>stop());helper.stderr.setEncoding('utf8');helper.stderr.on('data',s=>{nativeStderr=(nativeStderr+s).slice(-4096);});helper.once('error',error=>{sendParent({error:error.message});stop();});
|
|
54
58
|
helper.once('exit',code=>{if(!stopping)sendParent({error:nativeError??('Desktop painter exited ('+code+')'+(nativeStderr?' — '+nativeStderr.trim():''))});stop();});
|
|
55
59
|
const write=message=>{if(!stopping&&!helper.stdin.destroyed)helper.stdin.write(JSON.stringify(message)+'\n');};
|
|
@@ -62,6 +66,7 @@ try{
|
|
|
62
66
|
else if(m.type==='picker')pickerVisible=m.visible===true;
|
|
63
67
|
else if(m.type==='viewport'){pendingViewport=m;visible=m.visible;selected=m.selected===true;pump?.viewport(m);}
|
|
64
68
|
else if(m.type==='ack'){frames++;if(m.painted)paintedFrames++;pump?.ack();}
|
|
69
|
+
else if(m.type==='attention-label'){if(typeof m.text!=='string'||m.text.length>160)throw Error('Invalid attention label');attentionLabel=m.text;}
|
|
65
70
|
else if(m.type==='pause')pump?.pause(m.paused);
|
|
66
71
|
else if(m.type==='interact'){const accepted=pump?.interact(m.kind)??false;lastInteraction={kind:m.kind,accepted};write({type:'interaction',kind:m.kind,accepted});}
|
|
67
72
|
else if(m.type==='quit')stop();
|
|
@@ -70,6 +75,18 @@ try{
|
|
|
70
75
|
starting();const {createPet}=await import('../../retrieviewer.mjs');starting();const pet=await createPet({returnStyle});
|
|
71
76
|
if(stopping)pet.dispose();else{
|
|
72
77
|
pump=createFramePump({pet,send:write});if(pendingViewport)pump.viewport(pendingViewport);
|
|
73
|
-
|
|
78
|
+
if(!['off','stackbite'].includes(watchSource))throw Error('Invalid desktop watch source');
|
|
79
|
+
if(!['off','openrouter'].includes(brainSource)||brainSource==='openrouter'&&watchSource!=='stackbite')throw Error('Invalid desktop brain/watch combination');
|
|
80
|
+
if(brainSource==='openrouter'){const {createOpenRouterBrain}=await import('../openrouter/brain.mjs');starting();brain=createOpenRouterBrain();}
|
|
81
|
+
if(watchSource==='stackbite'){
|
|
82
|
+
const [{createAttentionMonitor},{createStackBiteSource}]=await Promise.all([import('../../retrieviewer-attention.mjs'),import('../stackbite/attention.mjs')]);starting();
|
|
83
|
+
const source=createStackBiteSource();
|
|
84
|
+
attention=createAttentionMonitor({readSnapshot:source.readSnapshot,...brain?{judgeNotice:input=>brain.decide(input)}:{},onNotice:notice=>pump.notify(notice),onClear:()=>pump.clearNotice(),onChange:status=>{
|
|
85
|
+
// Older POSIX helpers deliberately reject unknown message types. They can
|
|
86
|
+
// run the visual cue, but only the Windows bar implements this status label.
|
|
87
|
+
if(spec.mode==='windows-overlay')write({type:'attention',state:status.state,count:status.pendingCount,label:status.target?.label??'',kind:status.target?.kind??''});
|
|
88
|
+
}});
|
|
89
|
+
}
|
|
90
|
+
write({type:'loaded',ink:process.env.RETRIEVIEWER_INK??'cream'});timer=setInterval(()=>{try{attention?.tick({active:!pump.hidden});pump.tick();}catch(error){write({type:'error',message:error.message});stop();}},1000/spec.fps);
|
|
74
91
|
}
|
|
75
92
|
}catch(error){sendParent({error:error.message});await stop();}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitefoundry/retrieviewer",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"description": "A fluffy Braille desktop pet for Windows and Linux X11, with experimental macOS support and an embeddable TUI engine.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://bitefoundry.github.io/retrieviewer/",
|
|
@@ -47,6 +47,14 @@
|
|
|
47
47
|
"types": "./retrieviewer-plugin.d.mts",
|
|
48
48
|
"default": "./retrieviewer-plugin.mjs"
|
|
49
49
|
},
|
|
50
|
+
"./attention": {
|
|
51
|
+
"types": "./retrieviewer-attention.d.mts",
|
|
52
|
+
"default": "./retrieviewer-attention.mjs"
|
|
53
|
+
},
|
|
54
|
+
"./brain": {
|
|
55
|
+
"types": "./integrations/openrouter/brain.d.mts",
|
|
56
|
+
"default": "./integrations/openrouter/brain.mjs"
|
|
57
|
+
},
|
|
50
58
|
"./tui": "./integrations/opencode/tui.tsx",
|
|
51
59
|
"./opencode/tui": "./integrations/opencode/tui.tsx",
|
|
52
60
|
"./package.json": "./package.json"
|
|
@@ -62,6 +70,8 @@
|
|
|
62
70
|
"RELEASING.md",
|
|
63
71
|
"retrieviewer.mjs",
|
|
64
72
|
"retrieviewer.d.mts",
|
|
73
|
+
"retrieviewer-attention.mjs",
|
|
74
|
+
"retrieviewer-attention.d.mts",
|
|
65
75
|
"retrieviewer-stdio.mjs",
|
|
66
76
|
"retrieviewer-plugin.mjs",
|
|
67
77
|
"retrieviewer-plugin.d.mts",
|
|
@@ -75,6 +85,11 @@
|
|
|
75
85
|
"integrations/windows-overlay/worker.mjs",
|
|
76
86
|
"integrations/windows-overlay/Overlay.cs",
|
|
77
87
|
"integrations/desktop/platform.mjs",
|
|
88
|
+
"integrations/stackbite/attention.mjs",
|
|
89
|
+
"integrations/openrouter/brain.mjs",
|
|
90
|
+
"integrations/openrouter/brain.d.mts",
|
|
91
|
+
"BRAIN.md",
|
|
92
|
+
"ATTENTION.md",
|
|
78
93
|
"integrations/linux-overlay/overlay.py",
|
|
79
94
|
"integrations/linux-overlay/README.md",
|
|
80
95
|
"integrations/macos-overlay/overlay.jxa",
|
package/puppy-companion-life.mjs
CHANGED
|
@@ -26,7 +26,7 @@ const LABEL={
|
|
|
26
26
|
belly:'배를 보이고 발라당 쉬는 중',sleep:'새근새근 잠자는 중',tug:'장난감을 물고 당기는 중',
|
|
27
27
|
butterflyNotice:'나비를 발견하고 귀를 쫑긋하는 중',butterfly:'나비를 폴짝폴짝 쫓는 중',butterflyBye:'날아가는 나비를 바라보는 중',
|
|
28
28
|
fetch:'공을 쫓아가는 중',miss:'굴러가는 공을 한 번 놓친 중',reChase:'공을 다시 쫓아가는 중',tumble:'우당탕 균형을 되찾는 중',pickup:'공을 무는 중',carry:'공을 물고 돌아오는 중',drop:'공을 내려놓는 중',offer:'공을 내려놓고 기다리는 중',
|
|
29
|
-
treatSit:'간식 앞에 앉는 중',chew:'간식 먹는 중'
|
|
29
|
+
treatSit:'간식 앞에 앉는 중',chew:'간식 먹는 중',notifySit:'확인할 일을 발견하고 앉는 중',notifyContact:'확인할 일이 있어 사용자를 바라보는 중',notifyCome:'확인할 일을 알려주러 가까이 오는 중',notify:'확인할 일이 있는 쪽을 바라보는 중'
|
|
30
30
|
};
|
|
31
31
|
const PENDING_LABEL={treat:'곧 간식',tug:'곧 터그놀이'};
|
|
32
32
|
// Baked interaction clips, played only while holding at the shared seated endpoint.
|
|
@@ -62,7 +62,7 @@ export function createCompanionLife(actor,{seed=731,energy=.8,returnStyle='auto'
|
|
|
62
62
|
let clock=0,decisions=0,lastEpisode=null,quiet=0;
|
|
63
63
|
let queue=[],hold=0,holdIntent=null,waiting=null,gesture=null;
|
|
64
64
|
let intent='start',label=LABEL.start,pending=null,active=null,ball=null,treat=null;
|
|
65
|
-
let fetchPlan=null;
|
|
65
|
+
let fetchPlan=null,notice=null;
|
|
66
66
|
|
|
67
67
|
const cmd=(name,args,step,when)=>({cmd:name,args,intent:step,label:LABEL[step],when});
|
|
68
68
|
const wait=(seconds,step)=>({hold:seconds,intent:step,label:LABEL[step]});
|
|
@@ -236,6 +236,14 @@ export function createCompanionLife(actor,{seed=731,energy=.8,returnStyle='auto'
|
|
|
236
236
|
holdIntent=null;gesture=null;
|
|
237
237
|
if(!safe)return;
|
|
238
238
|
if(pending&&!active)startInteraction();
|
|
239
|
+
if(notice?.phase==='queued'&&!active&&!pending){
|
|
240
|
+
notice.phase='showing';
|
|
241
|
+
const far=()=>{const s=actor.sample(clock);return Math.hypot(s.x-HOME.x,s.z-HOME.z)>.8;};
|
|
242
|
+
queue=[sit('notifySit'),wait(.45,'notifyContact'),
|
|
243
|
+
fx(()=>{if(far())notice.phase='approaching';}),cmd('go',[HOME.x,HOME.z],'notifyCome',far),
|
|
244
|
+
sit('notifySit'),fx(()=>{notice.phase='showing';}),wait(.6,'notifyContact'),
|
|
245
|
+
cmd('look',[notice.side],'notify'),wait(2.5,'notify'),fx(()=>{notice=null;})];
|
|
246
|
+
}
|
|
239
247
|
// Run instant effects; stop at the first hold or issued command (at most one command per update).
|
|
240
248
|
for(;;){
|
|
241
249
|
if(!queue.length){
|
|
@@ -257,6 +265,7 @@ export function createCompanionLife(actor,{seed=731,energy=.8,returnStyle='auto'
|
|
|
257
265
|
if(!['ball','treat','tug'].includes(kind))return false;
|
|
258
266
|
if(kind==='tug'&&typeof actor.tug!=='function')return false;
|
|
259
267
|
if(pending||active||cooldowns[kind]>0)return false;
|
|
268
|
+
clearNotice();
|
|
260
269
|
quiet=0;
|
|
261
270
|
if(butterfly||lastEpisode==='butterfly'){
|
|
262
271
|
butterfly=null;queue=[];hold=0;holdIntent=null;gesture=null;waiting=null;
|
|
@@ -304,6 +313,24 @@ export function createCompanionLife(actor,{seed=731,energy=.8,returnStyle='auto'
|
|
|
304
313
|
return true;
|
|
305
314
|
}
|
|
306
315
|
|
|
316
|
+
function clearNotice(){
|
|
317
|
+
if(!notice)return;
|
|
318
|
+
const approaching=notice.phase==='approaching';
|
|
319
|
+
notice=null;queue=[];hold=0;holdIntent=null;gesture=null;waiting=null;
|
|
320
|
+
intent='recover';label=LABEL.recover;
|
|
321
|
+
if(approaching)actor.attend?.();
|
|
322
|
+
// Let any connected head turn/sit finish naturally. No snap or new route.
|
|
323
|
+
}
|
|
324
|
+
function notify({kind,side='left'}={}){
|
|
325
|
+
if(!['approval','input'].includes(kind)||!['left','right'].includes(side))throw Error('Invalid pet notice');
|
|
326
|
+
// Explicit play/feeding always wins. Notifications never queue a second pet.
|
|
327
|
+
if(active||pending||notice)return false;
|
|
328
|
+
notice={kind,side,phase:'queued'};
|
|
329
|
+
butterfly=null;queue=[];hold=0;holdIntent=null;gesture=null;waiting=null;
|
|
330
|
+
actor.attend?.();
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
|
|
307
334
|
// Recomputed from the live pose so the marker stays truthful before and after sitting.
|
|
308
335
|
function treatReport(){
|
|
309
336
|
if(!treat)return null;
|
|
@@ -322,13 +349,14 @@ export function createCompanionLife(actor,{seed=731,energy=.8,returnStyle='auto'
|
|
|
322
349
|
}
|
|
323
350
|
|
|
324
351
|
return {
|
|
325
|
-
update,interact,
|
|
352
|
+
update,interact,notify,clearNotice,
|
|
326
353
|
setReturnStyle(style){validateReturnStyle(style);returnStyle=style;return style;},
|
|
327
354
|
get report(){
|
|
328
355
|
const interaction=active??pending;
|
|
329
356
|
return {
|
|
330
357
|
intent:actor.state==='scramble'&&fetchPlan?'tumble':intent,label:pending?`${label} · ${PENDING_LABEL[pending]}`:actor.state==='scramble'&&fetchPlan?LABEL.tumble:label,episode:lastEpisode,interaction,busy:interaction!==null,
|
|
331
358
|
ball:ballReport(),fetchPattern:fetchPlan?.pattern??null,returnStyle,returnMode:fetchPlan?.returnMode??null,treat:treatReport(),butterfly:butterflyPoint(),tug:actor.state==='tug'?{progress:actor.sample(clock).comfortPose?.progress??0,grip:1}:null,
|
|
359
|
+
attention:notice?{...notice}:null,
|
|
332
360
|
snore:actor.state==='sleep'?((clock%3.6)/3.6):null,energy:needs.energy,drowsiness:needs.drowsiness,decisions,cooldowns:{...cooldowns},
|
|
333
361
|
// Local hold clock only: progress freezes between updates; null outside the stationary hold.
|
|
334
362
|
gesture:gesture&&SAFE.has(actor.state)?{clip:gesture.clip,progress:clamp(gesture.elapsed/gesture.duration,0,1)}:null
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface AttentionSnapshot {
|
|
2
|
+
source: string;
|
|
3
|
+
scope: string;
|
|
4
|
+
/** label is a bounded local display name, never remote model input. */
|
|
5
|
+
items: {id: string; generation: string; turnId: number | null; requestId?: string; kind: 'approval' | 'input'; label?: string}[];
|
|
6
|
+
}
|
|
7
|
+
export interface AttentionStatus {
|
|
8
|
+
state: 'connecting' | 'connected' | 'unavailable' | 'paused' | 'stale' | 'off';
|
|
9
|
+
pendingCount: number;
|
|
10
|
+
target: {seatId: string; label: string; kind: 'approval' | 'input'} | null;
|
|
11
|
+
retainedNotices: number;
|
|
12
|
+
inFlight: boolean;
|
|
13
|
+
judging?: boolean;
|
|
14
|
+
lastJudgment?: 'rule_first' | 'model_notify' | 'rule_override' | 'rule_fallback' | null;
|
|
15
|
+
}
|
|
16
|
+
export function validateAttentionSnapshot(value: unknown): AttentionSnapshot;
|
|
17
|
+
/** Host owns tick/lifetime. Import and construction start no timers or I/O. */
|
|
18
|
+
export function createAttentionMonitor(options: {
|
|
19
|
+
readSnapshot: (options: {signal: AbortSignal}) => Promise<AttentionSnapshot>;
|
|
20
|
+
onNotice: (notice: {kind: 'approval' | 'input'; side: 'left'}) => boolean;
|
|
21
|
+
onClear?: () => void;
|
|
22
|
+
onChange?: (status: AttentionStatus) => void;
|
|
23
|
+
/** Anonymous metadata only. Runs after the immediate rule alert, never delays or suppresses it. */
|
|
24
|
+
judgeNotice?: (input: {kind: 'approval' | 'input'; pendingCount: number; signal: AbortSignal}) => Promise<{decision: 'notify' | 'ignore' | 'uncertain'}>;
|
|
25
|
+
judgeTimeoutMs?: number;
|
|
26
|
+
now?: () => number;
|
|
27
|
+
intervalMs?: number;
|
|
28
|
+
staleMs?: number;
|
|
29
|
+
noticeCooldownMs?: number;
|
|
30
|
+
}): {
|
|
31
|
+
tick(options?: {active?: boolean}): void;
|
|
32
|
+
readonly status: AttentionStatus;
|
|
33
|
+
settled(): Promise<void>;
|
|
34
|
+
dispose(): void;
|
|
35
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Opt-in attention coordinator. No I/O, timers, models or pet assets on import.
|
|
2
|
+
// Screen/app text is data, never an instruction or an executable action.
|
|
3
|
+
const MAX_ITEMS=32,MAX_SEEN=128;
|
|
4
|
+
const bounded=(value,max=128)=>typeof value==='string'&&value.length>0&&value.length<=max&&!/[\x00-\x1f\x7f]/.test(value);
|
|
5
|
+
// Cosmetic, local-only metadata. Never let a malformed title hide a real alert.
|
|
6
|
+
const localLabel=value=>typeof value==='string'?value.slice(0,256).replace(/[\x00-\x1f\x7f-\x9f\u200b-\u200f\u202a-\u202e\u2060-\u206f]/g,'').replace(/\s+/g,' ').trim().slice(0,64).replace(/[\ud800-\udbff]$/,'')||undefined:undefined;
|
|
7
|
+
export function validateAttentionSnapshot(value){
|
|
8
|
+
if(!value||!bounded(value.source,32)||!bounded(value.scope,256)||!Array.isArray(value.items)||value.items.length>MAX_ITEMS)throw Error('Invalid attention snapshot');
|
|
9
|
+
const identities=new Set();
|
|
10
|
+
const items=value.items.map(item=>{
|
|
11
|
+
if(!item||!bounded(item.id)||!bounded(item.generation)||!(item.turnId===null||Number.isSafeInteger(item.turnId)&&item.turnId>=1)||item.requestId!==undefined&&!bounded(item.requestId)||item.turnId===null&&item.requestId===undefined||!['approval','input'].includes(item.kind))throw Error('Invalid attention item');
|
|
12
|
+
const key=JSON.stringify([item.id,item.generation,item.turnId,item.requestId??null]);if(identities.has(key))throw Error('Duplicate attention item');identities.add(key);
|
|
13
|
+
// No arbitrary source fields, output text, prompts, paths or tokens. A Seat's
|
|
14
|
+
// bounded display label is for local status only, never the remote judge.
|
|
15
|
+
const label=localLabel(item.label);
|
|
16
|
+
return {id:item.id,generation:item.generation,turnId:item.turnId,...item.requestId===undefined?{}:{requestId:item.requestId},kind:item.kind,...label?{label}:{}};
|
|
17
|
+
});
|
|
18
|
+
return {source:value.source,scope:value.scope,items};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createAttentionMonitor({readSnapshot,onNotice,onClear=()=>{},onChange=()=>{},judgeNotice,judgeTimeoutMs=3500,now=()=>performance.now(),intervalMs=5000,staleMs=15000,noticeCooldownMs=20000}={}){
|
|
22
|
+
if(typeof readSnapshot!=='function'||typeof onNotice!=='function'||typeof onClear!=='function'||typeof onChange!=='function'||typeof now!=='function')throw TypeError('Attention callbacks must be functions');
|
|
23
|
+
if(judgeNotice!==undefined&&typeof judgeNotice!=='function')throw TypeError('judgeNotice must be a function');
|
|
24
|
+
if(!Number.isFinite(judgeTimeoutMs)||judgeTimeoutMs<100||judgeTimeoutMs>15000)throw RangeError('Invalid judge timeout');
|
|
25
|
+
if(!Number.isFinite(intervalMs)||intervalMs<1000||intervalMs>60000||!Number.isFinite(staleMs)||staleMs<intervalMs*2||staleMs>300000||!Number.isFinite(noticeCooldownMs)||noticeCooldownMs<1000||noticeCooldownMs>300000)throw RangeError('Invalid attention timing');
|
|
26
|
+
let disposed=false,paused=false,flight=null,nextRead=0,nextNotice=0,lastSuccess=null,snapshot=null,state='connecting',lastReport='',cueKey=null,judgment=null,lastJudgment=null,target=null,targetKey='';
|
|
27
|
+
const seen=new Map();
|
|
28
|
+
const identity=(value,item)=>JSON.stringify([value.source,value.scope,item.id,item.generation,item.turnId,item.requestId??null]);
|
|
29
|
+
const refreshTarget=()=>{
|
|
30
|
+
const item=snapshot?.items.find(row=>identity(snapshot,row)===cueKey)??snapshot?.items.find(row=>row.kind==='approval')??snapshot?.items[0];
|
|
31
|
+
target=item?{seatId:item.id,label:item.label??(item.id.length<=32?item.id:'Seat '+item.id.slice(-8)),kind:item.kind}:null;
|
|
32
|
+
targetKey=JSON.stringify(target);
|
|
33
|
+
};
|
|
34
|
+
const clearCue=()=>{if(cueKey!==null){cueKey=null;refreshTarget();onClear();}};
|
|
35
|
+
const cancelJudgment=()=>{const old=judgment;judgment=null;old?.controller.abort();};
|
|
36
|
+
const report=()=>({state,pendingCount:snapshot?.items.length??0,target:target?{...target}:null,retainedNotices:seen.size,inFlight:flight!==null,...judgeNotice?{judging:!!judgment&&!judgment.ready,lastJudgment}:{}});
|
|
37
|
+
const changed=()=>{const key=state+'|'+(snapshot?.items.length??0)+'|'+targetKey+'|'+!!judgment+'|'+lastJudgment;if(key!==lastReport){lastReport=key;onChange(report());}};
|
|
38
|
+
const clear=next=>{snapshot=null;refreshTarget();lastSuccess=null;state=next;cancelJudgment();clearCue();changed();};
|
|
39
|
+
function startRead(t){
|
|
40
|
+
const operation={controller:new AbortController()};flight=operation;nextRead=t+intervalMs;
|
|
41
|
+
operation.done=(async()=>{
|
|
42
|
+
try{
|
|
43
|
+
const result=validateAttentionSnapshot(await readSnapshot({signal:operation.controller.signal}));
|
|
44
|
+
if(disposed||paused||flight!==operation)return;
|
|
45
|
+
// A scope change cannot inherit either pending facts or dedupe identities.
|
|
46
|
+
if(snapshot&&(snapshot.scope!==result.scope||snapshot.source!==result.source))seen.clear();
|
|
47
|
+
snapshot=result;lastSuccess=now();state='connected';
|
|
48
|
+
if(cueKey!==null&&!snapshot.items.some(item=>identity(snapshot,item)===cueKey))clearCue();
|
|
49
|
+
if(judgment&&!snapshot.items.some(item=>identity(snapshot,item)===judgment.key))cancelJudgment();
|
|
50
|
+
refreshTarget();
|
|
51
|
+
}catch{
|
|
52
|
+
if(disposed||paused||flight!==operation)return;
|
|
53
|
+
// No source stderr, credentials, or stale pending facts escape on failure.
|
|
54
|
+
clear('unavailable');
|
|
55
|
+
}finally{
|
|
56
|
+
if(flight===operation){flight=null;nextRead=now()+intervalMs;if(!disposed&&!paused)changed();}
|
|
57
|
+
}
|
|
58
|
+
})();
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
tick({active=true}={}){
|
|
62
|
+
if(disposed)return;
|
|
63
|
+
const t=now();if(!Number.isFinite(t))throw RangeError('Invalid attention clock');
|
|
64
|
+
if(!active){
|
|
65
|
+
if(!paused){paused=true;const old=flight;flight=null;old?.controller.abort();clear('paused');}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if(paused){paused=false;nextRead=t;state='connecting';changed();}
|
|
69
|
+
if(snapshot&&t-lastSuccess>=staleMs)clear('stale');
|
|
70
|
+
if(!flight&&t>=nextRead)startRead(t);
|
|
71
|
+
// Advice settles independently of alert delivery. It cannot delay, retract,
|
|
72
|
+
// repeat, or move an already-confirmed human-input notification.
|
|
73
|
+
if(judgment){
|
|
74
|
+
if(!judgment.ready&&t-judgment.start>=judgeTimeoutMs){judgment.ready=true;judgment.decision='uncertain';judgment.controller.abort();}
|
|
75
|
+
if(judgment.ready){lastJudgment=judgment.decision==='notify'?'model_notify':judgment.decision==='ignore'?'rule_override':'rule_fallback';judgment=null;}
|
|
76
|
+
}
|
|
77
|
+
// A refresh already in flight may resolve/replace the candidate. Do not emit
|
|
78
|
+
// an old cue in the microtask gap just before that fresh snapshot arrives.
|
|
79
|
+
if(!flight&&snapshot&&state==='connected'&&t<lastSuccess+staleMs&&t>=nextNotice){
|
|
80
|
+
// Approval and generic input for the same exact turn are one notification.
|
|
81
|
+
let item=null;
|
|
82
|
+
for(const row of snapshot.items)if(!seen.has(identity(snapshot,row))){if(!item)item=row;if(row.kind==='approval'){item=row;break;}}
|
|
83
|
+
if(item&&onNotice({kind:item.kind,side:'left'})===true){
|
|
84
|
+
const key=identity(snapshot,item);seen.set(key,true);cueKey=key;
|
|
85
|
+
refreshTarget();
|
|
86
|
+
if(judgeNotice){
|
|
87
|
+
cancelJudgment();lastJudgment='rule_first';
|
|
88
|
+
const operation={key,controller:new AbortController(),ready:false,start:t};judgment=operation;
|
|
89
|
+
const facts={kind:item.kind,pendingCount:snapshot.items.length,signal:operation.controller.signal};
|
|
90
|
+
operation.done=Promise.resolve().then(()=>{if(operation.controller.signal.aborted)throw Error('Canceled judgment');return judgeNotice(facts);}).then(result=>{
|
|
91
|
+
if(judgment===operation&&!disposed&&!paused&&!operation.controller.signal.aborted){operation.decision=['notify','ignore','uncertain'].includes(result?.decision)?result.decision:'uncertain';operation.ready=true;}
|
|
92
|
+
},()=>{if(judgment===operation&&!disposed&&!paused){operation.decision='uncertain';operation.ready=true;}});
|
|
93
|
+
}
|
|
94
|
+
// Still-pending identities cannot be evicted into repeated notifications.
|
|
95
|
+
const live=new Set(snapshot.items.map(row=>identity(snapshot,row)));
|
|
96
|
+
for(const prior of seen.keys()){if(seen.size<=MAX_SEEN)break;if(!live.has(prior))seen.delete(prior);}
|
|
97
|
+
nextNotice=t+noticeCooldownMs;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
changed();
|
|
101
|
+
},
|
|
102
|
+
get status(){return report();},
|
|
103
|
+
// For host shutdown/verification, not a second polling loop.
|
|
104
|
+
async settled(){await flight?.done;await judgment?.done;},
|
|
105
|
+
dispose(){if(disposed)return;disposed=true;const old=flight;flight=null;old?.controller.abort();cancelJudgment();snapshot=null;refreshTarget();clearCue();seen.clear();state='off';},
|
|
106
|
+
};
|
|
107
|
+
}
|
package/retrieviewer-cli.mjs
CHANGED
|
@@ -13,7 +13,9 @@ Usage:
|
|
|
13
13
|
retrieviewer doctor [--agent ID] [--json]
|
|
14
14
|
retrieviewer agents [--json]
|
|
15
15
|
retrieviewer preview [--original]
|
|
16
|
-
retrieviewer desktop [--ink cream|gold|dark] [--return-style auto|brake|trot|run-trot]
|
|
16
|
+
retrieviewer desktop [--ink cream|gold|dark] [--return-style auto|brake|trot|run-trot] [--watch stackbite|off] [--brain openrouter|off]
|
|
17
|
+
retrieviewer brain-check [--json]
|
|
18
|
+
retrieviewer attention [--json]
|
|
17
19
|
retrieviewer desktop-status [--json]
|
|
18
20
|
retrieviewer desktop-stop
|
|
19
21
|
retrieviewer desktop-clean --yes
|
|
@@ -28,12 +30,19 @@ use Codex normally. Exit from its tray icon or desktop-stop. No auto-start is ad
|
|
|
28
30
|
Windows is verified; macOS and Linux X11 adapters are experimental. Native Wayland
|
|
29
31
|
app tracking is not supported. Linux targets AlmaLinux8/GLIBC2.28, system Python3.6+.
|
|
30
32
|
Return style defaults to auto (mix all three). Restart the companion to change it.
|
|
33
|
+
Watch is OFF by default. Inside a live StackBite Seat, --watch stackbite reads only
|
|
34
|
+
that Room's pending-input metadata. No screen capture, clicks or approvals.
|
|
35
|
+
Hidden/paused companions stop polling. attention performs one read-only check.
|
|
36
|
+
Brain is OFF by default. --brain openrouter requires --watch stackbite and
|
|
37
|
+
OPENROUTER_API_KEY; sends only anonymous event kinds/counts to paid DeepSeek V4.1
|
|
38
|
+
Flash 1017 with token price ceilings. Failures fall back to rule alerts.
|
|
39
|
+
brain-check sends one synthetic event and incurs API usage charges.
|
|
31
40
|
`;
|
|
32
41
|
export function parseArguments(args){
|
|
33
42
|
const [command='help',...rest]=args,options={};
|
|
34
43
|
for(let i=0;i<rest.length;i++){
|
|
35
44
|
const token=rest[i];
|
|
36
|
-
if(token==='--agent'||token==='--ink'||token==='--return-style'){
|
|
45
|
+
if(token==='--agent'||token==='--ink'||token==='--return-style'||token==='--watch'||token==='--brain'){
|
|
37
46
|
const key=token.slice(2);
|
|
38
47
|
if(options[key]||!rest[i+1]||rest[i+1].startsWith('-'))throw Error(token+' requires one value');
|
|
39
48
|
options[key]=rest[++i];
|
|
@@ -43,19 +52,37 @@ export function parseArguments(args){
|
|
|
43
52
|
if(options.agent&&!AGENTS.some(a=>a.id===options.agent))throw Error('Unknown agent: '+options.agent);
|
|
44
53
|
if(options.ink&&!['cream','gold','dark'].includes(options.ink))throw Error('--ink must be cream, gold or dark');
|
|
45
54
|
if(options['return-style']&&!['auto','brake','trot','run-trot'].includes(options['return-style']))throw Error('--return-style must be auto, brake, trot or run-trot');
|
|
46
|
-
if(
|
|
47
|
-
|
|
55
|
+
if(options.watch&&!['off','stackbite'].includes(options.watch))throw Error('--watch must be stackbite or off');
|
|
56
|
+
if(options.brain&&!['off','openrouter'].includes(options.brain))throw Error('--brain must be openrouter or off');
|
|
57
|
+
if(options.brain==='openrouter'&&options.watch!=='stackbite')throw Error('--brain openrouter requires --watch stackbite');
|
|
58
|
+
if(!['help','--help','-h','enable','disable','disconnect','status','doctor','agents','preview','attention','brain-check','desktop','desktop-status','desktop-stop','desktop-clean','desktop-remove'].includes(command))throw Error('Unknown command: '+command);
|
|
59
|
+
const allowed={desktop:['ink','return-style','watch','brain'],attention:['json'],'brain-check':['json'],enable:['agent','yes','json','dry-run'],disconnect:['agent','yes','json','dry-run'],disable:['agent','json'],status:['agent','json'],doctor:['agent','json'],agents:['json'],preview:['original'],'desktop-status':['json'],'desktop-clean':['yes'],'desktop-remove':['yes']};
|
|
48
60
|
for(const key of Object.keys(options))if(!(allowed[command]??[]).includes(key))throw Error(`--${key} is not supported by ${command}`);
|
|
49
61
|
return {command,options};
|
|
50
62
|
}
|
|
51
63
|
export async function runCli(args,{input=process.stdin,output=process.stdout,error=process.stderr,installer=createInstaller(),desktop}={}){
|
|
52
64
|
const {command,options}=parseArguments(args),emit=value=>output.write(JSON.stringify(value,null,2)+'\n');
|
|
53
65
|
if(['help','--help','-h'].includes(command)){output.write(HELP);return 0;}
|
|
66
|
+
if(command==='brain-check'){
|
|
67
|
+
const {createOpenRouterBrain,BrainError}=await import('./integrations/openrouter/brain.mjs');
|
|
68
|
+
const brain=createOpenRouterBrain({timeoutMs:8000});
|
|
69
|
+
try{const result=await brain.decide({kind:'approval',pendingCount:1});emit({ok:true,synthetic:true,...result,brain:brain.status});return 0;}
|
|
70
|
+
catch(error){emit({ok:false,synthetic:true,error:error instanceof BrainError?error.code:'unavailable',brain:brain.status});return 2;}
|
|
71
|
+
finally{brain.dispose();}
|
|
72
|
+
}
|
|
73
|
+
if(command==='attention'){
|
|
74
|
+
const {createStackBiteSource}=await import('./integrations/stackbite/attention.mjs');
|
|
75
|
+
const snapshot=await createStackBiteSource().readSnapshot();
|
|
76
|
+
// Diagnostic metadata only; prompts/output/credentials were discarded upstream.
|
|
77
|
+
if(options.json)emit({...snapshot,pendingCount:snapshot.items.length});
|
|
78
|
+
else output.write(`StackBite current Room: ${snapshot.items.length} pending input item(s). No screen capture or actions.\n`);
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
54
81
|
if(command.startsWith('desktop')){
|
|
55
82
|
if(['desktop-clean','desktop-remove'].includes(command)&&!options.yes)throw Error('Use '+command+' --yes to remove only the companion cache.');
|
|
56
83
|
const api=desktop??await import('./integrations/windows-overlay/desktop.mjs');
|
|
57
84
|
const action={'desktop':'startDesktop','desktop-status':'desktopStatus','desktop-stop':'stopDesktop','desktop-clean':'cleanDesktop','desktop-remove':'removeDesktop'}[command];
|
|
58
|
-
emit(await api[action]({...options.ink?{ink:options.ink}:{},...options['return-style']?{returnStyle:options['return-style']}:{} }));return 0;
|
|
85
|
+
emit(await api[action]({...options.ink?{ink:options.ink}:{},...options['return-style']?{returnStyle:options['return-style']}:{},...options.watch?{watch:options.watch}:{},...options.brain?{brain:options.brain}:{} }));return 0;
|
|
59
86
|
}
|
|
60
87
|
if(command==='preview'){await import('./examples/live.mjs');return 0;}
|
|
61
88
|
if(['status','doctor','agents'].includes(command)){
|
package/retrieviewer.d.mts
CHANGED
|
@@ -40,6 +40,8 @@ export interface PetState {
|
|
|
40
40
|
[key: string]: unknown;
|
|
41
41
|
}
|
|
42
42
|
export interface LifeReport {
|
|
43
|
+
/** Metadata-triggered visual cue only. No host input or approval authority. */
|
|
44
|
+
attention: {kind: 'approval' | 'input'; side: 'left' | 'right'; phase: 'queued' | 'approaching' | 'showing'} | null;
|
|
43
45
|
/** Last selected autonomous episode; null before the first decision. */
|
|
44
46
|
episode: string | null;
|
|
45
47
|
intent: string;
|
|
@@ -93,6 +95,10 @@ export interface Pet {
|
|
|
93
95
|
needsFrame(options: FrameOptions): boolean;
|
|
94
96
|
/** False when busy or cooling down. */
|
|
95
97
|
interact(kind: 'ball' | 'treat' | 'tug'): boolean;
|
|
98
|
+
/** False during direct play/feeding or another cue. Does not operate the host. */
|
|
99
|
+
notify(notice: {kind: 'approval' | 'input'; side?: 'left' | 'right'}): boolean;
|
|
100
|
+
/** Cancel the cue, preserving any in-progress connected pose recovery. */
|
|
101
|
+
clearNotice(): void;
|
|
96
102
|
/** Sets the next throw's return style. Does not interrupt a fetch in progress. */
|
|
97
103
|
setReturnStyle(style: FetchReturnStyle): FetchReturnStyle;
|
|
98
104
|
readonly disposed: boolean;
|
package/retrieviewer.mjs
CHANGED
|
@@ -167,6 +167,9 @@ export async function createPet({loadAsset=defaultLoadAsset,seed,energy,pawTrans
|
|
|
167
167
|
},
|
|
168
168
|
// 'ball' | 'treat' | 'tug'. False when busy or cooling down; unknown kinds throw.
|
|
169
169
|
interact(kind){alive();if(!INTERACTIONS.includes(kind))throw TypeError(`Unknown interaction: ${kind}`);return life.interact(kind);},
|
|
170
|
+
// A bounded visual cue only; never clicks, types, focuses or approves anything.
|
|
171
|
+
notify(notice){alive();return life.notify(notice);},
|
|
172
|
+
clearNotice(){alive();life.clearNotice();},
|
|
170
173
|
// Applies to the next ball throw; never rewrites a fetch already in motion.
|
|
171
174
|
setReturnStyle(style){alive();return life.setReturnStyle(style);},
|
|
172
175
|
get disposed(){return disposed;},
|