@better_openclaw/betterclaw 1.4.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -21
- package/banner.png +0 -0
- package/openclaw.plugin.json +33 -12
- package/package.json +1 -1
- package/skills/betterclaw/SKILL.md +11 -10
- package/src/cli.ts +29 -0
- package/src/context.ts +56 -8
- package/src/events.ts +7 -2
- package/src/filter.ts +5 -4
- package/src/index.ts +252 -99
- package/src/learner.ts +205 -0
- package/src/patterns.ts +29 -12
- package/src/pipeline.ts +116 -44
- package/src/reactions.ts +57 -0
- package/src/tools/get-context.ts +28 -5
- package/src/triage.ts +134 -0
- package/src/triggers.ts +7 -0
- package/src/types.ts +38 -3
- package/src/judgment.ts +0 -145
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="
|
|
2
|
+
<img src="banner.png" alt="BetterClaw" width="100%" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
|
|
18
18
|
## What is this?
|
|
19
19
|
|
|
20
|
-
This is the server-side plugin for [BetterClaw](https://
|
|
20
|
+
This is the server-side plugin for [BetterClaw](https://betterclaw.app), an iOS app that connects your iPhone's sensors to your [OpenClaw](https://openclaw.dev) AI agent. The app streams device events (location, battery, health, geofences) to your gateway — this plugin decides what to do with them.
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
The plugin is the **sole event gateway** for all tiers. Smart mode controls filtering depth: OFF = passive context store, ON = full pipeline with rules, LLM triage, and proactive insights.
|
|
23
23
|
|
|
24
24
|
```
|
|
25
25
|
BetterClaw iOS App This Plugin (on gateway) Agent
|
|
@@ -28,7 +28,7 @@ Without this plugin, raw events go straight to your agent. With it, they're filt
|
|
|
28
28
|
battery ──────▶ ┌───────────────────┼───────────────────┐
|
|
29
29
|
location ─────▶ │ Rules Engine │ Context Store │
|
|
30
30
|
health ───────▶ │ LLM Triage │ Pattern Engine │ ──▶ filtered events
|
|
31
|
-
geofence ─────▶ │
|
|
31
|
+
geofence ─────▶ │ Daily Learner │ Proactive Triggers│ + full context
|
|
32
32
|
└───────────────────┼───────────────────┘
|
|
33
33
|
│
|
|
34
34
|
proactive insights
|
|
@@ -38,12 +38,15 @@ Without this plugin, raw events go straight to your agent. With it, they're filt
|
|
|
38
38
|
|
|
39
39
|
## Features
|
|
40
40
|
|
|
41
|
-
- **Smart
|
|
42
|
-
- **LLM Triage** —
|
|
43
|
-
- **
|
|
44
|
-
- **
|
|
41
|
+
- **Tier-Aware Smart Mode** — Smart mode ON = full pipeline (rules → triage → push). Smart mode OFF = passive store (context updated, no filtering or pushing). Synced via periodic heartbeat from iOS.
|
|
42
|
+
- **Two-Layer LLM Triage** — Daily learner builds a personalized triage profile from OpenClaw memory summaries + event reactions. Per-event cheap LLM call with structured output for ambiguous events.
|
|
43
|
+
- **Smart Filtering** — Per-source dedup, cooldown windows, and a configurable daily push budget prevent event spam
|
|
44
|
+
- **Device Context** — Rolling state snapshot with per-field timestamps: battery, GPS, zone occupancy, health metrics, activity classification
|
|
45
|
+
- **Pattern Recognition** — Daily analysis computes location routines, health trends (7d/30d baselines), and event frequency stats
|
|
45
46
|
- **Proactive Insights** — Combined-signal triggers: low battery away from home, unusual inactivity, sleep deficit, routine deviations, weekly digest
|
|
46
|
-
- **
|
|
47
|
+
- **Per-Device Config** — iOS app can override push budget and proactive settings at runtime via RPC
|
|
48
|
+
- **Agent Tool** — `get_context` tool lets your agent read the full device snapshot, tier, smart mode status, and triage profile on demand
|
|
49
|
+
- **CLI Setup** — `openclaw betterclaw setup` configures gateway allowedCommands automatically
|
|
47
50
|
|
|
48
51
|
## Requirements
|
|
49
52
|
|
|
@@ -54,6 +57,7 @@ Without this plugin, raw events go straight to your agent. With it, they're filt
|
|
|
54
57
|
|
|
55
58
|
```bash
|
|
56
59
|
openclaw plugins install @betterclaw-app/betterclaw
|
|
60
|
+
openclaw betterclaw setup # configures gateway allowedCommands
|
|
57
61
|
```
|
|
58
62
|
|
|
59
63
|
## Configure
|
|
@@ -67,10 +71,11 @@ Add to your `openclaw.json`:
|
|
|
67
71
|
"betterclaw": {
|
|
68
72
|
"enabled": true,
|
|
69
73
|
"config": {
|
|
70
|
-
"
|
|
74
|
+
"triageModel": "openai/gpt-4o-mini",
|
|
71
75
|
"pushBudgetPerDay": 10,
|
|
72
76
|
"patternWindowDays": 14,
|
|
73
|
-
"proactiveEnabled": true
|
|
77
|
+
"proactiveEnabled": true,
|
|
78
|
+
"analysisHour": 5
|
|
74
79
|
}
|
|
75
80
|
}
|
|
76
81
|
}
|
|
@@ -84,29 +89,41 @@ All config keys are optional — defaults are shown above.
|
|
|
84
89
|
|
|
85
90
|
| Key | Default | Description |
|
|
86
91
|
|-----|---------|-------------|
|
|
87
|
-
| `
|
|
92
|
+
| `triageModel` | `openai/gpt-4o-mini` | Model for per-event triage (supports `provider/model` format) |
|
|
93
|
+
| `triageApiBase` | — | Optional base URL for OpenAI-compatible endpoint (e.g., Ollama) |
|
|
88
94
|
| `pushBudgetPerDay` | `10` | Max events forwarded to the agent per day |
|
|
89
95
|
| `patternWindowDays` | `14` | Days of event history used for pattern computation |
|
|
90
96
|
| `proactiveEnabled` | `true` | Enable proactive combined-signal insights |
|
|
97
|
+
| `analysisHour` | `5` | Hour (0-23, system timezone) for daily pattern + learner analysis |
|
|
98
|
+
|
|
99
|
+
> **Migration:** `llmModel` still works as a deprecated alias for `triageModel`.
|
|
91
100
|
|
|
92
101
|
## How It Works
|
|
93
102
|
|
|
94
103
|
### Event Pipeline
|
|
95
104
|
|
|
96
|
-
Every device event from the BetterClaw app goes through
|
|
105
|
+
Every device event from the BetterClaw app goes through the plugin:
|
|
97
106
|
|
|
98
|
-
1. **
|
|
99
|
-
2. **
|
|
100
|
-
3. **
|
|
101
|
-
4. **
|
|
107
|
+
1. **Context Update** — Device context store is always updated with the latest sensor data.
|
|
108
|
+
2. **Smart Mode Check** — If smart mode is OFF, the event is stored and processing stops. If ON, continues.
|
|
109
|
+
3. **Rules Engine** — Checks dedup, cooldown timers, and daily budget. Critical events (geofence, low battery) always push. Obvious spam is dropped.
|
|
110
|
+
4. **LLM Triage** — Ambiguous events get a cheap LLM call with the personalized triage profile for a push/drop decision.
|
|
102
111
|
5. **Agent Injection** — Events that pass are injected into the agent's main session with formatted context.
|
|
103
112
|
|
|
104
113
|
### Background Services
|
|
105
114
|
|
|
106
|
-
|
|
115
|
+
- **Pattern Engine + Daily Learner** (daily at `analysisHour`) — Computes location routines, health trends, event stats. Then runs a subagent turn to build a personalized triage profile from OpenClaw memory summaries and notification reaction data.
|
|
116
|
+
- **Proactive Engine** (hourly) — Evaluates combined-signal conditions and fires insights when thresholds are met.
|
|
117
|
+
|
|
118
|
+
### Gateway RPCs
|
|
107
119
|
|
|
108
|
-
|
|
109
|
-
|
|
120
|
+
| RPC | Direction | Purpose |
|
|
121
|
+
|-----|-----------|---------|
|
|
122
|
+
| `betterclaw.event` | iOS → plugin | Send a device event for processing |
|
|
123
|
+
| `betterclaw.ping` | iOS → plugin | Heartbeat: sync tier + smartMode, get budget info |
|
|
124
|
+
| `betterclaw.config` | iOS → plugin | Per-device settings override |
|
|
125
|
+
| `betterclaw.context` | iOS → plugin | Full context for iOS Context tab |
|
|
126
|
+
| `betterclaw.snapshot` | iOS → plugin | Bulk device state catch-up |
|
|
110
127
|
|
|
111
128
|
## Commands
|
|
112
129
|
|
|
@@ -118,7 +135,8 @@ Two engines run on a schedule in the background:
|
|
|
118
135
|
|
|
119
136
|
| Plugin | BetterClaw iOS | OpenClaw |
|
|
120
137
|
|--------|----------------|----------|
|
|
121
|
-
|
|
|
138
|
+
| 2.x | 2.x+ | 2025.12+ |
|
|
139
|
+
| 1.x | 1.x | 2025.12+ |
|
|
122
140
|
|
|
123
141
|
## License
|
|
124
142
|
|
package/banner.png
ADDED
|
Binary file
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,34 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "betterclaw",
|
|
3
|
-
"name": "BetterClaw
|
|
4
|
-
"description": "Intelligent context
|
|
5
|
-
"version": "
|
|
3
|
+
"name": "BetterClaw",
|
|
4
|
+
"description": "Intelligent device context, event filtering, and proactive insights for BetterClaw iOS",
|
|
5
|
+
"version": "2.0.0",
|
|
6
|
+
"main": "dist/index.js",
|
|
6
7
|
"skills": ["./skills/betterclaw"],
|
|
7
8
|
"configSchema": {
|
|
8
9
|
"type": "object",
|
|
9
|
-
"additionalProperties": false,
|
|
10
10
|
"properties": {
|
|
11
|
+
"triageModel": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"default": "openai/gpt-4o-mini",
|
|
14
|
+
"description": "Model for per-event triage (accepts llmModel as deprecated alias)"
|
|
15
|
+
},
|
|
11
16
|
"llmModel": {
|
|
12
17
|
"type": "string",
|
|
13
|
-
"
|
|
18
|
+
"description": "Deprecated — use triageModel instead"
|
|
19
|
+
},
|
|
20
|
+
"triageApiBase": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "Optional base URL for OpenAI-compatible triage endpoint (e.g. http://localhost:11434/v1 for Ollama)"
|
|
14
23
|
},
|
|
15
24
|
"pushBudgetPerDay": {
|
|
16
25
|
"type": "number",
|
|
17
|
-
"default": 10
|
|
26
|
+
"default": 10,
|
|
27
|
+
"description": "Max events forwarded to agent per day"
|
|
18
28
|
},
|
|
19
29
|
"patternWindowDays": {
|
|
20
30
|
"type": "number",
|
|
21
|
-
"default": 14
|
|
31
|
+
"default": 14,
|
|
32
|
+
"description": "Days of event history for pattern computation"
|
|
22
33
|
},
|
|
23
34
|
"proactiveEnabled": {
|
|
24
35
|
"type": "boolean",
|
|
25
|
-
"default": true
|
|
36
|
+
"default": true,
|
|
37
|
+
"description": "Enable proactive combined-signal insights"
|
|
38
|
+
},
|
|
39
|
+
"analysisHour": {
|
|
40
|
+
"type": "number",
|
|
41
|
+
"default": 5,
|
|
42
|
+
"minimum": 0,
|
|
43
|
+
"maximum": 23,
|
|
44
|
+
"description": "Hour (system timezone) for daily analysis run"
|
|
26
45
|
}
|
|
27
|
-
}
|
|
46
|
+
},
|
|
47
|
+
"additionalProperties": false
|
|
28
48
|
},
|
|
29
49
|
"uiHints": {
|
|
30
|
-
"
|
|
31
|
-
"pushBudgetPerDay": { "label": "
|
|
32
|
-
"proactiveEnabled": { "label": "
|
|
50
|
+
"triageModel": { "label": "Triage Model", "placeholder": "openai/gpt-4o-mini" },
|
|
51
|
+
"pushBudgetPerDay": { "label": "Push Budget / Day", "type": "number" },
|
|
52
|
+
"proactiveEnabled": { "label": "Proactive Insights", "type": "boolean" },
|
|
53
|
+
"analysisHour": { "label": "Analysis Hour (0-23)", "type": "number" }
|
|
33
54
|
}
|
|
34
55
|
}
|
package/package.json
CHANGED
|
@@ -9,23 +9,24 @@ You have access to the user's physical device state via BetterClaw (iOS companio
|
|
|
9
9
|
|
|
10
10
|
## Capabilities
|
|
11
11
|
|
|
12
|
-
- **
|
|
12
|
+
- **Sensors**: battery level/state, GPS location, health metrics (steps, heart rate, HRV, sleep, distance, energy)
|
|
13
13
|
- **Geofence events**: enter/exit named zones (Home, Office, etc.)
|
|
14
14
|
- **Patterns**: location routines, health trends (7d/30d), battery drain rate
|
|
15
15
|
- **Tool**: `get_context` — call anytime to read the full current device snapshot and derived patterns
|
|
16
16
|
|
|
17
|
-
##
|
|
17
|
+
## Adaptive Behavior
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Check `get_context().smartMode` and `get_context().tier` to understand your data flow:
|
|
20
20
|
|
|
21
|
-
- **Device events
|
|
22
|
-
- **
|
|
21
|
+
- **Smart mode ON**: Device events are being pushed to you automatically, pre-filtered for relevance. If you receive one, it's worth acknowledging.
|
|
22
|
+
- **Smart mode OFF**: No events are pushed automatically. Call `get_context` when the user asks about their physical state (battery, location, health, activity) or when physical context would improve your response.
|
|
23
|
+
|
|
24
|
+
Check `get_context().triageProfile.summary` for a description of the user's notification preferences.
|
|
23
25
|
|
|
24
26
|
## Guidelines
|
|
25
27
|
|
|
26
|
-
-
|
|
27
|
-
- Use `get_context` proactively when physical context would improve your response (weather questions, schedule planning, health discussions).
|
|
28
|
+
- Use `get_context` when physical context would improve your response — don't rely on stale data.
|
|
28
29
|
- Don't parrot raw data. Synthesize naturally: "You're running low and away from home" not "Battery: 0.15, location label: null".
|
|
29
|
-
- Proactive insights are observations, not commands. Use your judgment about whether to relay them
|
|
30
|
-
-
|
|
31
|
-
-
|
|
30
|
+
- Proactive insights are observations, not commands. Use your judgment about whether to relay them.
|
|
31
|
+
- Respond with `no_reply` for routine events that don't need user attention.
|
|
32
|
+
- Check timestamps (`updatedAt`, `computedAt`) to assess data freshness.
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const BETTERCLAW_COMMANDS = [
|
|
2
|
+
"clipboard.write",
|
|
3
|
+
"device.battery",
|
|
4
|
+
"geofence.add",
|
|
5
|
+
"geofence.list",
|
|
6
|
+
"geofence.remove",
|
|
7
|
+
"health.distance",
|
|
8
|
+
"health.heartrate",
|
|
9
|
+
"health.hrv",
|
|
10
|
+
"health.restinghr",
|
|
11
|
+
"health.sleep",
|
|
12
|
+
"health.steps",
|
|
13
|
+
"health.summary",
|
|
14
|
+
"health.workouts",
|
|
15
|
+
"location.get",
|
|
16
|
+
"shortcuts.run",
|
|
17
|
+
"subscribe.add",
|
|
18
|
+
"subscribe.list",
|
|
19
|
+
"subscribe.pause",
|
|
20
|
+
"subscribe.remove",
|
|
21
|
+
"subscribe.resume",
|
|
22
|
+
"system.capabilities",
|
|
23
|
+
"system.notify",
|
|
24
|
+
].sort();
|
|
25
|
+
|
|
26
|
+
export function mergeAllowCommands(existing: string[], toAdd: string[]): string[] {
|
|
27
|
+
const set = new Set([...existing, ...toAdd]);
|
|
28
|
+
return [...set].sort();
|
|
29
|
+
}
|
package/src/context.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import type { DeviceContext, DeviceEvent, Patterns } from "./types.js";
|
|
3
|
+
import type { DeviceConfig, DeviceContext, DeviceEvent, Patterns, RuntimeState } from "./types.js";
|
|
4
4
|
|
|
5
5
|
const CONTEXT_FILE = "context.json";
|
|
6
6
|
const PATTERNS_FILE = "patterns.json";
|
|
@@ -9,6 +9,9 @@ export class ContextManager {
|
|
|
9
9
|
private contextPath: string;
|
|
10
10
|
private patternsPath: string;
|
|
11
11
|
private context: DeviceContext;
|
|
12
|
+
private runtimeState: RuntimeState = { tier: "free", smartMode: false };
|
|
13
|
+
private timestamps: Record<string, number> = {};
|
|
14
|
+
private deviceConfig: DeviceConfig = {};
|
|
12
15
|
|
|
13
16
|
constructor(stateDir: string) {
|
|
14
17
|
this.contextPath = path.join(stateDir, CONTEXT_FILE);
|
|
@@ -38,9 +41,20 @@ export class ContextManager {
|
|
|
38
41
|
async load(): Promise<void> {
|
|
39
42
|
try {
|
|
40
43
|
const raw = await fs.readFile(this.contextPath, "utf8");
|
|
41
|
-
|
|
44
|
+
const parsed = JSON.parse(raw);
|
|
45
|
+
this.timestamps = parsed._timestamps ?? {};
|
|
46
|
+
delete parsed._timestamps;
|
|
47
|
+
this.context = parsed as DeviceContext;
|
|
42
48
|
} catch {
|
|
43
49
|
this.context = ContextManager.empty();
|
|
50
|
+
this.timestamps = {};
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const configPath = path.join(path.dirname(this.contextPath), "device-config.json");
|
|
54
|
+
const rawConfig = await fs.readFile(configPath, "utf8");
|
|
55
|
+
this.deviceConfig = JSON.parse(rawConfig) as DeviceConfig;
|
|
56
|
+
} catch {
|
|
57
|
+
this.deviceConfig = {};
|
|
44
58
|
}
|
|
45
59
|
}
|
|
46
60
|
|
|
@@ -48,18 +62,27 @@ export class ContextManager {
|
|
|
48
62
|
return this.context;
|
|
49
63
|
}
|
|
50
64
|
|
|
65
|
+
getTimestamp(field: string): number | undefined {
|
|
66
|
+
return this.timestamps[field];
|
|
67
|
+
}
|
|
68
|
+
|
|
51
69
|
async save(): Promise<void> {
|
|
52
70
|
await fs.mkdir(path.dirname(this.contextPath), { recursive: true });
|
|
53
|
-
|
|
71
|
+
const data = { ...this.context, _timestamps: this.timestamps };
|
|
72
|
+
await fs.writeFile(this.contextPath, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
73
|
+
const configPath = path.join(path.dirname(this.contextPath), "device-config.json");
|
|
74
|
+
await fs.writeFile(configPath, JSON.stringify(this.deviceConfig, null, 2) + "\n", "utf8");
|
|
54
75
|
}
|
|
55
76
|
|
|
56
77
|
updateFromEvent(event: DeviceEvent): void {
|
|
57
78
|
const now = event.firedAt;
|
|
58
79
|
const data = event.data;
|
|
59
80
|
|
|
60
|
-
// Reset daily counters at midnight
|
|
61
|
-
const
|
|
62
|
-
const
|
|
81
|
+
// Reset daily counters at local midnight
|
|
82
|
+
const lastDate = new Date(this.context.meta.lastEventAt * 1000);
|
|
83
|
+
const currentDate = new Date(now * 1000);
|
|
84
|
+
const lastDay = `${lastDate.getFullYear()}-${lastDate.getMonth()}-${lastDate.getDate()}`;
|
|
85
|
+
const currentDay = `${currentDate.getFullYear()}-${currentDate.getMonth()}-${currentDate.getDate()}`;
|
|
63
86
|
if (lastDay !== currentDay && this.context.meta.lastEventAt > 0) {
|
|
64
87
|
this.context.meta.eventsToday = 0;
|
|
65
88
|
this.context.meta.pushesToday = 0;
|
|
@@ -76,6 +99,7 @@ export class ContextManager {
|
|
|
76
99
|
isLowPowerMode: (data.isLowPowerMode ?? 0) === 1,
|
|
77
100
|
updatedAt: data.updatedAt ?? now,
|
|
78
101
|
};
|
|
102
|
+
this.timestamps.battery = event.firedAt;
|
|
79
103
|
break;
|
|
80
104
|
|
|
81
105
|
case "geofence.triggered": {
|
|
@@ -112,6 +136,8 @@ export class ContextManager {
|
|
|
112
136
|
label: this.context.activity.currentZone,
|
|
113
137
|
updatedAt: data.timestamp ?? now,
|
|
114
138
|
};
|
|
139
|
+
this.timestamps.location = event.firedAt;
|
|
140
|
+
this.timestamps.activity = event.firedAt;
|
|
115
141
|
break;
|
|
116
142
|
}
|
|
117
143
|
|
|
@@ -127,6 +153,7 @@ export class ContextManager {
|
|
|
127
153
|
sleepDurationSeconds: data.sleepDurationSeconds ?? this.context.device.health?.sleepDurationSeconds ?? null,
|
|
128
154
|
updatedAt: data.updatedAt ?? now,
|
|
129
155
|
};
|
|
156
|
+
this.timestamps.health = event.firedAt;
|
|
130
157
|
}
|
|
131
158
|
break;
|
|
132
159
|
}
|
|
@@ -141,8 +168,8 @@ export class ContextManager {
|
|
|
141
168
|
sleepDurationSeconds?: number;
|
|
142
169
|
};
|
|
143
170
|
geofence?: { type: string; zoneName: string; latitude: number; longitude: number };
|
|
144
|
-
}): void {
|
|
145
|
-
const now = Date.now() / 1000;
|
|
171
|
+
}, timestamp?: number): void {
|
|
172
|
+
const now = timestamp ?? Date.now() / 1000;
|
|
146
173
|
|
|
147
174
|
if (snapshot.battery) {
|
|
148
175
|
this.context.device.battery = {
|
|
@@ -151,6 +178,7 @@ export class ContextManager {
|
|
|
151
178
|
isLowPowerMode: snapshot.battery.isLowPowerMode,
|
|
152
179
|
updatedAt: now,
|
|
153
180
|
};
|
|
181
|
+
this.timestamps.battery = now;
|
|
154
182
|
}
|
|
155
183
|
|
|
156
184
|
if (snapshot.location) {
|
|
@@ -161,6 +189,7 @@ export class ContextManager {
|
|
|
161
189
|
label: this.context.activity.currentZone,
|
|
162
190
|
updatedAt: now,
|
|
163
191
|
};
|
|
192
|
+
this.timestamps.location = now;
|
|
164
193
|
}
|
|
165
194
|
|
|
166
195
|
if (snapshot.health) {
|
|
@@ -174,6 +203,7 @@ export class ContextManager {
|
|
|
174
203
|
sleepDurationSeconds: snapshot.health.sleepDurationSeconds ?? null,
|
|
175
204
|
updatedAt: now,
|
|
176
205
|
};
|
|
206
|
+
this.timestamps.health = now;
|
|
177
207
|
}
|
|
178
208
|
|
|
179
209
|
if (snapshot.geofence) {
|
|
@@ -196,6 +226,24 @@ export class ContextManager {
|
|
|
196
226
|
this.context.device.location.label = this.context.activity.currentZone;
|
|
197
227
|
}
|
|
198
228
|
}
|
|
229
|
+
|
|
230
|
+
this.timestamps.lastSnapshot = now;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
getRuntimeState(): RuntimeState {
|
|
234
|
+
return { ...this.runtimeState };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
setRuntimeState(state: RuntimeState): void {
|
|
238
|
+
this.runtimeState = { ...state };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
getDeviceConfig(): DeviceConfig {
|
|
242
|
+
return { ...this.deviceConfig };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
setDeviceConfig(update: DeviceConfig): void {
|
|
246
|
+
this.deviceConfig = { ...this.deviceConfig, ...update };
|
|
199
247
|
}
|
|
200
248
|
|
|
201
249
|
recordPush(): void {
|
package/src/events.ts
CHANGED
|
@@ -26,7 +26,10 @@ export class EventLog {
|
|
|
26
26
|
.trim()
|
|
27
27
|
.split("\n")
|
|
28
28
|
.filter((line) => line.length > 0)
|
|
29
|
-
.
|
|
29
|
+
.flatMap((line) => {
|
|
30
|
+
try { return [JSON.parse(line) as EventLogEntry]; }
|
|
31
|
+
catch { return []; }
|
|
32
|
+
});
|
|
30
33
|
} catch {
|
|
31
34
|
return [];
|
|
32
35
|
}
|
|
@@ -51,7 +54,9 @@ export class EventLog {
|
|
|
51
54
|
const removed = entries.length - kept.length;
|
|
52
55
|
|
|
53
56
|
const content = kept.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
54
|
-
|
|
57
|
+
const tmpPath = this.filePath + ".tmp";
|
|
58
|
+
await fs.writeFile(tmpPath, content, "utf8");
|
|
59
|
+
await fs.rename(tmpPath, this.filePath);
|
|
55
60
|
|
|
56
61
|
return removed;
|
|
57
62
|
}
|
package/src/filter.ts
CHANGED
|
@@ -18,7 +18,7 @@ export class RulesEngine {
|
|
|
18
18
|
this.pushBudget = pushBudget;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
evaluate(event: DeviceEvent, context: DeviceContext): FilterDecision {
|
|
21
|
+
evaluate(event: DeviceEvent, context: DeviceContext, budgetOverride?: number): FilterDecision {
|
|
22
22
|
// Debug events always pass
|
|
23
23
|
if (event.data._debugFired === 1.0) {
|
|
24
24
|
return { action: "push", reason: "debug event — always push" };
|
|
@@ -65,12 +65,13 @@ export class RulesEngine {
|
|
|
65
65
|
if (hour >= 6 && hour <= 10) {
|
|
66
66
|
return { action: "push", reason: "daily health summary — within morning window" };
|
|
67
67
|
}
|
|
68
|
-
return { action: "
|
|
68
|
+
return { action: "drop", reason: "daily health summary — outside morning window" };
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
// Push budget check
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
const budget = budgetOverride ?? this.pushBudget;
|
|
73
|
+
if (context.meta.pushesToday >= budget) {
|
|
74
|
+
return { action: "drop", reason: `push budget exhausted (${context.meta.pushesToday}/${budget} today)` };
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
// Anything else is ambiguous — forward to LLM judgment
|