@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
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface AttentionSnapshot {
|
|
2
|
+
source: string;
|
|
3
|
+
scope: string;
|
|
4
|
+
items: {id: string; generation: string; turnId: number | null; requestId?: string; kind: 'approval' | 'input'}[];
|
|
5
|
+
}
|
|
6
|
+
export interface AttentionStatus {
|
|
7
|
+
state: 'connecting' | 'connected' | 'unavailable' | 'paused' | 'stale' | 'off';
|
|
8
|
+
pendingCount: number;
|
|
9
|
+
retainedNotices: number;
|
|
10
|
+
inFlight: boolean;
|
|
11
|
+
judging?: boolean;
|
|
12
|
+
lastJudgment?: 'model_notify' | 'rule_override' | 'rule_fallback' | null;
|
|
13
|
+
}
|
|
14
|
+
export function validateAttentionSnapshot(value: unknown): AttentionSnapshot;
|
|
15
|
+
/** Host owns tick/lifetime. Import and construction start no timers or I/O. */
|
|
16
|
+
export function createAttentionMonitor(options: {
|
|
17
|
+
readSnapshot: (options: {signal: AbortSignal}) => Promise<AttentionSnapshot>;
|
|
18
|
+
onNotice: (notice: {kind: 'approval' | 'input'; side: 'left'}) => boolean;
|
|
19
|
+
onClear?: () => void;
|
|
20
|
+
onChange?: (status: AttentionStatus) => void;
|
|
21
|
+
/** Anonymous metadata only. Confirmed pending alerts cannot be suppressed. */
|
|
22
|
+
judgeNotice?: (input: {kind: 'approval' | 'input'; pendingCount: number; signal: AbortSignal}) => Promise<{decision: 'notify' | 'ignore' | 'uncertain'}>;
|
|
23
|
+
judgeTimeoutMs?: number;
|
|
24
|
+
now?: () => number;
|
|
25
|
+
intervalMs?: number;
|
|
26
|
+
staleMs?: number;
|
|
27
|
+
noticeCooldownMs?: number;
|
|
28
|
+
}): {
|
|
29
|
+
tick(options?: {active?: boolean}): void;
|
|
30
|
+
readonly status: AttentionStatus;
|
|
31
|
+
settled(): Promise<void>;
|
|
32
|
+
dispose(): void;
|
|
33
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
export function validateAttentionSnapshot(value){
|
|
6
|
+
if(!value||!bounded(value.source,32)||!bounded(value.scope,256)||!Array.isArray(value.items)||value.items.length>MAX_ITEMS)throw Error('Invalid attention snapshot');
|
|
7
|
+
const identities=new Set();
|
|
8
|
+
const items=value.items.map(item=>{
|
|
9
|
+
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');
|
|
10
|
+
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);
|
|
11
|
+
// Do not retain arbitrary source fields, output text, prompts, paths or tokens.
|
|
12
|
+
return {id:item.id,generation:item.generation,turnId:item.turnId,...item.requestId===undefined?{}:{requestId:item.requestId},kind:item.kind};
|
|
13
|
+
});
|
|
14
|
+
return {source:value.source,scope:value.scope,items};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createAttentionMonitor({readSnapshot,onNotice,onClear=()=>{},onChange=()=>{},judgeNotice,judgeTimeoutMs=3500,now=()=>performance.now(),intervalMs=5000,staleMs=15000,noticeCooldownMs=20000}={}){
|
|
18
|
+
if(typeof readSnapshot!=='function'||typeof onNotice!=='function'||typeof onClear!=='function'||typeof onChange!=='function'||typeof now!=='function')throw TypeError('Attention callbacks must be functions');
|
|
19
|
+
if(judgeNotice!==undefined&&typeof judgeNotice!=='function')throw TypeError('judgeNotice must be a function');
|
|
20
|
+
if(!Number.isFinite(judgeTimeoutMs)||judgeTimeoutMs<100||judgeTimeoutMs>15000)throw RangeError('Invalid judge timeout');
|
|
21
|
+
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');
|
|
22
|
+
let disposed=false,paused=false,flight=null,nextRead=0,nextNotice=0,lastSuccess=null,snapshot=null,state='connecting',lastReport='',cueKey=null,judgment=null,lastJudgment=null;
|
|
23
|
+
const seen=new Map();
|
|
24
|
+
const identity=(value,item)=>JSON.stringify([value.source,value.scope,item.id,item.generation,item.turnId,item.requestId??null]);
|
|
25
|
+
const clearCue=()=>{if(cueKey!==null){cueKey=null;onClear();}};
|
|
26
|
+
const cancelJudgment=()=>{const old=judgment;judgment=null;old?.controller.abort();};
|
|
27
|
+
const report=()=>({state,pendingCount:snapshot?.items.length??0,retainedNotices:seen.size,inFlight:flight!==null,...judgeNotice?{judging:!!judgment&&!judgment.ready,lastJudgment}:{}});
|
|
28
|
+
const changed=()=>{const key=state+'|'+(snapshot?.items.length??0);if(key!==lastReport){lastReport=key;onChange(report());}};
|
|
29
|
+
const clear=next=>{snapshot=null;lastSuccess=null;state=next;cancelJudgment();clearCue();changed();};
|
|
30
|
+
function startRead(t){
|
|
31
|
+
const operation={controller:new AbortController()};flight=operation;nextRead=t+intervalMs;
|
|
32
|
+
operation.done=(async()=>{
|
|
33
|
+
try{
|
|
34
|
+
const result=validateAttentionSnapshot(await readSnapshot({signal:operation.controller.signal}));
|
|
35
|
+
if(disposed||paused||flight!==operation)return;
|
|
36
|
+
// A scope change cannot inherit either pending facts or dedupe identities.
|
|
37
|
+
if(snapshot&&(snapshot.scope!==result.scope||snapshot.source!==result.source))seen.clear();
|
|
38
|
+
snapshot=result;lastSuccess=now();state='connected';
|
|
39
|
+
if(cueKey!==null&&!snapshot.items.some(item=>identity(snapshot,item)===cueKey))clearCue();
|
|
40
|
+
if(judgment&&!snapshot.items.some(item=>identity(snapshot,item)===judgment.key))cancelJudgment();
|
|
41
|
+
}catch{
|
|
42
|
+
if(disposed||paused||flight!==operation)return;
|
|
43
|
+
// No source stderr, credentials, or stale pending facts escape on failure.
|
|
44
|
+
clear('unavailable');
|
|
45
|
+
}finally{
|
|
46
|
+
if(flight===operation){flight=null;nextRead=now()+intervalMs;if(!disposed&&!paused)changed();}
|
|
47
|
+
}
|
|
48
|
+
})();
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
tick({active=true}={}){
|
|
52
|
+
if(disposed)return;
|
|
53
|
+
const t=now();if(!Number.isFinite(t))throw RangeError('Invalid attention clock');
|
|
54
|
+
if(!active){
|
|
55
|
+
if(!paused){paused=true;const old=flight;flight=null;old?.controller.abort();clear('paused');}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if(paused){paused=false;nextRead=t;state='connecting';changed();}
|
|
59
|
+
if(snapshot&&t-lastSuccess>=staleMs)clear('stale');
|
|
60
|
+
if(!flight&&t>=nextRead)startRead(t);
|
|
61
|
+
// A refresh already in flight may resolve/replace the candidate. Do not emit
|
|
62
|
+
// an old cue in the microtask gap just before that fresh snapshot arrives.
|
|
63
|
+
if(!flight&&snapshot&&state==='connected'&&t<lastSuccess+staleMs&&t>=nextNotice){
|
|
64
|
+
// Approval and generic input for the same exact turn are one notification.
|
|
65
|
+
let item=null;
|
|
66
|
+
for(const row of snapshot.items)if(!seen.has(identity(snapshot,row))){if(!item)item=row;if(row.kind==='approval'){item=row;break;}}
|
|
67
|
+
if(item&&judgeNotice){
|
|
68
|
+
const key=identity(snapshot,item);
|
|
69
|
+
if(judgment?.key!==key){
|
|
70
|
+
cancelJudgment();const operation={key,controller:new AbortController(),ready:false,start:t};judgment=operation;
|
|
71
|
+
const facts={kind:item.kind,pendingCount:snapshot.items.length,signal:operation.controller.signal};
|
|
72
|
+
// Only anonymous enums/counts leave the coordinator; identities stay local.
|
|
73
|
+
operation.done=Promise.resolve().then(()=>{if(operation.controller.signal.aborted)throw Error('Canceled judgment');return judgeNotice(facts);}).then(result=>{
|
|
74
|
+
if(judgment===operation&&!disposed&&!paused&&!operation.controller.signal.aborted){operation.decision=['notify','ignore','uncertain'].includes(result?.decision)?result.decision:'uncertain';operation.ready=true;}
|
|
75
|
+
},()=>{if(judgment===operation&&!disposed&&!paused){operation.decision='uncertain';operation.ready=true;}});
|
|
76
|
+
}
|
|
77
|
+
if(!judgment.ready&&t-judgment.start>=judgeTimeoutMs){judgment.ready=true;judgment.decision='uncertain';judgment.controller.abort();}
|
|
78
|
+
if(!judgment.ready){changed();return;}
|
|
79
|
+
// These are confirmed pending human-input facts, not speculative screen
|
|
80
|
+
// guesses. A model may advise, but may never hide them or authorize input.
|
|
81
|
+
}
|
|
82
|
+
if(item&&onNotice({kind:item.kind,side:'left'})===true){
|
|
83
|
+
if(judgment)lastJudgment=judgment.decision==='notify'?'model_notify':judgment.decision==='ignore'?'rule_override':'rule_fallback';
|
|
84
|
+
const key=identity(snapshot,item);seen.set(key,true);cueKey=key;
|
|
85
|
+
// Still-pending identities cannot be evicted into repeated notifications.
|
|
86
|
+
const live=new Set(snapshot.items.map(row=>identity(snapshot,row)));
|
|
87
|
+
for(const prior of seen.keys()){if(seen.size<=MAX_SEEN)break;if(!live.has(prior))seen.delete(prior);}
|
|
88
|
+
nextNotice=t+noticeCooldownMs;
|
|
89
|
+
cancelJudgment();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
changed();
|
|
93
|
+
},
|
|
94
|
+
get status(){return report();},
|
|
95
|
+
// For host shutdown/verification, not a second polling loop.
|
|
96
|
+
async settled(){await flight?.done;await judgment?.done;},
|
|
97
|
+
dispose(){if(disposed)return;disposed=true;const old=flight;flight=null;old?.controller.abort();cancelJudgment();clearCue();snapshot=null;seen.clear();state='off';},
|
|
98
|
+
};
|
|
99
|
+
}
|
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-config.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Tiny runtime settings API. No art, processes, timers or I/O on import.
|
|
2
|
-
import {homedir} from 'node:os';
|
|
2
|
+
import {homedir,hostname} from 'node:os';
|
|
3
3
|
import {join,resolve} from 'node:path';
|
|
4
4
|
import {readFile,mkdir,lstat,open,rename,unlink} from 'node:fs/promises';
|
|
5
5
|
import {randomUUID} from 'node:crypto';
|
|
@@ -30,8 +30,19 @@ export async function writeAtomicJson(path,value){
|
|
|
30
30
|
export async function withSettingsLock(fn,options={}){
|
|
31
31
|
const paths=getConfigPaths(options);await noLink(paths.root);await mkdir(paths.root,{recursive:true,mode:0o700});
|
|
32
32
|
await noLink(paths.lock);let handle;
|
|
33
|
-
|
|
34
|
-
try{
|
|
33
|
+
const owner={owner:'retrieviewer-settings-lock-v1',pid:process.pid,host:hostname(),startedAt:new Date().toISOString(),token:randomUUID()};
|
|
34
|
+
try{handle=await open(paths.lock,'wx',0o600);}catch(e){
|
|
35
|
+
if(e.code!=='EEXIST')throw e;
|
|
36
|
+
let pid;try{await noLink(paths.lock);const info=JSON.parse(await readFile(paths.lock,'utf8'));if(info.owner===owner.owner&&Number.isSafeInteger(info.pid)&&info.pid>0)pid=info.pid;}catch{}
|
|
37
|
+
throw Error(`Retrieviewer settings are locked${pid?' (owner PID '+pid+')':''}: ${paths.lock}. Retry when it finishes. If the owner crashed, stop ALL Retrieviewer settings writers first, then follow INSTALLING.md#recovering-an-interrupted-settings-change. No lock was removed automatically.`);
|
|
38
|
+
}
|
|
39
|
+
try{await handle.writeFile(JSON.stringify(owner)+'\n');await handle.sync();return await fn(paths);}
|
|
40
|
+
finally{
|
|
41
|
+
try{await handle.close();}finally{
|
|
42
|
+
// Do not remove a replacement lock if someone moved ours during the operation.
|
|
43
|
+
try{await noLink(paths.lock);const current=JSON.parse(await readFile(paths.lock,'utf8'));if(current.token===owner.token)await unlink(paths.lock);}catch(e){if(e.code!=='ENOENT')throw e;}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
35
46
|
}
|
|
36
47
|
export async function setAgentEnabled(agent,enabled,options={}){
|
|
37
48
|
agentId(agent);if(typeof enabled!=='boolean')throw TypeError('enabled must be boolean');
|
|
@@ -21,7 +21,7 @@ export interface PetPluginScheduler {
|
|
|
21
21
|
export interface PetPluginOptions {
|
|
22
22
|
host: PetPluginHost;
|
|
23
23
|
petOptions?: PetOptions;
|
|
24
|
-
/** Scheduled frame rate, default 30. */
|
|
24
|
+
/** Scheduled frame rate, default 30; 1000/2147483647 through 120 inclusive. */
|
|
25
25
|
fps?: number;
|
|
26
26
|
/** Default: performance.now() with setTimeout/clearTimeout, used only after start(). */
|
|
27
27
|
scheduler?: PetPluginScheduler;
|
package/retrieviewer-plugin.mjs
CHANGED
|
@@ -11,7 +11,7 @@ export async function createPetPlugin({host,petOptions,fps=30,scheduler}={}){
|
|
|
11
11
|
fn(host.getSize,'host.getSize');fn(host.render,'host.render');
|
|
12
12
|
if(host.clear!==undefined)fn(host.clear,'host.clear');
|
|
13
13
|
if(host.onError!==undefined)fn(host.onError,'host.onError');
|
|
14
|
-
if(typeof fps!=='number'||!Number.isFinite(fps)||fps<=0||fps>120)throw RangeError('fps must be within
|
|
14
|
+
if(typeof fps!=='number'||!Number.isFinite(fps)||fps<=0||fps>120||1000/fps>2147483647)throw RangeError('fps must be within [1000/2147483647, 120] (bounded timer delay)');
|
|
15
15
|
if(scheduler!==undefined){
|
|
16
16
|
if(!scheduler||typeof scheduler!=='object')throw TypeError('scheduler must be an object');
|
|
17
17
|
fn(scheduler.now,'scheduler.now');fn(scheduler.schedule,'scheduler.schedule');fn(scheduler.cancel,'scheduler.cancel');
|
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' | '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;},
|