mirror of
https://github.com/bckelley/cgm-remote-monitor.git
synced 2026-08-24 03:14:12 -05:00
Add proposal for agentic collaboration control plane and data plane
Create a new RFC document and associated JSON schemas defining an agentic control plane for automated insulin delivery systems, including event envelopes, capabilities models, delivery tracking, and conflict rules. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 06715f81-f852-4156-8ab0-cf9e0aa3564d Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 010cdc4a-2c86-4d2e-9768-bf2769a89953 Replit-Helium-Checkpoint-Created: true
This commit is contained in:
+151
@@ -0,0 +1,151 @@
|
|||||||
|
Below is a refinement / validation pass on your plan, with strong concurrence, notable gaps, and a question list for Trio / Loop / AAPS implementers.
|
||||||
|
|
||||||
|
Strong concurrence
|
||||||
|
• Separating config vs runtime vs computed state is exactly right
|
||||||
|
• ProfileDefinition / OverrideDefinition (user-authored, versioned)
|
||||||
|
• ProfileSelection / OverrideInstance (runtime intent)
|
||||||
|
• PolicyComposition (effective “what’s in force”)
|
||||||
|
This is the cleanest way to make Nightscout a neutral control-plane and keep MDI as the always-valid fallback.
|
||||||
|
• Bridge mode from devicestatus is the correct migration strategy
|
||||||
|
• Synthesize canonical events from snapshots initially; let controllers move to native events over time.
|
||||||
|
• This preserves backward compatibility and lets Nightscout evolve without forcing every client to update immediately.
|
||||||
|
• Authority + audit trails as first-class concerns are non-negotiable
|
||||||
|
• Your requestedBy + authority fields are foundational for safe delegation/agents.
|
||||||
|
|
||||||
|
What’s missing / important to add
|
||||||
|
|
||||||
|
1) You’ll want an explicit Event layer (even if you store objects too)
|
||||||
|
|
||||||
|
Right now the plan describes objects; it should explicitly define:
|
||||||
|
• Event envelope (append-only stream)
|
||||||
|
• eventId (stable UUID)
|
||||||
|
• eventType (e.g., profile.definition.upserted, override.instance.activated, policy.composed, delivery.requested)
|
||||||
|
• issuer (controller/user/agent id)
|
||||||
|
• issuerSeq (monotonic per issuer) or (issuer, time, nonce)
|
||||||
|
• cursor (server-assigned monotonic global ordering)
|
||||||
|
• idempotencyKey (for retries)
|
||||||
|
• refs (IDs/hashes referenced)
|
||||||
|
• payload (object snapshot or delta)
|
||||||
|
|
||||||
|
This is what makes cursor-based sync and reconciliation robust.
|
||||||
|
|
||||||
|
2) Capabilities need a real model (not just an optional pointer)
|
||||||
|
|
||||||
|
Your capabilitySnapshotId? is good, but you’ll likely need:
|
||||||
|
• ControllerKindDefinition (declared schema + what it supports)
|
||||||
|
• ControllerInstanceRegistration (this phone/controller right now)
|
||||||
|
• CapabilitySnapshot (what it can do now, including degraded states)
|
||||||
|
• pump connectivity status
|
||||||
|
• automation enabled/disabled
|
||||||
|
• max basal/bolus ceilings effective right now
|
||||||
|
• CGM health / confidence
|
||||||
|
• time sync health
|
||||||
|
|
||||||
|
Without this, “smart forms” and “digital twin honesty” get shaky.
|
||||||
|
|
||||||
|
3) Delivery tracking objects (you alluded, but not in the schema list)
|
||||||
|
|
||||||
|
To complete “intent vs reality” you want the minimal trio:
|
||||||
|
• DeliveryRequest (controller intent)
|
||||||
|
• DeliveryObservation (pump-confirmed reality)
|
||||||
|
• Reconciliation (match/partial/blocked/unknown + why)
|
||||||
|
|
||||||
|
This is also how you keep “taking a shot” present: an injection is a DeliveryObservation from a human source.
|
||||||
|
|
||||||
|
4) Conflict rules & multi-writer semantics
|
||||||
|
|
||||||
|
Nightscout will see inputs from:
|
||||||
|
• controller app
|
||||||
|
• caregiver app
|
||||||
|
• agent(s)
|
||||||
|
• manual UI
|
||||||
|
• bridge-synthesized from devicestatus
|
||||||
|
|
||||||
|
Define in core:
|
||||||
|
• How concurrent OverrideInstance are merged (composition is authoritative record)
|
||||||
|
• What “superseded” means across issuers
|
||||||
|
• Whether Nightscout allows multiple active overrides of same type
|
||||||
|
• How to prevent “flip-flop” loops (agent toggles override repeatedly)
|
||||||
|
|
||||||
|
5) Hashing/versioning rules need to be explicit
|
||||||
|
|
||||||
|
For ProfileDefinition:
|
||||||
|
• Canonicalization before hashing (sorted keys, normalized units/timezone)
|
||||||
|
• How to represent “same content” across clients
|
||||||
|
• Backward compatibility: allow “legacy profile name” mapping to hash/ID
|
||||||
|
|
||||||
|
6) Security / trust boundary (important for delegation)
|
||||||
|
|
||||||
|
At minimum:
|
||||||
|
• issuer identity model (API keys, OAuth identity, device identity)
|
||||||
|
• authority scopes (“override-only”, “suggest-only”, “can-approve”, “can-activate”)
|
||||||
|
• audit immutability expectations
|
||||||
|
Optional but valuable:
|
||||||
|
• signed events (controller signs with a device key)
|
||||||
|
• tamper-evidence (hash chain per issuer)
|
||||||
|
|
||||||
|
7) WebSockets: good idea, but make it optional
|
||||||
|
|
||||||
|
Real-time subscriptions are helpful, but:
|
||||||
|
• Start with cursor polling and/or SSE
|
||||||
|
• Add WebSockets after the event contract stabilizes
|
||||||
|
This reduces operational complexity early.
|
||||||
|
|
||||||
|
Questions to answer with Trio / Loop / AAPS going forward
|
||||||
|
|
||||||
|
These are the key “integration truth” questions that determine how cleanly they can emit native events and how well bridge mode works.
|
||||||
|
|
||||||
|
A) Profiles & overrides semantics
|
||||||
|
1. Do you have stable identifiers for a profile (beyond name)? If not, can you emit a content hash?
|
||||||
|
2. Can you represent overrides as:
|
||||||
|
• “template” (definition) vs “activation” (instance)?
|
||||||
|
3. What override dimensions exist today?
|
||||||
|
• target range vs single target
|
||||||
|
• sensitivity multiplier / autosens ratio
|
||||||
|
• carb ratio multiplier
|
||||||
|
• basal multiplier / max basal
|
||||||
|
4. How do you resolve multiple overrides? (precedence rules)
|
||||||
|
|
||||||
|
B) Composition
|
||||||
|
5. Do you compute an explicit “effective therapy settings now” object internally?
|
||||||
|
6. Can you emit:
|
||||||
|
• inputs referenced (profile hash + active override IDs)
|
||||||
|
• effective parameters (target range, effective ISF/CR adjustments, safety ceilings)
|
||||||
|
• controller version/build
|
||||||
|
|
||||||
|
If they can emit #5–#6, Nightscout’s digital twin becomes reliable without simulating their algorithm.
|
||||||
|
|
||||||
|
C) Delivery fidelity
|
||||||
|
7. Can you distinguish clearly between:
|
||||||
|
• suggested/recommended action
|
||||||
|
• requested command sent to pump
|
||||||
|
• pump-confirmed enactment
|
||||||
|
8. Do you have pump ACK/NAK/error codes that can be surfaced?
|
||||||
|
9. How do you represent “capped by limits” vs “could not enact due to comms”?
|
||||||
|
|
||||||
|
D) Timing and ordering
|
||||||
|
10. Can you provide monotonic ordering per controller? (sequence numbers)
|
||||||
|
If not, what clock guarantees exist?
|
||||||
|
11. How do you handle offline batching / delayed uploads?
|
||||||
|
|
||||||
|
E) Minimal payload commitment
|
||||||
|
12. What is the smallest “native event set” you’re willing to emit first?
|
||||||
|
|
||||||
|
• override activated/ended
|
||||||
|
• policy composed
|
||||||
|
• delivery observed summary
|
||||||
|
|
||||||
|
This determines Phase 1 adoption feasibility.
|
||||||
|
|
||||||
|
Net: plan is solid, but to be “complete” add these core pieces
|
||||||
|
|
||||||
|
If you add (1) Event envelope, (2) Capabilities model, and (3) Delivery request/observe/reconcile, you have a full, coherent control-plane that:
|
||||||
|
• stays neutral
|
||||||
|
• supports smart forms / CRD-like extensibility
|
||||||
|
• preserves MDI as first-class
|
||||||
|
• enables agents safely
|
||||||
|
|
||||||
|
If you want, I can turn this into a tight Phase 1–3 implementation plan with:
|
||||||
|
• collections/endpoints,
|
||||||
|
• bridge rules from devicestatus,
|
||||||
|
• and the minimal JSON Schemas (OverrideInstance + PolicyComposition + EventEnvelope) as actual draft-2020-12 schema files.
|
||||||
@@ -0,0 +1,804 @@
|
|||||||
|
# RFC: Agentic Control Plane for Automated Insulin Delivery Systems
|
||||||
|
|
||||||
|
**Status:** Draft
|
||||||
|
**Authors:** Nightscout Community
|
||||||
|
**Created:** 2026-01-01
|
||||||
|
**Last Updated:** 2026-01-01
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Abstract
|
||||||
|
|
||||||
|
This RFC proposes a clean separation between **control plane** (policy, configuration, intent) and **data plane** (observations, telemetry, delivery) for Nightscout and compatible automated insulin delivery (AID) systems like Loop, Trio, and AAPS.
|
||||||
|
|
||||||
|
The goal is to enable **agentic collaboration**—where AI agents, caregivers, and automation systems can safely participate in therapy management alongside the primary controller—while maintaining MDI (manual insulin delivery) as an always-valid fallback.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Motivation](#motivation)
|
||||||
|
2. [Design Principles](#design-principles)
|
||||||
|
3. [Architecture Overview](#architecture-overview)
|
||||||
|
4. [Core Data Model](#core-data-model)
|
||||||
|
- [Event Envelope](#event-envelope)
|
||||||
|
- [Configuration Objects](#configuration-objects)
|
||||||
|
- [Runtime State Objects](#runtime-state-objects)
|
||||||
|
- [Computed State Objects](#computed-state-objects)
|
||||||
|
- [Delivery Tracking Objects](#delivery-tracking-objects)
|
||||||
|
- [Capabilities Model](#capabilities-model)
|
||||||
|
5. [API Design](#api-design)
|
||||||
|
6. [Bridge Mode: Legacy Compatibility](#bridge-mode-legacy-compatibility)
|
||||||
|
7. [Multi-Writer Semantics & Conflict Resolution](#multi-writer-semantics--conflict-resolution)
|
||||||
|
8. [Security & Authority Model](#security--authority-model)
|
||||||
|
9. [Implementation Phases](#implementation-phases)
|
||||||
|
10. [Integration Questions for Loop/AAPS/Trio](#integration-questions-for-loopaapstrio)
|
||||||
|
11. [Appendix: JSON Schemas](#appendix-json-schemas)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
### Current State Problems
|
||||||
|
|
||||||
|
1. **Profiles as monolithic blobs** — Entire profile uploaded on every change; no versioning, content-hashing, or stable identifiers
|
||||||
|
2. **Overrides buried in devicestatus** — Temporary adjustments embedded in controller snapshots rather than discrete, auditable events
|
||||||
|
3. **No "effective policy" view** — No materialized representation of "what parameters are actually in force right now"
|
||||||
|
4. **Implicit authority** — No distinction between human intent, controller automation, and delegated agent actions
|
||||||
|
5. **Intent vs. reality gap** — Difficult to distinguish suggested actions from requested commands from confirmed delivery
|
||||||
|
|
||||||
|
### Why This Matters for Agents
|
||||||
|
|
||||||
|
For AI agents to safely assist with therapy management (reconciling hormone cycles, activity levels, geolocation, stress indicators), they need:
|
||||||
|
|
||||||
|
- **Clear authority boundaries** — What can an agent suggest vs. activate?
|
||||||
|
- **Audit trails** — Who changed what, when, and why?
|
||||||
|
- **Composable overrides** — Layer multiple adjustments without conflicts
|
||||||
|
- **Real-time policy state** — What's actually in force right now?
|
||||||
|
- **Delivery verification** — Did the suggested action actually happen?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Design Principles
|
||||||
|
|
||||||
|
1. **Config vs. Runtime vs. Computed** — Separate user-authored configuration from runtime activations from computed effective state
|
||||||
|
2. **Events over Snapshots** — Append-only event streams with cursor-based sync, not mutable state blobs
|
||||||
|
3. **MDI as First-Class** — Manual injections are DeliveryObservations from a human source; the system never assumes automation
|
||||||
|
4. **Authority Hierarchy** — Human > Agent > Controller for conflict resolution
|
||||||
|
5. **Bridge Compatibility** — Synthesize canonical events from legacy devicestatus uploads
|
||||||
|
6. **Neutral Control Plane** — Nightscout stores intent and policy; controllers execute
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ NIGHTSCOUT │
|
||||||
|
│ (Neutral Control Plane) │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────┐ ┌──────────────────────┐ ┌───────────────────┐ │
|
||||||
|
│ │ CONFIG OBJECTS │ │ RUNTIME EVENTS │ │ COMPUTED STATE │ │
|
||||||
|
│ │ │ │ │ │ │ │
|
||||||
|
│ │ • ProfileDefinition │ │ • ProfileSelection │ │ • PolicyCompos- │ │
|
||||||
|
│ │ • OverrideDefinition │ │ • OverrideInstance │ │ ition │ │
|
||||||
|
│ │ • ControllerKind │ │ • DeliveryRequest │ │ • CapabilitySnap- │ │
|
||||||
|
│ │ Definition │ │ • DeliveryObserv- │ │ shot │ │
|
||||||
|
│ │ │ │ ation │ │ │ │
|
||||||
|
│ └──────────────────────┘ │ • Reconciliation │ └───────────────────┘ │
|
||||||
|
│ └──────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ EVENT STREAM │ │
|
||||||
|
│ │ cursor-ordered, append-only, per-issuer sequencing │ │
|
||||||
|
│ └──────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ DATA PLANE │
|
||||||
|
│ ┌──────────────────────┐ ┌──────────────────────┐ ┌───────────────────┐ │
|
||||||
|
│ │ ENTRIES │ │ TREATMENTS │ │ DEVICESTATUS │ │
|
||||||
|
│ │ (CGM readings) │ │ (carbs, insulin) │ │ (legacy blob) │ │
|
||||||
|
│ └──────────────────────┘ └──────────────────────┘ └───────────────────┘ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ LOOP │ │ TRIO │ │ AAPS │
|
||||||
|
│ (Controller) │ │ (Controller) │ │ (Controller) │
|
||||||
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ AI AGENT │ │ CAREGIVER │ │ HUMAN │
|
||||||
|
│ (delegated) │ │ (remote) │ │ (primary) │
|
||||||
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Data Model
|
||||||
|
|
||||||
|
### Event Envelope
|
||||||
|
|
||||||
|
All state changes are wrapped in an **Event Envelope** for consistent ordering, replay, and audit.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
EventEnvelope:
|
||||||
|
eventId: string # Stable UUID
|
||||||
|
eventType: string # e.g., "profile.definition.created", "override.instance.activated"
|
||||||
|
cursor: integer # Server-assigned monotonic global ordering
|
||||||
|
issuer: string # Controller/user/agent identifier
|
||||||
|
issuerSeq: integer # Monotonic sequence per issuer
|
||||||
|
idempotencyKey: string # For retry deduplication
|
||||||
|
timestamp: datetime # ISO 8601
|
||||||
|
refs: # Referenced object IDs/hashes
|
||||||
|
- refType: string
|
||||||
|
refId: string
|
||||||
|
payload: object # The actual event data
|
||||||
|
```
|
||||||
|
|
||||||
|
**Event Types:**
|
||||||
|
|
||||||
|
| Category | Event Types |
|
||||||
|
|----------|-------------|
|
||||||
|
| Profile | `profile.definition.created`, `profile.definition.updated`, `profile.selection.changed` |
|
||||||
|
| Override | `override.definition.created`, `override.instance.activated`, `override.instance.ended`, `override.instance.superseded` |
|
||||||
|
| Policy | `policy.composition.computed` |
|
||||||
|
| Delivery | `delivery.requested`, `delivery.observed`, `delivery.reconciled` |
|
||||||
|
| Capability | `controller.registered`, `capability.snapshot.updated` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Configuration Objects
|
||||||
|
|
||||||
|
These are **user-authored, versioned, addressable** objects.
|
||||||
|
|
||||||
|
#### ProfileDefinition
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ProfileDefinition:
|
||||||
|
profileId: string # Stable identifier
|
||||||
|
contentHash: string # SHA-256 of canonicalized content
|
||||||
|
title: string # Human-readable name
|
||||||
|
timezone: string # IANA timezone
|
||||||
|
units: "mg/dL" | "mmol/L"
|
||||||
|
|
||||||
|
schedules:
|
||||||
|
basal: # Time-based basal rates
|
||||||
|
- time: "HH:MM"
|
||||||
|
rate: number # U/hr
|
||||||
|
isf: # Insulin sensitivity factor
|
||||||
|
- time: "HH:MM"
|
||||||
|
value: number
|
||||||
|
cr: # Carb ratio
|
||||||
|
- time: "HH:MM"
|
||||||
|
value: number
|
||||||
|
target: # Target glucose ranges
|
||||||
|
- time: "HH:MM"
|
||||||
|
low: number
|
||||||
|
high: number
|
||||||
|
|
||||||
|
insulinModel:
|
||||||
|
type: "rapid" | "fiasp" | "lyumjev" | "custom"
|
||||||
|
dia: number # Duration of insulin action (hours)
|
||||||
|
peakTime: number # Minutes to peak
|
||||||
|
|
||||||
|
createdBy:
|
||||||
|
issuerType: "human" | "controller" | "agent"
|
||||||
|
issuerId: string
|
||||||
|
createdAt: datetime
|
||||||
|
|
||||||
|
legacyProfileName: string # For backward compatibility mapping
|
||||||
|
```
|
||||||
|
|
||||||
|
#### OverrideDefinition
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
OverrideDefinition:
|
||||||
|
definitionId: string
|
||||||
|
overrideType: "exercise" | "sleep" | "preMeal" | "illness" | "highActivity" | "hormones" | "custom"
|
||||||
|
title: string
|
||||||
|
defaultDuration: integer # seconds, null = indefinite
|
||||||
|
|
||||||
|
effects:
|
||||||
|
targetRange:
|
||||||
|
low: number
|
||||||
|
high: number
|
||||||
|
targetDelta: number # mg/dL adjustment to existing target
|
||||||
|
basalMultiplier: number # 1.0 = no change, 0.5 = 50%
|
||||||
|
maxBasalCeiling: number # U/hr cap
|
||||||
|
sensitivityMultiplier: number
|
||||||
|
carbRatioMultiplier: number
|
||||||
|
automationAggressiveness: number # 0.0 - 1.0 if controller supports
|
||||||
|
|
||||||
|
createdBy:
|
||||||
|
issuerType: "human" | "controller" | "agent"
|
||||||
|
issuerId: string
|
||||||
|
createdAt: datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Runtime State Objects
|
||||||
|
|
||||||
|
These represent **concrete activations and intents**.
|
||||||
|
|
||||||
|
#### ProfileSelection
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ProfileSelection:
|
||||||
|
selectionId: string
|
||||||
|
selectedProfileId: string
|
||||||
|
selectedProfileHash: string # For verification
|
||||||
|
effectiveAt: datetime
|
||||||
|
|
||||||
|
selectedBy:
|
||||||
|
issuerType: "human" | "controller" | "agent"
|
||||||
|
issuerId: string
|
||||||
|
authority: "primary" | "delegated" | "automated"
|
||||||
|
|
||||||
|
reason: string # Optional annotation
|
||||||
|
```
|
||||||
|
|
||||||
|
#### OverrideInstance
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
OverrideInstance:
|
||||||
|
instanceId: string
|
||||||
|
definitionId: string # Optional - may be ad-hoc
|
||||||
|
|
||||||
|
start: datetime
|
||||||
|
duration: integer # seconds, null = indefinite
|
||||||
|
end: datetime # Computed or explicit
|
||||||
|
|
||||||
|
effectiveEffects: # Resolved effects (may differ from definition)
|
||||||
|
targetRange:
|
||||||
|
low: number
|
||||||
|
high: number
|
||||||
|
basalMultiplier: number
|
||||||
|
sensitivityMultiplier: number
|
||||||
|
carbRatioMultiplier: number
|
||||||
|
|
||||||
|
requestedBy:
|
||||||
|
issuerType: "human" | "controller" | "agent"
|
||||||
|
issuerId: string
|
||||||
|
authority: "primary" | "delegated" | "automated"
|
||||||
|
|
||||||
|
status: "active" | "ended" | "canceled" | "superseded"
|
||||||
|
supersededBy: string # instanceId of superseding override
|
||||||
|
|
||||||
|
reason: string # Why this override was activated
|
||||||
|
annotations: object # Extensible metadata
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Computed State Objects
|
||||||
|
|
||||||
|
These are **materialized views** computed by Nightscout from events.
|
||||||
|
|
||||||
|
#### PolicyComposition
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
PolicyComposition:
|
||||||
|
compositionId: string
|
||||||
|
|
||||||
|
references:
|
||||||
|
profileId: string
|
||||||
|
profileHash: string
|
||||||
|
activeOverrideInstanceIds: [string]
|
||||||
|
capabilitySnapshotId: string
|
||||||
|
|
||||||
|
effectiveParameters:
|
||||||
|
targetRange:
|
||||||
|
low: number
|
||||||
|
high: number
|
||||||
|
effectiveISF: number
|
||||||
|
effectiveCR: number
|
||||||
|
effectiveBasal: number # Current scheduled rate after multipliers
|
||||||
|
maxBasalAllowed: number
|
||||||
|
maxBolusAllowed: number
|
||||||
|
automationEnabled: boolean
|
||||||
|
|
||||||
|
computedBy:
|
||||||
|
controllerKind: string
|
||||||
|
controllerVersion: string
|
||||||
|
computedAt: datetime
|
||||||
|
|
||||||
|
validFrom: datetime
|
||||||
|
validTo: datetime # null = current
|
||||||
|
|
||||||
|
cursor: integer # For ordering
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Delivery Tracking Objects
|
||||||
|
|
||||||
|
These complete the **intent → action → confirmation** loop.
|
||||||
|
|
||||||
|
#### DeliveryRequest
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
DeliveryRequest:
|
||||||
|
requestId: string
|
||||||
|
requestType: "tempBasal" | "bolus" | "suspend" | "resume"
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
rate: number # U/hr for temp basal
|
||||||
|
units: number # Units for bolus
|
||||||
|
duration: integer # seconds for temp basal
|
||||||
|
|
||||||
|
requestedBy:
|
||||||
|
issuerType: "human" | "controller" | "agent"
|
||||||
|
issuerId: string
|
||||||
|
|
||||||
|
basedOn:
|
||||||
|
policyCompositionId: string
|
||||||
|
algorithmSuggestion: object # Optional: the suggestion that led to this
|
||||||
|
|
||||||
|
requestedAt: datetime
|
||||||
|
expiresAt: datetime # Request is stale after this
|
||||||
|
```
|
||||||
|
|
||||||
|
#### DeliveryObservation
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
DeliveryObservation:
|
||||||
|
observationId: string
|
||||||
|
observationType: "basal" | "bolus" | "suspend" | "injection" | "pen"
|
||||||
|
|
||||||
|
source:
|
||||||
|
sourceType: "pump" | "manual" | "pen" | "inhaler"
|
||||||
|
sourceId: string # Device identifier
|
||||||
|
sourceKind: string # "omnipod" | "medtronic" | "tandem" | "pen"
|
||||||
|
|
||||||
|
observed:
|
||||||
|
rate: number
|
||||||
|
units: number
|
||||||
|
duration: integer
|
||||||
|
startTime: datetime
|
||||||
|
endTime: datetime
|
||||||
|
|
||||||
|
confidence: "confirmed" | "inferred" | "reported"
|
||||||
|
|
||||||
|
reportedBy:
|
||||||
|
issuerType: "human" | "controller" | "agent"
|
||||||
|
issuerId: string
|
||||||
|
|
||||||
|
observedAt: datetime
|
||||||
|
|
||||||
|
pumpResponse:
|
||||||
|
acked: boolean
|
||||||
|
errorCode: string
|
||||||
|
errorMessage: string
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Reconciliation
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
Reconciliation:
|
||||||
|
reconciliationId: string
|
||||||
|
|
||||||
|
requestId: string # The DeliveryRequest
|
||||||
|
observationId: string # The DeliveryObservation
|
||||||
|
|
||||||
|
outcome: "matched" | "partial" | "blocked" | "unknown" | "expired"
|
||||||
|
|
||||||
|
discrepancy:
|
||||||
|
requestedUnits: number
|
||||||
|
deliveredUnits: number
|
||||||
|
delta: number
|
||||||
|
reason: string # "capped_by_limit" | "comm_failure" | "user_canceled" | "pump_error"
|
||||||
|
|
||||||
|
reconciledAt: datetime
|
||||||
|
reconciledBy:
|
||||||
|
issuerType: "controller" | "agent"
|
||||||
|
issuerId: string
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Capabilities Model
|
||||||
|
|
||||||
|
For "digital twin honesty"—knowing what the controller can actually do right now.
|
||||||
|
|
||||||
|
#### ControllerKindDefinition
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ControllerKindDefinition:
|
||||||
|
kindId: string # "loop" | "trio" | "aaps" | "openaps"
|
||||||
|
version: string
|
||||||
|
|
||||||
|
supportedFeatures:
|
||||||
|
tempBasal: boolean
|
||||||
|
microBolus: boolean
|
||||||
|
suspend: boolean
|
||||||
|
overrides: boolean
|
||||||
|
autoSens: boolean
|
||||||
|
dynamicISF: boolean
|
||||||
|
dynamicCR: boolean
|
||||||
|
smbWithCOB: boolean
|
||||||
|
uam: boolean
|
||||||
|
|
||||||
|
supportedPumps: [string]
|
||||||
|
supportedCGMs: [string]
|
||||||
|
|
||||||
|
eventCapabilities:
|
||||||
|
canEmitNativeEvents: boolean
|
||||||
|
minimalEventSet: [string] # Event types it can emit
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ControllerInstanceRegistration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ControllerInstanceRegistration:
|
||||||
|
instanceId: string
|
||||||
|
kindId: string
|
||||||
|
version: string
|
||||||
|
|
||||||
|
device:
|
||||||
|
deviceId: string
|
||||||
|
platform: "ios" | "android" | "linux"
|
||||||
|
model: string
|
||||||
|
|
||||||
|
pumpBinding:
|
||||||
|
pumpKind: string
|
||||||
|
pumpSerial: string
|
||||||
|
connectedSince: datetime
|
||||||
|
|
||||||
|
cgmBinding:
|
||||||
|
cgmKind: string
|
||||||
|
cgmId: string
|
||||||
|
|
||||||
|
registeredAt: datetime
|
||||||
|
lastSeenAt: datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
#### CapabilitySnapshot
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
CapabilitySnapshot:
|
||||||
|
snapshotId: string
|
||||||
|
controllerInstanceId: string
|
||||||
|
|
||||||
|
connectivity:
|
||||||
|
pumpConnected: boolean
|
||||||
|
pumpLastContact: datetime
|
||||||
|
cgmConnected: boolean
|
||||||
|
cgmLastReading: datetime
|
||||||
|
|
||||||
|
automationState:
|
||||||
|
closedLoopEnabled: boolean
|
||||||
|
suspended: boolean
|
||||||
|
suspendReason: string
|
||||||
|
|
||||||
|
effectiveLimits:
|
||||||
|
maxBasal: number
|
||||||
|
maxBolus: number
|
||||||
|
maxIOB: number
|
||||||
|
|
||||||
|
health:
|
||||||
|
reservoirUnits: number
|
||||||
|
batteryPercent: number
|
||||||
|
cgmCalibrationStatus: string
|
||||||
|
timeSyncHealth: "good" | "drift" | "unknown"
|
||||||
|
|
||||||
|
snapshotAt: datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Design
|
||||||
|
|
||||||
|
### New Collections (API v3)
|
||||||
|
|
||||||
|
| Collection | Operations | Description |
|
||||||
|
|------------|------------|-------------|
|
||||||
|
| `/profileDefinitions` | CRUD + history | User-authored profile configs |
|
||||||
|
| `/profileSelections` | CRUD + history | Profile activation events |
|
||||||
|
| `/overrideDefinitions` | CRUD + history | Reusable override templates |
|
||||||
|
| `/overrideInstances` | CRUD + history | Concrete override activations |
|
||||||
|
| `/policyCompositions` | Read + history | Computed effective policy (read-only) |
|
||||||
|
| `/deliveryRequests` | CRUD + history | Delivery intent records |
|
||||||
|
| `/deliveryObservations` | CRUD + history | Confirmed delivery records |
|
||||||
|
| `/reconciliations` | Read + history | Request/observation matching |
|
||||||
|
| `/controllerRegistrations` | CRUD + history | Controller instance registry |
|
||||||
|
| `/capabilitySnapshots` | CRUD + history | Controller capability state |
|
||||||
|
| `/events` | Read + subscribe | Unified event stream |
|
||||||
|
|
||||||
|
### Event Stream Endpoint
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v3/events?cursor={lastCursor}&eventTypes={types}&issuers={ids}
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns events after the given cursor, optionally filtered by type and issuer.
|
||||||
|
|
||||||
|
### WebSocket / SSE Subscriptions
|
||||||
|
|
||||||
|
```
|
||||||
|
WS /api/v3/events/subscribe
|
||||||
|
SSE /api/v3/events/stream?cursor={cursor}
|
||||||
|
```
|
||||||
|
|
||||||
|
Real-time event delivery with cursor-based resumption.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bridge Mode: Legacy Compatibility
|
||||||
|
|
||||||
|
For controllers that continue uploading `devicestatus` blobs, Nightscout synthesizes canonical events.
|
||||||
|
|
||||||
|
### Bridge Processing Pipeline
|
||||||
|
|
||||||
|
```
|
||||||
|
devicestatus upload
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────┐
|
||||||
|
│ Parse devicestatus │
|
||||||
|
│ • Extract profile │
|
||||||
|
│ • Extract overrides │
|
||||||
|
│ • Extract pump │
|
||||||
|
│ • Extract suggested │
|
||||||
|
│ • Extract enacted │
|
||||||
|
└──────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────┐
|
||||||
|
│ Diff against last │
|
||||||
|
│ known state │
|
||||||
|
└──────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────┐
|
||||||
|
│ Emit events: │
|
||||||
|
│ • ProfileDefinition │ (if profile content hash changed)
|
||||||
|
│ • ProfileSelection │ (if active profile changed)
|
||||||
|
│ • OverrideInstance │ (if override state changed)
|
||||||
|
│ • PolicyComposition │ (always, as snapshot)
|
||||||
|
│ • DeliveryObserv- │ (if enacted present)
|
||||||
|
│ ation │
|
||||||
|
└──────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────┐
|
||||||
|
│ Store devicestatus │
|
||||||
|
│ with bridge refs │
|
||||||
|
└──────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bridge Rules
|
||||||
|
|
||||||
|
1. **Profile hashing:** Canonicalize (sort keys, normalize units/timezone), then SHA-256
|
||||||
|
2. **Override diffing:** Compare active override state; emit `activated`/`ended` as needed
|
||||||
|
3. **Delivery extraction:** Map `enacted` to `DeliveryObservation`, `suggested` to metadata
|
||||||
|
4. **Idempotency:** Use `(issuer, issuerSeq)` or `(devicestatus._id, field)` for dedup
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Multi-Writer Semantics & Conflict Resolution
|
||||||
|
|
||||||
|
### Writers
|
||||||
|
|
||||||
|
Nightscout accepts inputs from:
|
||||||
|
- Controller app (primary automation)
|
||||||
|
- Caregiver app (remote monitoring/intervention)
|
||||||
|
- AI agents (delegated assistance)
|
||||||
|
- Manual UI (user-initiated)
|
||||||
|
- Bridge (synthesized from legacy uploads)
|
||||||
|
|
||||||
|
### Authority Hierarchy
|
||||||
|
|
||||||
|
```
|
||||||
|
HUMAN (primary)
|
||||||
|
│
|
||||||
|
├── HUMAN (caregiver, delegated)
|
||||||
|
│
|
||||||
|
├── AGENT (delegated)
|
||||||
|
│
|
||||||
|
└── CONTROLLER (automated)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Conflict Rules
|
||||||
|
|
||||||
|
1. **Override composition:** Multiple active overrides are composed into PolicyComposition; conflicts resolved by:
|
||||||
|
- Most restrictive target range
|
||||||
|
- Lowest basal multiplier (safety bias)
|
||||||
|
- Human > Agent > Controller authority
|
||||||
|
|
||||||
|
2. **Supersession:** A new override of the same type from equal or higher authority supersedes the previous
|
||||||
|
|
||||||
|
3. **Flip-flop prevention:**
|
||||||
|
- Rate limiting per issuer (max N changes per time window)
|
||||||
|
- Cooldown period after override end before same type can be activated
|
||||||
|
- Agent-initiated overrides require human confirmation if >N in period
|
||||||
|
|
||||||
|
4. **Profile selection:** Most recent selection wins; PolicyComposition always references current selection
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security & Authority Model
|
||||||
|
|
||||||
|
### Identity Model
|
||||||
|
|
||||||
|
| Issuer Type | Identity Mechanism |
|
||||||
|
|-------------|-------------------|
|
||||||
|
| Human | OAuth identity (Nightscout account) |
|
||||||
|
| Controller | Device-bound API key + device attestation |
|
||||||
|
| Agent | OAuth + scoped delegation token |
|
||||||
|
| Caregiver | OAuth + explicit delegation grant |
|
||||||
|
|
||||||
|
### Authority Scopes
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
Scopes:
|
||||||
|
read:
|
||||||
|
- entries.read
|
||||||
|
- treatments.read
|
||||||
|
- policy.read
|
||||||
|
- delivery.read
|
||||||
|
|
||||||
|
suggest:
|
||||||
|
- override.suggest # Can propose, human must approve
|
||||||
|
- delivery.suggest
|
||||||
|
|
||||||
|
activate:
|
||||||
|
- override.activate # Can directly activate
|
||||||
|
- profile.select
|
||||||
|
|
||||||
|
deliver:
|
||||||
|
- delivery.request # Can issue delivery requests
|
||||||
|
|
||||||
|
admin:
|
||||||
|
- controller.register
|
||||||
|
- delegation.grant
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delegation Model
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
DelegationGrant:
|
||||||
|
grantId: string
|
||||||
|
grantedBy: string # Human issuer ID
|
||||||
|
grantedTo: string # Agent/caregiver issuer ID
|
||||||
|
|
||||||
|
scopes: [string]
|
||||||
|
|
||||||
|
constraints:
|
||||||
|
maxOverrideDuration: integer
|
||||||
|
allowedOverrideTypes: [string]
|
||||||
|
requireConfirmation: boolean
|
||||||
|
expiresAt: datetime
|
||||||
|
|
||||||
|
grantedAt: datetime
|
||||||
|
revokedAt: datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
### Audit Requirements
|
||||||
|
|
||||||
|
1. All events are append-only
|
||||||
|
2. Events include `issuer`, `authority`, `timestamp`
|
||||||
|
3. Optional: issuer-signed events with device keys
|
||||||
|
4. Optional: hash chain per issuer for tamper evidence
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Phases
|
||||||
|
|
||||||
|
### Phase 1: Foundation (4-6 weeks)
|
||||||
|
|
||||||
|
**Goal:** Core event model and minimal collections
|
||||||
|
|
||||||
|
- [ ] EventEnvelope schema and storage
|
||||||
|
- [ ] ProfileDefinition collection with content hashing
|
||||||
|
- [ ] OverrideInstance collection
|
||||||
|
- [ ] PolicyComposition collection (computed)
|
||||||
|
- [ ] Basic bridge from devicestatus
|
||||||
|
- [ ] Cursor-based event polling endpoint
|
||||||
|
- [ ] SSE subscription (before WebSocket)
|
||||||
|
|
||||||
|
**Deliverables:**
|
||||||
|
- New API v3 collections operational
|
||||||
|
- Legacy devicestatus → event synthesis working
|
||||||
|
- Clients can poll for events
|
||||||
|
|
||||||
|
### Phase 2: Delivery & Capabilities (4-6 weeks)
|
||||||
|
|
||||||
|
**Goal:** Complete intent-to-delivery loop
|
||||||
|
|
||||||
|
- [ ] DeliveryRequest / DeliveryObservation / Reconciliation
|
||||||
|
- [ ] ControllerKindDefinition / ControllerInstanceRegistration
|
||||||
|
- [ ] CapabilitySnapshot
|
||||||
|
- [ ] Enhanced bridge for delivery extraction
|
||||||
|
- [ ] WebSocket subscriptions
|
||||||
|
|
||||||
|
**Deliverables:**
|
||||||
|
- Full delivery tracking operational
|
||||||
|
- Controller capability awareness
|
||||||
|
- Real-time subscriptions
|
||||||
|
|
||||||
|
### Phase 3: Agents & Delegation (6-8 weeks)
|
||||||
|
|
||||||
|
**Goal:** Safe multi-writer with agents
|
||||||
|
|
||||||
|
- [ ] Authority scopes and delegation grants
|
||||||
|
- [ ] Agent identity and authentication
|
||||||
|
- [ ] Conflict resolution rules
|
||||||
|
- [ ] Rate limiting and flip-flop prevention
|
||||||
|
- [ ] Confirmation workflows for agent suggestions
|
||||||
|
- [ ] Audit dashboard
|
||||||
|
|
||||||
|
**Deliverables:**
|
||||||
|
- Agents can suggest overrides
|
||||||
|
- Caregivers can delegate to agents
|
||||||
|
- Full audit trail
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration Questions for Loop/AAPS/Trio
|
||||||
|
|
||||||
|
See [integration-questionnaire.md](./integration-questionnaire.md) for the complete questionnaire.
|
||||||
|
|
||||||
|
### Key Questions Summary
|
||||||
|
|
||||||
|
**A) Profiles & Overrides**
|
||||||
|
1. Do you have stable profile identifiers beyond name?
|
||||||
|
2. Can you represent overrides as template vs. activation?
|
||||||
|
3. What override dimensions exist (target, sensitivity, basal, CR)?
|
||||||
|
4. How do you resolve multiple concurrent overrides?
|
||||||
|
|
||||||
|
**B) Composition**
|
||||||
|
5. Do you compute an explicit "effective therapy settings" internally?
|
||||||
|
6. Can you emit: profile hash + override IDs + effective parameters?
|
||||||
|
|
||||||
|
**C) Delivery Fidelity**
|
||||||
|
7. Can you distinguish suggested vs. requested vs. confirmed?
|
||||||
|
8. Do you surface pump ACK/NAK/error codes?
|
||||||
|
9. How do you represent "capped by limits" vs. "comm failure"?
|
||||||
|
|
||||||
|
**D) Timing & Ordering**
|
||||||
|
10. Can you provide monotonic per-controller sequencing?
|
||||||
|
11. How do you handle offline batching?
|
||||||
|
|
||||||
|
**E) Minimal Commitment**
|
||||||
|
12. What's the smallest native event set you'd emit first?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix: JSON Schemas
|
||||||
|
|
||||||
|
Full JSON Schema (draft-2020-12) files are available in the `schemas/` directory:
|
||||||
|
|
||||||
|
- [event-envelope.schema.json](./schemas/event-envelope.schema.json)
|
||||||
|
- [profile-definition.schema.json](./schemas/profile-definition.schema.json)
|
||||||
|
- [profile-selection.schema.json](./schemas/profile-selection.schema.json)
|
||||||
|
- [override-definition.schema.json](./schemas/override-definition.schema.json)
|
||||||
|
- [override-instance.schema.json](./schemas/override-instance.schema.json)
|
||||||
|
- [policy-composition.schema.json](./schemas/policy-composition.schema.json)
|
||||||
|
- [delivery-request.schema.json](./schemas/delivery-request.schema.json)
|
||||||
|
- [delivery-observation.schema.json](./schemas/delivery-observation.schema.json)
|
||||||
|
- [reconciliation.schema.json](./schemas/reconciliation.schema.json)
|
||||||
|
- [controller-kind-definition.schema.json](./schemas/controller-kind-definition.schema.json)
|
||||||
|
- [controller-instance-registration.schema.json](./schemas/controller-instance-registration.schema.json)
|
||||||
|
- [capability-snapshot.schema.json](./schemas/capability-snapshot.schema.json)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Nightscout API v3 Documentation](../api3/swagger.yaml)
|
||||||
|
- [Loop Documentation](https://loopkit.github.io/loopdocs/)
|
||||||
|
- [AAPS Documentation](https://androidaps.readthedocs.io/)
|
||||||
|
- [OpenAPS Documentation](https://openaps.readthedocs.io/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
| Date | Change |
|
||||||
|
|------|--------|
|
||||||
|
| 2026-01-01 | Initial draft |
|
||||||
@@ -0,0 +1,642 @@
|
|||||||
|
# Bridge Mode: Legacy devicestatus to Event Synthesis
|
||||||
|
|
||||||
|
**Purpose:** Define rules for synthesizing canonical control plane events from legacy `devicestatus` uploads, enabling backward compatibility while controllers transition to native event emission.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Bridge mode enables Nightscout to:
|
||||||
|
1. Accept legacy `devicestatus` uploads unchanged
|
||||||
|
2. Parse and extract control plane information
|
||||||
|
3. Synthesize canonical events (`ProfileDefinition`, `OverrideInstance`, `PolicyComposition`, etc.)
|
||||||
|
4. Store both the original devicestatus and synthesized events
|
||||||
|
5. Enable event-based queries and subscriptions
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ DEVICESTATUS UPLOAD │
|
||||||
|
│ POST /api/v1/devicestatus │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ BRIDGE PROCESSOR │
|
||||||
|
│ │
|
||||||
|
│ 1. Store original devicestatus │
|
||||||
|
│ 2. Identify controller type (loop, openaps, aaps) │
|
||||||
|
│ 3. Extract profile information │
|
||||||
|
│ 4. Extract override state │
|
||||||
|
│ 5. Extract delivery information │
|
||||||
|
│ 6. Compute diffs from last known state │
|
||||||
|
│ 7. Emit synthesized events │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ EVENT STREAM │
|
||||||
|
│ │
|
||||||
|
│ • ProfileDefinition (if hash changed) │
|
||||||
|
│ • ProfileSelection (if active profile changed) │
|
||||||
|
│ • OverrideInstance (start/end as needed) │
|
||||||
|
│ • PolicyComposition (always) │
|
||||||
|
│ • DeliveryObservation (if enacted present) │
|
||||||
|
│ • CapabilitySnapshot (if pump/cgm status present) │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Controller Detection
|
||||||
|
|
||||||
|
### Detection Rules
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function detectControllerType(devicestatus) {
|
||||||
|
if (devicestatus.loop) return 'loop';
|
||||||
|
if (devicestatus.openaps) return 'openaps';
|
||||||
|
if (devicestatus.pump && devicestatus.pump.source === 'AAPS') return 'aaps';
|
||||||
|
if (devicestatus.trio) return 'trio';
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Controller-Specific Parsers
|
||||||
|
|
||||||
|
Each controller type has a dedicated parser:
|
||||||
|
|
||||||
|
| Controller | Parser Module | Primary Fields |
|
||||||
|
|------------|--------------|----------------|
|
||||||
|
| Loop | `parsers/loop.js` | `loop.enacted`, `loop.predicted`, `loop.override` |
|
||||||
|
| OpenAPS | `parsers/openaps.js` | `openaps.suggested`, `openaps.enacted`, `openaps.iob` |
|
||||||
|
| AAPS | `parsers/aaps.js` | `pump`, `configuration` |
|
||||||
|
| Trio | `parsers/trio.js` | `trio.enacted`, `trio.override` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Profile Extraction & Hashing
|
||||||
|
|
||||||
|
### Canonicalization Rules
|
||||||
|
|
||||||
|
Before hashing, profiles must be canonicalized:
|
||||||
|
|
||||||
|
1. **Sort keys** alphabetically at all levels
|
||||||
|
2. **Normalize timezone** to IANA format
|
||||||
|
3. **Normalize units** to lowercase (`mg/dl` → `mg/dL`, `mmol/l` → `mmol/L`)
|
||||||
|
4. **Normalize times** to `HH:MM` format (zero-padded)
|
||||||
|
5. **Round numbers** to standard precision (rates: 3 decimals, ratios: 2 decimals)
|
||||||
|
6. **Remove null/undefined** fields
|
||||||
|
7. **Sort arrays** by time field
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function canonicalizeProfile(profile) {
|
||||||
|
const canonical = {
|
||||||
|
basal: sortByTime(profile.basal).map(b => ({
|
||||||
|
time: normalizeTime(b.time),
|
||||||
|
rate: round(b.rate, 3)
|
||||||
|
})),
|
||||||
|
isf: sortByTime(profile.sens || profile.isf).map(s => ({
|
||||||
|
time: normalizeTime(s.time),
|
||||||
|
value: round(s.value, 1)
|
||||||
|
})),
|
||||||
|
cr: sortByTime(profile.carbratio || profile.cr).map(c => ({
|
||||||
|
time: normalizeTime(c.time),
|
||||||
|
value: round(c.value, 1)
|
||||||
|
})),
|
||||||
|
target: sortByTime(profile.target_low ?
|
||||||
|
mergeTargets(profile.target_low, profile.target_high) :
|
||||||
|
profile.target
|
||||||
|
).map(t => ({
|
||||||
|
time: normalizeTime(t.time),
|
||||||
|
low: round(t.low, 0),
|
||||||
|
high: round(t.high, 0)
|
||||||
|
})),
|
||||||
|
dia: round(profile.dia, 1),
|
||||||
|
timezone: normalizeTimezone(profile.timezone),
|
||||||
|
units: normalizeUnits(profile.units)
|
||||||
|
};
|
||||||
|
|
||||||
|
return canonical;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hashProfile(canonicalProfile) {
|
||||||
|
const json = JSON.stringify(canonicalProfile, Object.keys(canonicalProfile).sort());
|
||||||
|
return crypto.createHash('sha256').update(json).digest('hex');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Profile Definition Emission
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function maybeEmitProfileDefinition(devicestatus, lastKnownState) {
|
||||||
|
const profile = extractProfile(devicestatus);
|
||||||
|
if (!profile) return null;
|
||||||
|
|
||||||
|
const canonical = canonicalizeProfile(profile);
|
||||||
|
const hash = hashProfile(canonical);
|
||||||
|
|
||||||
|
// Check if we've seen this profile before
|
||||||
|
if (lastKnownState.profileHashes.has(hash)) {
|
||||||
|
return null; // Already exists, no need to emit
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
eventType: 'profile.definition.created',
|
||||||
|
payload: {
|
||||||
|
profileId: generateProfileId(hash),
|
||||||
|
contentHash: hash,
|
||||||
|
title: profile.profileName || 'Default',
|
||||||
|
timezone: canonical.timezone,
|
||||||
|
units: canonical.units,
|
||||||
|
schedules: {
|
||||||
|
basal: canonical.basal,
|
||||||
|
isf: canonical.isf,
|
||||||
|
cr: canonical.cr,
|
||||||
|
target: canonical.target
|
||||||
|
},
|
||||||
|
insulinModel: {
|
||||||
|
dia: canonical.dia
|
||||||
|
},
|
||||||
|
createdBy: {
|
||||||
|
issuerType: 'controller',
|
||||||
|
issuerId: devicestatus.device || 'unknown'
|
||||||
|
},
|
||||||
|
legacyProfileName: profile.profileName
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
bridgeSource: 'devicestatus',
|
||||||
|
bridgeSourceId: devicestatus._id
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Override State Extraction
|
||||||
|
|
||||||
|
### Loop Override Parsing
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function parseLoopOverride(devicestatus) {
|
||||||
|
const override = devicestatus.loop?.override;
|
||||||
|
if (!override) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
active: override.active !== false,
|
||||||
|
name: override.name,
|
||||||
|
targetRange: override.currentCorrectionRange ? {
|
||||||
|
low: override.currentCorrectionRange.minValue,
|
||||||
|
high: override.currentCorrectionRange.maxValue
|
||||||
|
} : null,
|
||||||
|
basalMultiplier: override.multiplier,
|
||||||
|
sensitivityMultiplier: override.insulinSensitivityScaleFactor,
|
||||||
|
duration: override.duration, // seconds
|
||||||
|
startTime: override.startTime || devicestatus.created_at,
|
||||||
|
symbol: override.symbol
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### AAPS Override Parsing
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function parseAAPSOverride(devicestatus) {
|
||||||
|
// AAPS uses "Temp Target" and "Profile Switch" differently
|
||||||
|
const tempTarget = devicestatus.configuration?.tempTarget;
|
||||||
|
const profileSwitch = devicestatus.configuration?.profileSwitch;
|
||||||
|
|
||||||
|
const overrides = [];
|
||||||
|
|
||||||
|
if (tempTarget && tempTarget.isValid) {
|
||||||
|
overrides.push({
|
||||||
|
type: 'tempTarget',
|
||||||
|
active: true,
|
||||||
|
targetRange: {
|
||||||
|
low: tempTarget.lowTarget,
|
||||||
|
high: tempTarget.highTarget
|
||||||
|
},
|
||||||
|
reason: tempTarget.reason,
|
||||||
|
duration: tempTarget.duration * 60 // minutes to seconds
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profileSwitch && profileSwitch.percentage !== 100) {
|
||||||
|
overrides.push({
|
||||||
|
type: 'profilePercentage',
|
||||||
|
active: true,
|
||||||
|
basalMultiplier: profileSwitch.percentage / 100,
|
||||||
|
duration: profileSwitch.duration * 60
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return overrides;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Override Instance Emission
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function emitOverrideEvents(currentOverride, lastKnownOverride, issuer) {
|
||||||
|
const events = [];
|
||||||
|
|
||||||
|
// Detect override start
|
||||||
|
if (currentOverride?.active && !lastKnownOverride?.active) {
|
||||||
|
events.push({
|
||||||
|
eventType: 'override.instance.activated',
|
||||||
|
payload: {
|
||||||
|
instanceId: generateUUID(),
|
||||||
|
start: currentOverride.startTime,
|
||||||
|
duration: currentOverride.duration,
|
||||||
|
effectiveEffects: {
|
||||||
|
targetRange: currentOverride.targetRange,
|
||||||
|
basalMultiplier: currentOverride.basalMultiplier,
|
||||||
|
sensitivityMultiplier: currentOverride.sensitivityMultiplier
|
||||||
|
},
|
||||||
|
requestedBy: {
|
||||||
|
issuerType: 'controller',
|
||||||
|
issuerId: issuer,
|
||||||
|
authority: 'automated'
|
||||||
|
},
|
||||||
|
status: 'active',
|
||||||
|
reason: currentOverride.name || currentOverride.reason
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect override end
|
||||||
|
if (!currentOverride?.active && lastKnownOverride?.active) {
|
||||||
|
events.push({
|
||||||
|
eventType: 'override.instance.ended',
|
||||||
|
payload: {
|
||||||
|
instanceId: lastKnownOverride.instanceId,
|
||||||
|
status: 'ended',
|
||||||
|
endedBy: {
|
||||||
|
issuerType: 'controller',
|
||||||
|
issuerId: issuer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect override change (supersession)
|
||||||
|
if (currentOverride?.active && lastKnownOverride?.active &&
|
||||||
|
overridesDiffer(currentOverride, lastKnownOverride)) {
|
||||||
|
const newInstanceId = generateUUID();
|
||||||
|
events.push({
|
||||||
|
eventType: 'override.instance.superseded',
|
||||||
|
payload: {
|
||||||
|
instanceId: lastKnownOverride.instanceId,
|
||||||
|
status: 'superseded',
|
||||||
|
supersededBy: newInstanceId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
events.push({
|
||||||
|
eventType: 'override.instance.activated',
|
||||||
|
payload: {
|
||||||
|
instanceId: newInstanceId,
|
||||||
|
// ... same as above
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Policy Composition Synthesis
|
||||||
|
|
||||||
|
Always emit a PolicyComposition for every devicestatus, as it represents the current state.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function synthesizePolicyComposition(devicestatus, controllerType, state) {
|
||||||
|
const profile = extractProfile(devicestatus);
|
||||||
|
const override = extractOverride(devicestatus, controllerType);
|
||||||
|
const limits = extractLimits(devicestatus, controllerType);
|
||||||
|
|
||||||
|
// Compute effective parameters
|
||||||
|
const scheduledBasal = getCurrentScheduledValue(profile.basal);
|
||||||
|
const basalMultiplier = override?.basalMultiplier || 1.0;
|
||||||
|
const effectiveBasal = scheduledBasal * basalMultiplier;
|
||||||
|
|
||||||
|
const scheduledTarget = getCurrentScheduledValue(profile.target);
|
||||||
|
const effectiveTarget = override?.targetRange || scheduledTarget;
|
||||||
|
|
||||||
|
return {
|
||||||
|
eventType: 'policy.composition.computed',
|
||||||
|
payload: {
|
||||||
|
compositionId: generateUUID(),
|
||||||
|
references: {
|
||||||
|
profileId: state.currentProfileId,
|
||||||
|
profileHash: state.currentProfileHash,
|
||||||
|
activeOverrideInstanceIds: state.activeOverrideIds,
|
||||||
|
capabilitySnapshotId: state.latestCapabilitySnapshotId
|
||||||
|
},
|
||||||
|
effectiveParameters: {
|
||||||
|
targetRange: effectiveTarget,
|
||||||
|
effectiveISF: getCurrentScheduledValue(profile.isf),
|
||||||
|
effectiveCR: getCurrentScheduledValue(profile.cr),
|
||||||
|
effectiveBasal: effectiveBasal,
|
||||||
|
scheduledBasal: scheduledBasal,
|
||||||
|
basalMultiplier: basalMultiplier,
|
||||||
|
maxBasalAllowed: limits.maxBasal,
|
||||||
|
maxBolusAllowed: limits.maxBolus,
|
||||||
|
maxIOB: limits.maxIOB,
|
||||||
|
automationEnabled: extractAutomationState(devicestatus),
|
||||||
|
automationMode: extractAutomationMode(devicestatus)
|
||||||
|
},
|
||||||
|
computedBy: {
|
||||||
|
controllerKind: controllerType,
|
||||||
|
controllerVersion: devicestatus.version || 'unknown',
|
||||||
|
computedAt: new Date().toISOString()
|
||||||
|
},
|
||||||
|
validFrom: devicestatus.created_at
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Delivery Observation Extraction
|
||||||
|
|
||||||
|
### Loop Enacted Parsing
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function parseLoopEnacted(devicestatus) {
|
||||||
|
const enacted = devicestatus.loop?.enacted;
|
||||||
|
if (!enacted || !enacted.received) return null;
|
||||||
|
|
||||||
|
const observations = [];
|
||||||
|
|
||||||
|
// Temp basal
|
||||||
|
if (enacted.rate !== undefined) {
|
||||||
|
observations.push({
|
||||||
|
observationType: 'tempBasal',
|
||||||
|
source: {
|
||||||
|
sourceType: 'pump',
|
||||||
|
sourceKind: devicestatus.pump?.pumpModel || 'unknown'
|
||||||
|
},
|
||||||
|
observed: {
|
||||||
|
rate: enacted.rate,
|
||||||
|
duration: enacted.duration * 60, // minutes to seconds
|
||||||
|
startTime: enacted.timestamp
|
||||||
|
},
|
||||||
|
confidence: 'confirmed',
|
||||||
|
relatedSuggestion: {
|
||||||
|
reason: enacted.reason,
|
||||||
|
eventualBG: enacted.eventualBG,
|
||||||
|
predBGs: enacted.predBGs
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bolus (if present)
|
||||||
|
if (enacted.units) {
|
||||||
|
observations.push({
|
||||||
|
observationType: enacted.units < 1 ? 'microBolus' : 'bolus',
|
||||||
|
source: {
|
||||||
|
sourceType: 'pump',
|
||||||
|
sourceKind: devicestatus.pump?.pumpModel || 'unknown'
|
||||||
|
},
|
||||||
|
observed: {
|
||||||
|
units: enacted.units,
|
||||||
|
startTime: enacted.timestamp
|
||||||
|
},
|
||||||
|
confidence: 'confirmed'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return observations;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### OpenAPS Enacted Parsing
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function parseOpenAPSEnacted(devicestatus) {
|
||||||
|
const enacted = devicestatus.openaps?.enacted;
|
||||||
|
const suggested = devicestatus.openaps?.suggested;
|
||||||
|
|
||||||
|
if (!enacted) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
observationType: 'tempBasal',
|
||||||
|
source: {
|
||||||
|
sourceType: 'pump',
|
||||||
|
sourceKind: devicestatus.pump?.pumpmanufacturer || 'unknown'
|
||||||
|
},
|
||||||
|
observed: {
|
||||||
|
rate: enacted.rate,
|
||||||
|
duration: enacted.duration * 60,
|
||||||
|
startTime: enacted.timestamp
|
||||||
|
},
|
||||||
|
confidence: 'confirmed',
|
||||||
|
pumpResponse: {
|
||||||
|
acked: true
|
||||||
|
},
|
||||||
|
relatedSuggestion: suggested ? {
|
||||||
|
reason: suggested.reason,
|
||||||
|
iob: devicestatus.openaps?.iob?.iob,
|
||||||
|
eventualBG: suggested.eventualBG
|
||||||
|
} : null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Capability Snapshot Extraction
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function extractCapabilitySnapshot(devicestatus, controllerType) {
|
||||||
|
const pump = devicestatus.pump || devicestatus.uploaderBattery;
|
||||||
|
|
||||||
|
return {
|
||||||
|
eventType: 'capability.snapshot.updated',
|
||||||
|
payload: {
|
||||||
|
snapshotId: generateUUID(),
|
||||||
|
controllerInstanceId: devicestatus.device,
|
||||||
|
connectivity: {
|
||||||
|
pumpConnected: pump?.status?.bolusing !== undefined,
|
||||||
|
pumpLastContact: pump?.clock,
|
||||||
|
cgmConnected: devicestatus.cgm !== undefined,
|
||||||
|
cgmLastReading: devicestatus.cgm?.mills
|
||||||
|
},
|
||||||
|
automationState: {
|
||||||
|
closedLoopEnabled: extractClosedLoopState(devicestatus, controllerType),
|
||||||
|
suspended: pump?.status?.suspended || false,
|
||||||
|
lastLoopTime: devicestatus.loop?.timestamp || devicestatus.openaps?.suggested?.timestamp
|
||||||
|
},
|
||||||
|
health: {
|
||||||
|
reservoirUnits: pump?.reservoir,
|
||||||
|
batteryPercent: pump?.battery?.percent || devicestatus.uploaderBattery,
|
||||||
|
phoneBatteryPercent: devicestatus.uploaderBattery
|
||||||
|
},
|
||||||
|
snapshotAt: devicestatus.created_at
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Idempotency & Deduplication
|
||||||
|
|
||||||
|
### Idempotency Keys
|
||||||
|
|
||||||
|
For bridge-synthesized events, use deterministic idempotency keys:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function generateIdempotencyKey(devicestatusId, eventType, subKey = '') {
|
||||||
|
return crypto
|
||||||
|
.createHash('sha256')
|
||||||
|
.update(`bridge:${devicestatusId}:${eventType}:${subKey}`)
|
||||||
|
.digest('hex')
|
||||||
|
.substring(0, 32);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deduplication Rules
|
||||||
|
|
||||||
|
1. **ProfileDefinition:** Dedupe by `contentHash` — same hash = same profile
|
||||||
|
2. **OverrideInstance:** Dedupe by `(start, effectiveEffects hash)` within tolerance window
|
||||||
|
3. **PolicyComposition:** Allow duplicates (they're snapshots), but optimize storage
|
||||||
|
4. **DeliveryObservation:** Dedupe by `(startTime, type, units/rate)` within 30-second window
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## State Management
|
||||||
|
|
||||||
|
The bridge processor maintains per-device state:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const deviceState = {
|
||||||
|
deviceId: string,
|
||||||
|
lastDevicestatusId: string,
|
||||||
|
lastProcessedAt: datetime,
|
||||||
|
|
||||||
|
// Profile state
|
||||||
|
currentProfileId: string,
|
||||||
|
currentProfileHash: string,
|
||||||
|
profileHashes: Set<string>, // All known profile hashes
|
||||||
|
|
||||||
|
// Override state
|
||||||
|
activeOverrides: [{
|
||||||
|
instanceId: string,
|
||||||
|
type: string,
|
||||||
|
startTime: datetime,
|
||||||
|
effectsHash: string
|
||||||
|
}],
|
||||||
|
|
||||||
|
// Capability state
|
||||||
|
latestCapabilitySnapshotId: string,
|
||||||
|
|
||||||
|
// Sequence tracking
|
||||||
|
issuerSeq: number
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Parsing Failures
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function processBridgeWithFallback(devicestatus) {
|
||||||
|
try {
|
||||||
|
const events = processDevicestatus(devicestatus);
|
||||||
|
return { success: true, events };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Bridge parsing error:', error);
|
||||||
|
|
||||||
|
// Emit a minimal PolicyComposition with error annotation
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
events: [{
|
||||||
|
eventType: 'policy.composition.computed',
|
||||||
|
payload: {
|
||||||
|
compositionId: generateUUID(),
|
||||||
|
references: {},
|
||||||
|
effectiveParameters: {},
|
||||||
|
computedBy: {
|
||||||
|
controllerKind: 'unknown',
|
||||||
|
computedAt: new Date().toISOString()
|
||||||
|
},
|
||||||
|
validFrom: devicestatus.created_at
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
bridgeError: error.message,
|
||||||
|
bridgeSource: 'devicestatus',
|
||||||
|
bridgeSourceId: devicestatus._id
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Missing Fields
|
||||||
|
|
||||||
|
Handle gracefully with defaults:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const DEFAULTS = {
|
||||||
|
basalMultiplier: 1.0,
|
||||||
|
sensitivityMultiplier: 1.0,
|
||||||
|
automationEnabled: true,
|
||||||
|
confidence: 'inferred'
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Test Cases
|
||||||
|
|
||||||
|
1. **Profile change detection** — Same profile should not emit new definition
|
||||||
|
2. **Override start/end** — Correctly detect transitions
|
||||||
|
3. **Override supersession** — Detect when override changes while active
|
||||||
|
4. **Delivery extraction** — Parse all enacted formats
|
||||||
|
5. **Clock skew** — Handle out-of-order devicestatus
|
||||||
|
6. **Idempotency** — Retry same devicestatus produces no duplicates
|
||||||
|
7. **Controller-specific parsing** — Each controller type parsed correctly
|
||||||
|
|
||||||
|
### Test Data
|
||||||
|
|
||||||
|
See `tests/bridge/fixtures/` for sample devicestatus payloads from each controller.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const BRIDGE_CONFIG = {
|
||||||
|
enabled: true,
|
||||||
|
|
||||||
|
// Which controllers to bridge
|
||||||
|
controllers: ['loop', 'openaps', 'aaps', 'trio'],
|
||||||
|
|
||||||
|
// Event emission
|
||||||
|
emitProfileDefinitions: true,
|
||||||
|
emitOverrideInstances: true,
|
||||||
|
emitPolicyCompositions: true,
|
||||||
|
emitDeliveryObservations: true,
|
||||||
|
emitCapabilitySnapshots: true,
|
||||||
|
|
||||||
|
// Deduplication
|
||||||
|
dedupeWindowSeconds: 30,
|
||||||
|
|
||||||
|
// State persistence
|
||||||
|
stateStorageCollection: 'bridgeState',
|
||||||
|
stateExpiryDays: 7,
|
||||||
|
|
||||||
|
// Error handling
|
||||||
|
continueOnParseError: true,
|
||||||
|
logParseErrors: true
|
||||||
|
};
|
||||||
|
```
|
||||||
@@ -0,0 +1,523 @@
|
|||||||
|
# Multi-Writer Semantics & Conflict Resolution
|
||||||
|
|
||||||
|
**Purpose:** Define rules for handling concurrent inputs from multiple sources (controllers, caregivers, agents, manual UI) to the Nightscout control plane.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The control plane accepts inputs from multiple writer types, each with different authority levels and use cases. This document defines how conflicts are detected, resolved, and audited.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Writer Types
|
||||||
|
|
||||||
|
### Primary Writers
|
||||||
|
|
||||||
|
| Writer Type | Description | Authority Level | Examples |
|
||||||
|
|-------------|-------------|-----------------|----------|
|
||||||
|
| **Human (Primary)** | The person with diabetes (PWD) or primary caregiver | Highest | User activating override in app |
|
||||||
|
| **Human (Caregiver)** | Delegated caregiver with explicit permissions | High | Parent adjusting child's settings remotely |
|
||||||
|
| **Agent** | AI/automated system with delegated authority | Medium | AI agent suggesting/activating sleep mode |
|
||||||
|
| **Controller** | AID algorithm on device | Base | Loop activating temp basal |
|
||||||
|
|
||||||
|
### Writer Identity
|
||||||
|
|
||||||
|
Each writer has a unique identity:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
IssuerIdentity:
|
||||||
|
issuerType: "human" | "controller" | "agent" | "caregiver" | "system"
|
||||||
|
issuerId: string # Unique identifier
|
||||||
|
authority: "primary" | "delegated" | "automated"
|
||||||
|
delegatedBy: string # If delegated, who granted authority
|
||||||
|
delegationScopes: [string] # What actions are permitted
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authority Hierarchy
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ HUMAN (PRIMARY) │ ← Can do anything
|
||||||
|
│ Authority Level: 100 │
|
||||||
|
└───────────────────┬─────────────────────┘
|
||||||
|
│
|
||||||
|
┌───────────────────▼─────────────────────┐
|
||||||
|
│ HUMAN (CAREGIVER) │ ← Delegated by primary
|
||||||
|
│ Authority Level: 80 │
|
||||||
|
└───────────────────┬─────────────────────┘
|
||||||
|
│
|
||||||
|
┌───────────────────▼─────────────────────┐
|
||||||
|
│ AGENT │ ← Delegated by primary/caregiver
|
||||||
|
│ Authority Level: 50 │
|
||||||
|
└───────────────────┬─────────────────────┘
|
||||||
|
│
|
||||||
|
┌───────────────────▼─────────────────────┐
|
||||||
|
│ CONTROLLER │ ← Automated, follows policy
|
||||||
|
│ Authority Level: 30 │
|
||||||
|
└─────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authority Rules
|
||||||
|
|
||||||
|
1. Higher authority can always override lower authority
|
||||||
|
2. Equal authority uses temporal precedence (last write wins)
|
||||||
|
3. Lower authority cannot override higher authority actions
|
||||||
|
4. Controller cannot override active human/agent overrides
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conflict Scenarios
|
||||||
|
|
||||||
|
### Scenario 1: Override Supersession
|
||||||
|
|
||||||
|
**Situation:** Human starts "Exercise" override, then Agent tries to start "High Activity" override.
|
||||||
|
|
||||||
|
**Resolution:**
|
||||||
|
```javascript
|
||||||
|
function canSupersede(newOverride, existingOverride) {
|
||||||
|
const newAuthority = getAuthorityLevel(newOverride.requestedBy);
|
||||||
|
const existingAuthority = getAuthorityLevel(existingOverride.requestedBy);
|
||||||
|
|
||||||
|
if (newAuthority > existingAuthority) {
|
||||||
|
return { allowed: true, action: 'supersede' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newAuthority === existingAuthority) {
|
||||||
|
// Same type? Supersede. Different type? May compose.
|
||||||
|
if (newOverride.type === existingOverride.type) {
|
||||||
|
return { allowed: true, action: 'supersede' };
|
||||||
|
} else {
|
||||||
|
return { allowed: true, action: 'compose' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lower authority cannot supersede
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
reason: 'insufficient_authority',
|
||||||
|
requiredAuthority: existingAuthority
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario 2: Concurrent Override Composition
|
||||||
|
|
||||||
|
**Situation:** Two different override types are active simultaneously.
|
||||||
|
|
||||||
|
**Resolution:** Compose effects with conservative (safe) combination:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function composeOverrides(overrides) {
|
||||||
|
// Sort by authority (highest first), then by start time
|
||||||
|
const sorted = sortBy(overrides, ['-authority', 'start']);
|
||||||
|
|
||||||
|
const composed = {
|
||||||
|
targetRange: null,
|
||||||
|
basalMultiplier: 1.0,
|
||||||
|
sensitivityMultiplier: 1.0,
|
||||||
|
carbRatioMultiplier: 1.0,
|
||||||
|
maxBasalCeiling: Infinity
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const override of sorted) {
|
||||||
|
const effects = override.effectiveEffects;
|
||||||
|
|
||||||
|
// Target: Use most restrictive (highest low, lowest high)
|
||||||
|
if (effects.targetRange) {
|
||||||
|
if (!composed.targetRange) {
|
||||||
|
composed.targetRange = { ...effects.targetRange };
|
||||||
|
} else {
|
||||||
|
composed.targetRange.low = Math.max(
|
||||||
|
composed.targetRange.low,
|
||||||
|
effects.targetRange.low
|
||||||
|
);
|
||||||
|
composed.targetRange.high = Math.min(
|
||||||
|
composed.targetRange.high,
|
||||||
|
effects.targetRange.high
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basal multiplier: Use lowest (most conservative)
|
||||||
|
if (effects.basalMultiplier !== undefined) {
|
||||||
|
composed.basalMultiplier = Math.min(
|
||||||
|
composed.basalMultiplier,
|
||||||
|
effects.basalMultiplier
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max basal ceiling: Use lowest
|
||||||
|
if (effects.maxBasalCeiling !== undefined) {
|
||||||
|
composed.maxBasalCeiling = Math.min(
|
||||||
|
composed.maxBasalCeiling,
|
||||||
|
effects.maxBasalCeiling
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sensitivity: Compound multiply
|
||||||
|
if (effects.sensitivityMultiplier !== undefined) {
|
||||||
|
composed.sensitivityMultiplier *= effects.sensitivityMultiplier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return composed;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario 3: Profile Switch During Active Override
|
||||||
|
|
||||||
|
**Situation:** Human switches profile while override is active.
|
||||||
|
|
||||||
|
**Resolution:**
|
||||||
|
- Profile switch proceeds
|
||||||
|
- Override remains active
|
||||||
|
- PolicyComposition recalculated with new profile + existing override
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function handleProfileSwitch(newProfileSelection, activeOverrides) {
|
||||||
|
// Profile switch always allowed at appropriate authority level
|
||||||
|
emitEvent('profile.selection.changed', newProfileSelection);
|
||||||
|
|
||||||
|
// Recalculate composition with new profile and existing overrides
|
||||||
|
const composition = computePolicyComposition(
|
||||||
|
newProfileSelection.selectedProfileId,
|
||||||
|
activeOverrides
|
||||||
|
);
|
||||||
|
|
||||||
|
emitEvent('policy.composition.computed', composition);
|
||||||
|
|
||||||
|
// Overrides remain active - their effects are relative to new profile
|
||||||
|
return composition;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario 4: Agent Flip-Flop Prevention
|
||||||
|
|
||||||
|
**Situation:** Agent activates/deactivates same override repeatedly.
|
||||||
|
|
||||||
|
**Resolution:** Rate limiting and cooldown periods.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const FLIP_FLOP_CONFIG = {
|
||||||
|
maxActivationsPerHour: 4,
|
||||||
|
cooldownAfterEnd: 15 * 60, // 15 minutes in seconds
|
||||||
|
requireConfirmationAfter: 2 // After 2 activations, require human confirmation
|
||||||
|
};
|
||||||
|
|
||||||
|
function checkFlipFlopLimits(agentId, overrideType) {
|
||||||
|
const recentActivations = getRecentActivations(agentId, overrideType, 3600);
|
||||||
|
|
||||||
|
if (recentActivations.length >= FLIP_FLOP_CONFIG.maxActivationsPerHour) {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
reason: 'rate_limit_exceeded',
|
||||||
|
retryAfter: calculateRetryTime(recentActivations)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastEnd = getLastOverrideEnd(agentId, overrideType);
|
||||||
|
if (lastEnd) {
|
||||||
|
const cooldownRemaining = FLIP_FLOP_CONFIG.cooldownAfterEnd -
|
||||||
|
(Date.now() - lastEnd) / 1000;
|
||||||
|
|
||||||
|
if (cooldownRemaining > 0) {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
reason: 'cooldown_active',
|
||||||
|
retryAfter: cooldownRemaining
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recentActivations.length >= FLIP_FLOP_CONFIG.requireConfirmationAfter) {
|
||||||
|
return {
|
||||||
|
allowed: 'pending_confirmation',
|
||||||
|
reason: 'requires_human_confirmation',
|
||||||
|
confirmationRequest: createConfirmationRequest(agentId, overrideType)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario 5: Controller vs. Human Override Interaction
|
||||||
|
|
||||||
|
**Situation:** Human has active override; controller tries to adjust.
|
||||||
|
|
||||||
|
**Resolution:** Controller respects human override; can only work within its bounds.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function controllerCanAdjust(controllerRequest, activeHumanOverride) {
|
||||||
|
// Controller cannot:
|
||||||
|
// - End human-initiated override
|
||||||
|
// - Exceed limits set by human override
|
||||||
|
// - Change target outside human-set range
|
||||||
|
|
||||||
|
if (activeHumanOverride) {
|
||||||
|
// Controller works within bounds
|
||||||
|
return {
|
||||||
|
allowed: true,
|
||||||
|
constraints: {
|
||||||
|
maxBasal: Math.min(
|
||||||
|
controllerRequest.maxBasal,
|
||||||
|
activeHumanOverride.effectiveEffects.maxBasalCeiling || Infinity
|
||||||
|
),
|
||||||
|
targetRange: activeHumanOverride.effectiveEffects.targetRange,
|
||||||
|
// Controller can micro-adjust within these bounds
|
||||||
|
canAdjustBasal: true,
|
||||||
|
canBolus: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allowed: true, constraints: null };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Concurrency Control
|
||||||
|
|
||||||
|
### Optimistic Locking
|
||||||
|
|
||||||
|
For updates to existing objects:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function updateWithOptimisticLock(collection, id, update, expectedVersion) {
|
||||||
|
const result = await db.collection(collection).findOneAndUpdate(
|
||||||
|
{
|
||||||
|
_id: id,
|
||||||
|
srvModified: expectedVersion
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$set: update,
|
||||||
|
$inc: { version: 1 }
|
||||||
|
},
|
||||||
|
{ returnDocument: 'after' }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.value) {
|
||||||
|
throw new ConflictError('Version mismatch - document was modified');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Ordering
|
||||||
|
|
||||||
|
All events receive a monotonic cursor for global ordering:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function assignEventCursor(event) {
|
||||||
|
// Atomic increment of global cursor
|
||||||
|
const counter = await db.collection('eventCursors').findOneAndUpdate(
|
||||||
|
{ _id: 'global' },
|
||||||
|
{ $inc: { cursor: 1 } },
|
||||||
|
{ upsert: true, returnDocument: 'after' }
|
||||||
|
);
|
||||||
|
|
||||||
|
event.cursor = counter.value.cursor;
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Audit Trail
|
||||||
|
|
||||||
|
### Conflict Events
|
||||||
|
|
||||||
|
When conflicts are detected and resolved, emit audit events:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function emitConflictResolution(conflict) {
|
||||||
|
return {
|
||||||
|
eventType: 'conflict.resolved',
|
||||||
|
payload: {
|
||||||
|
conflictId: generateUUID(),
|
||||||
|
conflictType: conflict.type,
|
||||||
|
participants: conflict.participants.map(p => ({
|
||||||
|
issuerId: p.issuerId,
|
||||||
|
issuerType: p.issuerType,
|
||||||
|
authority: p.authority,
|
||||||
|
action: p.action,
|
||||||
|
timestamp: p.timestamp
|
||||||
|
})),
|
||||||
|
resolution: {
|
||||||
|
outcome: conflict.resolution.outcome,
|
||||||
|
winner: conflict.resolution.winner,
|
||||||
|
reason: conflict.resolution.reason,
|
||||||
|
appliedAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authority Escalation
|
||||||
|
|
||||||
|
When lower authority is blocked:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function emitAuthorityBlock(request, blocker) {
|
||||||
|
return {
|
||||||
|
eventType: 'authority.blocked',
|
||||||
|
payload: {
|
||||||
|
requestedAction: request.action,
|
||||||
|
requestedBy: request.issuer,
|
||||||
|
requestedAuthority: request.authority,
|
||||||
|
blockedBy: blocker.instanceId,
|
||||||
|
blockerAuthority: blocker.authority,
|
||||||
|
reason: 'insufficient_authority',
|
||||||
|
requiredAuthority: blocker.authority,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Delegation Grants
|
||||||
|
|
||||||
|
### Grant Structure
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const delegationGrant = {
|
||||||
|
grantId: 'uuid',
|
||||||
|
grantedBy: 'human-user-id',
|
||||||
|
grantedTo: 'agent-id',
|
||||||
|
|
||||||
|
scopes: [
|
||||||
|
'override.activate:exercise',
|
||||||
|
'override.activate:sleep',
|
||||||
|
'override.suggest:*'
|
||||||
|
],
|
||||||
|
|
||||||
|
constraints: {
|
||||||
|
maxOverrideDuration: 4 * 3600, // 4 hours max
|
||||||
|
allowedOverrideTypes: ['exercise', 'sleep', 'preMeal'],
|
||||||
|
requireConfirmation: false,
|
||||||
|
maxActivationsPerDay: 6,
|
||||||
|
validTimeWindows: [
|
||||||
|
{ start: '06:00', end: '22:00' } // Only during waking hours
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
grantedAt: '2026-01-01T00:00:00Z',
|
||||||
|
expiresAt: '2026-12-31T23:59:59Z',
|
||||||
|
revokedAt: null
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Grant Validation
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function validateDelegation(action, agent, grants) {
|
||||||
|
const applicableGrants = grants.filter(g =>
|
||||||
|
g.grantedTo === agent.issuerId &&
|
||||||
|
!g.revokedAt &&
|
||||||
|
new Date() < new Date(g.expiresAt)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const grant of applicableGrants) {
|
||||||
|
if (!scopeMatches(action, grant.scopes)) continue;
|
||||||
|
if (!withinConstraints(action, grant.constraints)) continue;
|
||||||
|
if (!withinTimeWindow(grant.constraints.validTimeWindows)) continue;
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
grantId: grant.grantId,
|
||||||
|
effectiveAuthority: 'delegated'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
reason: 'no_valid_delegation',
|
||||||
|
availableGrants: applicableGrants.length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Safety Invariants
|
||||||
|
|
||||||
|
### Never Violated
|
||||||
|
|
||||||
|
1. **Human always wins** — Human-initiated action cannot be blocked by lower authority
|
||||||
|
2. **Safety limits respected** — Composed effects never exceed safety limits
|
||||||
|
3. **Conservative composition** — When in doubt, choose the safer option
|
||||||
|
4. **Audit everything** — All conflict resolutions are logged
|
||||||
|
5. **Explicit revocation** — Delegations must be explicitly revoked
|
||||||
|
|
||||||
|
### Validated on Every Write
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function validateSafetyInvariants(proposedState) {
|
||||||
|
const checks = [
|
||||||
|
checkTargetRangeValid(proposedState.effectiveTarget),
|
||||||
|
checkBasalWithinLimits(proposedState.effectiveBasal),
|
||||||
|
checkIOBWithinLimits(proposedState.currentIOB),
|
||||||
|
checkNoConflictingOverrides(proposedState.activeOverrides),
|
||||||
|
checkAuthorityHierarchy(proposedState.recentActions)
|
||||||
|
];
|
||||||
|
|
||||||
|
const violations = checks.filter(c => !c.valid);
|
||||||
|
|
||||||
|
if (violations.length > 0) {
|
||||||
|
throw new SafetyViolationError(violations);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const CONFLICT_RESOLUTION_CONFIG = {
|
||||||
|
// Authority levels
|
||||||
|
authorityLevels: {
|
||||||
|
'human-primary': 100,
|
||||||
|
'human-caregiver': 80,
|
||||||
|
'agent': 50,
|
||||||
|
'controller': 30,
|
||||||
|
'system': 10
|
||||||
|
},
|
||||||
|
|
||||||
|
// Composition strategy
|
||||||
|
compositionStrategy: 'conservative', // or 'permissive'
|
||||||
|
|
||||||
|
// Rate limiting
|
||||||
|
rateLimits: {
|
||||||
|
agent: {
|
||||||
|
overrideActivationsPerHour: 4,
|
||||||
|
profileSwitchesPerDay: 6
|
||||||
|
},
|
||||||
|
controller: {
|
||||||
|
// Controllers are not rate-limited (they self-regulate)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Cooldowns
|
||||||
|
cooldowns: {
|
||||||
|
overrideReactivation: 900, // 15 minutes
|
||||||
|
profileSwitch: 300 // 5 minutes
|
||||||
|
},
|
||||||
|
|
||||||
|
// Confirmation requirements
|
||||||
|
confirmationRequired: {
|
||||||
|
agentAfterNActivations: 2,
|
||||||
|
highRiskActions: ['override.illness', 'profile.switch']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
# Integration Questionnaire for Loop/AAPS/Trio Implementers
|
||||||
|
|
||||||
|
**Purpose:** This questionnaire helps determine how well existing AID controllers can support the proposed event-driven control plane architecture. Answers will inform bridge mode implementation and native event adoption roadmaps.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## A) Profiles & Overrides Semantics
|
||||||
|
|
||||||
|
### A1. Profile Identification
|
||||||
|
**Question:** Do you have stable identifiers for a profile (beyond just the name)?
|
||||||
|
|
||||||
|
| Controller | Current State | Can Emit Content Hash? |
|
||||||
|
|------------|--------------|----------------------|
|
||||||
|
| Loop | | |
|
||||||
|
| Trio | | |
|
||||||
|
| AAPS | | |
|
||||||
|
| OpenAPS | | |
|
||||||
|
|
||||||
|
**Follow-up:** If not stable IDs, can you emit a content hash of the profile?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### A2. Override Templates vs. Activations
|
||||||
|
**Question:** Can you represent overrides as separate concepts?
|
||||||
|
- **Template/Definition:** The reusable preset (e.g., "Exercise mode - 140 target, 70% basal")
|
||||||
|
- **Instance/Activation:** A concrete use of that template
|
||||||
|
|
||||||
|
| Controller | Has Templates? | Templates Persisted? | Can Separate? |
|
||||||
|
|------------|---------------|---------------------|---------------|
|
||||||
|
| Loop | | | |
|
||||||
|
| Trio | | | |
|
||||||
|
| AAPS | | | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### A3. Override Dimensions
|
||||||
|
**Question:** What override adjustment dimensions exist in your controller?
|
||||||
|
|
||||||
|
| Dimension | Loop | Trio | AAPS | OpenAPS |
|
||||||
|
|-----------|------|------|------|---------|
|
||||||
|
| Target range (min/max) | | | | |
|
||||||
|
| Single target value | | | | |
|
||||||
|
| Sensitivity multiplier | | | | |
|
||||||
|
| Basal multiplier/percentage | | | | |
|
||||||
|
| Max basal ceiling | | | | |
|
||||||
|
| Carb ratio multiplier | | | | |
|
||||||
|
| Automation aggressiveness | | | | |
|
||||||
|
| Duration (fixed/indefinite) | | | | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### A4. Multiple Override Resolution
|
||||||
|
**Question:** How do you resolve multiple concurrent overrides?
|
||||||
|
|
||||||
|
- [ ] Only one override can be active at a time
|
||||||
|
- [ ] Overrides stack/compose with precedence rules
|
||||||
|
- [ ] Most recent override wins
|
||||||
|
- [ ] User must explicitly end one before starting another
|
||||||
|
- [ ] Other: _____________
|
||||||
|
|
||||||
|
**Precedence rules (if applicable):**
|
||||||
|
```
|
||||||
|
Describe how conflicts are resolved...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## B) Policy Composition
|
||||||
|
|
||||||
|
### B5. Effective Parameters Computation
|
||||||
|
**Question:** Do you compute an explicit "effective therapy settings right now" object internally?
|
||||||
|
|
||||||
|
| Controller | Has Explicit Object? | Fields Included |
|
||||||
|
|------------|---------------------|-----------------|
|
||||||
|
| Loop | | |
|
||||||
|
| Trio | | |
|
||||||
|
| AAPS | | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B6. Composition Emission
|
||||||
|
**Question:** Can you emit a composition snapshot containing:
|
||||||
|
|
||||||
|
| Field | Loop | Trio | AAPS | Notes |
|
||||||
|
|-------|------|------|------|-------|
|
||||||
|
| Profile hash/ID | | | | |
|
||||||
|
| Active override IDs | | | | |
|
||||||
|
| Effective target range | | | | |
|
||||||
|
| Effective ISF (current) | | | | |
|
||||||
|
| Effective CR (current) | | | | |
|
||||||
|
| Effective basal (current) | | | | |
|
||||||
|
| Safety limits in force | | | | |
|
||||||
|
| Controller version/build | | | | |
|
||||||
|
|
||||||
|
**If "yes" to most fields:** Nightscout's digital twin becomes reliable without simulating the algorithm.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## C) Delivery Fidelity
|
||||||
|
|
||||||
|
### C7. Delivery State Distinction
|
||||||
|
**Question:** Can you distinguish clearly between:
|
||||||
|
|
||||||
|
| State | Loop | Trio | AAPS | Where Reported |
|
||||||
|
|-------|------|------|------|----------------|
|
||||||
|
| **Suggested/recommended** (algorithm output) | | | | |
|
||||||
|
| **Requested** (command sent to pump) | | | | |
|
||||||
|
| **Confirmed/enacted** (pump acknowledged) | | | | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### C8. Pump Response Codes
|
||||||
|
**Question:** Do you have pump ACK/NAK/error codes that can be surfaced?
|
||||||
|
|
||||||
|
| Controller | Has Error Codes | Examples | Can Expose? |
|
||||||
|
|------------|----------------|----------|-------------|
|
||||||
|
| Loop | | | |
|
||||||
|
| Trio | | | |
|
||||||
|
| AAPS | | | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### C9. Failure Reason Distinction
|
||||||
|
**Question:** How do you represent different failure modes?
|
||||||
|
|
||||||
|
| Failure Type | Loop | Trio | AAPS |
|
||||||
|
|-------------|------|------|------|
|
||||||
|
| Capped by max basal limit | | | |
|
||||||
|
| Capped by max IOB | | | |
|
||||||
|
| Communication failure with pump | | | |
|
||||||
|
| Pump busy/unavailable | | | |
|
||||||
|
| User canceled | | | |
|
||||||
|
| Pump error (occlusion, etc.) | | | |
|
||||||
|
|
||||||
|
**Current representation format:** (e.g., error codes, status strings, flags)
|
||||||
|
```
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## D) Timing and Ordering
|
||||||
|
|
||||||
|
### D10. Monotonic Ordering
|
||||||
|
**Question:** Can you provide monotonic ordering per controller?
|
||||||
|
|
||||||
|
| Controller | Has Sequence Numbers | Clock Guarantees | Ordering Method |
|
||||||
|
|------------|---------------------|------------------|-----------------|
|
||||||
|
| Loop | | | |
|
||||||
|
| Trio | | | |
|
||||||
|
| AAPS | | | |
|
||||||
|
|
||||||
|
**If no sequence numbers:** What clock/timing guarantees exist?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### D11. Offline Batching
|
||||||
|
**Question:** How do you handle offline operation and delayed uploads?
|
||||||
|
|
||||||
|
| Scenario | Loop | Trio | AAPS |
|
||||||
|
|----------|------|------|------|
|
||||||
|
| Queue events when offline | | | |
|
||||||
|
| Batch upload on reconnect | | | |
|
||||||
|
| Preserve ordering in batch | | | |
|
||||||
|
| Mark events as "delayed" | | | |
|
||||||
|
| Dedup on retry | | | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## E) Minimal Commitment for Phase 1
|
||||||
|
|
||||||
|
### E12. Smallest Native Event Set
|
||||||
|
**Question:** What is the smallest set of native events you're willing to emit first?
|
||||||
|
|
||||||
|
**Proposed minimal set for Phase 1:**
|
||||||
|
|
||||||
|
| Event | Priority | Loop | Trio | AAPS |
|
||||||
|
|-------|----------|------|------|------|
|
||||||
|
| `override.instance.activated` | High | | | |
|
||||||
|
| `override.instance.ended` | High | | | |
|
||||||
|
| `policy.composition.computed` | High | | | |
|
||||||
|
| `delivery.observed` (summary) | Medium | | | |
|
||||||
|
| `profile.selection.changed` | Medium | | | |
|
||||||
|
| `capability.snapshot.updated` | Low | | | |
|
||||||
|
|
||||||
|
**Your minimal commitment:**
|
||||||
|
```
|
||||||
|
List the events you can commit to emitting...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## F) Additional Capabilities
|
||||||
|
|
||||||
|
### F13. Remote Commands
|
||||||
|
**Question:** Can your controller accept commands from Nightscout?
|
||||||
|
|
||||||
|
| Command Type | Loop | Trio | AAPS | Security Model |
|
||||||
|
|-------------|------|------|------|----------------|
|
||||||
|
| Activate override | | | | |
|
||||||
|
| End override | | | | |
|
||||||
|
| Switch profile | | | | |
|
||||||
|
| Set temp target | | | | |
|
||||||
|
| Bolus (caregiving) | | | | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### F14. Controller Registration
|
||||||
|
**Question:** Can you emit controller registration events?
|
||||||
|
|
||||||
|
| Field | Loop | Trio | AAPS |
|
||||||
|
|-------|------|------|------|
|
||||||
|
| Unique instance ID | | | |
|
||||||
|
| Device info | | | |
|
||||||
|
| Pump binding info | | | |
|
||||||
|
| CGM binding info | | | |
|
||||||
|
| Version/build | | | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## G) Migration Path
|
||||||
|
|
||||||
|
### G15. Adoption Timeline
|
||||||
|
**Question:** What's your estimated timeline for native event emission?
|
||||||
|
|
||||||
|
| Phase | Loop | Trio | AAPS |
|
||||||
|
|-------|------|------|------|
|
||||||
|
| Can test in dev builds | | | |
|
||||||
|
| Can ship in beta | | | |
|
||||||
|
| Can ship in stable | | | |
|
||||||
|
| Full native (no devicestatus) | | | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### G16. Bridge Mode Feedback
|
||||||
|
**Question:** What concerns do you have about Nightscout synthesizing events from your devicestatus uploads?
|
||||||
|
|
||||||
|
- [ ] Accuracy of override state extraction
|
||||||
|
- [ ] Accuracy of delivery extraction
|
||||||
|
- [ ] Missing fields in current devicestatus
|
||||||
|
- [ ] Timing/ordering issues
|
||||||
|
- [ ] Duplicate events
|
||||||
|
- [ ] Other: _____________
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How to Submit
|
||||||
|
|
||||||
|
Please fill out this questionnaire and submit via:
|
||||||
|
|
||||||
|
1. **GitHub Issue** on the Nightscout cgm-remote-monitor repository with label `agentic-control-plane`
|
||||||
|
2. **Pull Request** adding your controller's responses to this file
|
||||||
|
3. **Discord** in the #development channel
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
Questions about this questionnaire:
|
||||||
|
- Nightscout Discord: [link]
|
||||||
|
- GitHub Discussions: [link]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Responses
|
||||||
|
|
||||||
|
### Loop
|
||||||
|
*Status: Pending*
|
||||||
|
|
||||||
|
### Trio
|
||||||
|
*Status: Pending*
|
||||||
|
|
||||||
|
### AAPS
|
||||||
|
*Status: Pending*
|
||||||
|
|
||||||
|
### OpenAPS
|
||||||
|
*Status: Pending*
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://nightscout.github.io/schemas/capability-snapshot.schema.json",
|
||||||
|
"title": "CapabilitySnapshot",
|
||||||
|
"description": "Point-in-time snapshot of what the controller can actually do right now",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["snapshotId", "controllerInstanceId", "snapshotAt"],
|
||||||
|
"properties": {
|
||||||
|
"snapshotId": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"description": "Unique identifier for this snapshot"
|
||||||
|
},
|
||||||
|
"controllerInstanceId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Reference to ControllerInstanceRegistration"
|
||||||
|
},
|
||||||
|
"connectivity": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Current connectivity status",
|
||||||
|
"properties": {
|
||||||
|
"pumpConnected": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Is pump currently connected"
|
||||||
|
},
|
||||||
|
"pumpLastContact": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "Last successful communication with pump"
|
||||||
|
},
|
||||||
|
"pumpRSSI": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Pump radio signal strength"
|
||||||
|
},
|
||||||
|
"cgmConnected": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Is CGM currently connected"
|
||||||
|
},
|
||||||
|
"cgmLastReading": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "Timestamp of last CGM reading"
|
||||||
|
},
|
||||||
|
"internetConnected": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Is device online"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"automationState": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Current automation status",
|
||||||
|
"properties": {
|
||||||
|
"closedLoopEnabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Is closed-loop automation active"
|
||||||
|
},
|
||||||
|
"suspended": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Is insulin delivery suspended"
|
||||||
|
},
|
||||||
|
"suspendReason": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Why suspended (if applicable)"
|
||||||
|
},
|
||||||
|
"lastLoopTime": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "When loop algorithm last ran"
|
||||||
|
},
|
||||||
|
"lastEnactTime": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "When last delivery was enacted"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"effectiveLimits": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Current safety limits in effect",
|
||||||
|
"properties": {
|
||||||
|
"maxBasal": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Maximum basal rate allowed (U/hr)"
|
||||||
|
},
|
||||||
|
"maxBolus": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Maximum bolus allowed (U)"
|
||||||
|
},
|
||||||
|
"maxIOB": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Maximum IOB allowed (U)"
|
||||||
|
},
|
||||||
|
"minBG": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Minimum target BG"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"health": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Device health indicators",
|
||||||
|
"properties": {
|
||||||
|
"reservoirUnits": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Remaining insulin units in reservoir"
|
||||||
|
},
|
||||||
|
"reservoirLevel": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["full", "adequate", "low", "critical"],
|
||||||
|
"description": "Reservoir level category"
|
||||||
|
},
|
||||||
|
"batteryPercent": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 100,
|
||||||
|
"description": "Pump battery percentage"
|
||||||
|
},
|
||||||
|
"phoneBatteryPercent": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 100,
|
||||||
|
"description": "Controller device battery"
|
||||||
|
},
|
||||||
|
"podAge": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Hours since pod was activated (for pod pumps)"
|
||||||
|
},
|
||||||
|
"podExpires": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "When pod expires"
|
||||||
|
},
|
||||||
|
"cgmCalibrationStatus": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["ok", "needed", "overdue", "na"],
|
||||||
|
"description": "CGM calibration status"
|
||||||
|
},
|
||||||
|
"sensorAge": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Hours since sensor was started"
|
||||||
|
},
|
||||||
|
"timeSyncHealth": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["good", "drift", "unknown"],
|
||||||
|
"description": "Clock synchronization status"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"degradedCapabilities": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Capabilities that are currently degraded or unavailable",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"capability": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Which capability is affected"
|
||||||
|
},
|
||||||
|
"reason": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Why it's degraded"
|
||||||
|
},
|
||||||
|
"since": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"snapshotAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "When this snapshot was taken"
|
||||||
|
},
|
||||||
|
"validFor": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "How long this snapshot is valid (seconds)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://nightscout.github.io/schemas/controller-instance-registration.schema.json",
|
||||||
|
"title": "ControllerInstanceRegistration",
|
||||||
|
"description": "Registration of a specific controller instance (this phone/device running right now)",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["instanceId", "kindId", "version", "device", "registeredAt"],
|
||||||
|
"properties": {
|
||||||
|
"instanceId": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"description": "Unique identifier for this controller instance"
|
||||||
|
},
|
||||||
|
"kindId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Reference to ControllerKindDefinition"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Specific version/build of the controller app"
|
||||||
|
},
|
||||||
|
"device": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["deviceId", "platform"],
|
||||||
|
"description": "Device information",
|
||||||
|
"properties": {
|
||||||
|
"deviceId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Unique device identifier"
|
||||||
|
},
|
||||||
|
"platform": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["ios", "android", "linux", "macos", "windows"],
|
||||||
|
"description": "Operating system platform"
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Device model (iPhone 14, Pixel 7, etc.)"
|
||||||
|
},
|
||||||
|
"osVersion": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "OS version"
|
||||||
|
},
|
||||||
|
"appBuild": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "App build number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pumpBinding": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Currently bound pump",
|
||||||
|
"properties": {
|
||||||
|
"pumpKind": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Type of pump (omnipod, omnipod-dash, medtronic-x23, etc.)"
|
||||||
|
},
|
||||||
|
"pumpSerial": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Pump serial number (may be partial/hashed)"
|
||||||
|
},
|
||||||
|
"pumpFirmware": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Pump firmware version"
|
||||||
|
},
|
||||||
|
"connectedSince": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "When pump was connected"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cgmBinding": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Currently bound CGM",
|
||||||
|
"properties": {
|
||||||
|
"cgmKind": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Type of CGM (dexcom-g6, dexcom-g7, libre-3, etc.)"
|
||||||
|
},
|
||||||
|
"cgmId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "CGM identifier (transmitter ID, sensor ID)"
|
||||||
|
},
|
||||||
|
"cgmFirmware": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"sensorStarted": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"sensorExpires": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"registeredAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "When this instance was first registered"
|
||||||
|
},
|
||||||
|
"lastSeenAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "When this instance last communicated"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["active", "inactive", "deregistered"],
|
||||||
|
"description": "Current status of this registration"
|
||||||
|
},
|
||||||
|
"deregisteredAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://nightscout.github.io/schemas/controller-kind-definition.schema.json",
|
||||||
|
"title": "ControllerKindDefinition",
|
||||||
|
"description": "Declared capabilities and schema for a type of AID controller (Loop, Trio, AAPS, etc.)",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["kindId", "name", "version"],
|
||||||
|
"properties": {
|
||||||
|
"kindId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Unique identifier for this controller type",
|
||||||
|
"examples": ["loop", "trio", "aaps", "openaps", "tidepool-loop"]
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Human-readable name"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Version of this controller definition"
|
||||||
|
},
|
||||||
|
"vendor": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Organization or project responsible"
|
||||||
|
},
|
||||||
|
"supportedFeatures": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Feature flags for this controller type",
|
||||||
|
"properties": {
|
||||||
|
"tempBasal": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Can set temporary basal rates"
|
||||||
|
},
|
||||||
|
"microBolus": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Uses micro-bolus (SMB) strategy"
|
||||||
|
},
|
||||||
|
"suspend": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Can suspend insulin delivery"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Supports override presets"
|
||||||
|
},
|
||||||
|
"autoSens": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Has autosensitivity detection"
|
||||||
|
},
|
||||||
|
"dynamicISF": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Supports dynamic ISF adjustments"
|
||||||
|
},
|
||||||
|
"dynamicCR": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Supports dynamic carb ratio adjustments"
|
||||||
|
},
|
||||||
|
"smbWithCOB": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Allows SMB when carbs on board"
|
||||||
|
},
|
||||||
|
"uam": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Supports unannounced meals detection"
|
||||||
|
},
|
||||||
|
"profileSwitching": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Supports switching between profiles"
|
||||||
|
},
|
||||||
|
"remoteCommands": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Can accept remote commands"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"supportedPumps": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"description": "List of compatible pump types"
|
||||||
|
},
|
||||||
|
"supportedCGMs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"description": "List of compatible CGM types"
|
||||||
|
},
|
||||||
|
"algorithmType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["oref0", "oref1", "dosing-decision", "custom"],
|
||||||
|
"description": "Core algorithm family"
|
||||||
|
},
|
||||||
|
"eventCapabilities": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "What events this controller can emit",
|
||||||
|
"properties": {
|
||||||
|
"canEmitNativeEvents": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether it can emit canonical events directly"
|
||||||
|
},
|
||||||
|
"minimalEventSet": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"description": "Minimum set of event types it can emit"
|
||||||
|
},
|
||||||
|
"deviceStatusFields": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"description": "Fields available in devicestatus uploads"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"documentation": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri",
|
||||||
|
"description": "Link to controller documentation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://nightscout.github.io/schemas/delivery-observation.schema.json",
|
||||||
|
"title": "DeliveryObservation",
|
||||||
|
"description": "Observed/confirmed insulin delivery from any source (pump, pen, injection)",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["observationId", "observationType", "source", "observed", "reportedBy", "observedAt"],
|
||||||
|
"properties": {
|
||||||
|
"observationId": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"description": "Unique identifier for this observation"
|
||||||
|
},
|
||||||
|
"observationType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["basal", "tempBasal", "bolus", "microBolus", "suspend", "injection", "pen", "inhaler"],
|
||||||
|
"description": "Type of delivery observed"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["sourceType"],
|
||||||
|
"description": "Where this delivery came from",
|
||||||
|
"properties": {
|
||||||
|
"sourceType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["pump", "manual", "pen", "inhaler", "estimated"],
|
||||||
|
"description": "Category of delivery source"
|
||||||
|
},
|
||||||
|
"sourceId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Device identifier if from pump"
|
||||||
|
},
|
||||||
|
"sourceKind": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Specific device type (omnipod, medtronic, tandem, novopen, etc.)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"observed": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "What was actually delivered",
|
||||||
|
"properties": {
|
||||||
|
"rate": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Observed rate in U/hr (for basal)"
|
||||||
|
},
|
||||||
|
"units": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Observed units delivered"
|
||||||
|
},
|
||||||
|
"duration": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Duration in seconds (for temp basal)"
|
||||||
|
},
|
||||||
|
"startTime": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "When delivery started"
|
||||||
|
},
|
||||||
|
"endTime": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "When delivery ended (if completed)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"confidence": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["confirmed", "inferred", "reported", "estimated"],
|
||||||
|
"description": "Confidence level of this observation"
|
||||||
|
},
|
||||||
|
"reportedBy": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["issuerType", "issuerId"],
|
||||||
|
"properties": {
|
||||||
|
"issuerType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["human", "controller", "agent", "caregiver", "pump"]
|
||||||
|
},
|
||||||
|
"issuerId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"observedAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "When this observation was recorded"
|
||||||
|
},
|
||||||
|
"pumpResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Response from pump if applicable",
|
||||||
|
"properties": {
|
||||||
|
"acked": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether pump acknowledged the command"
|
||||||
|
},
|
||||||
|
"errorCode": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Error code if failed"
|
||||||
|
},
|
||||||
|
"errorMessage": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Human-readable error message"
|
||||||
|
},
|
||||||
|
"rawResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Raw pump response for debugging"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"relatedRequestId": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"format": "uuid",
|
||||||
|
"description": "DeliveryRequest this observation corresponds to, if any"
|
||||||
|
},
|
||||||
|
"annotations": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Additional context",
|
||||||
|
"properties": {
|
||||||
|
"notes": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Human-provided notes (e.g., 'correction for high')"
|
||||||
|
},
|
||||||
|
"mealContext": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["breakfast", "lunch", "dinner", "snack", "correction", "other"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://nightscout.github.io/schemas/delivery-request.schema.json",
|
||||||
|
"title": "DeliveryRequest",
|
||||||
|
"description": "Intent to deliver insulin - the command sent to the pump",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["requestId", "requestType", "requestedBy", "requestedAt"],
|
||||||
|
"properties": {
|
||||||
|
"requestId": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"description": "Unique identifier for this delivery request"
|
||||||
|
},
|
||||||
|
"requestType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["tempBasal", "scheduledBasal", "bolus", "microBolus", "suspend", "resume", "cancelTemp"],
|
||||||
|
"description": "Type of delivery action requested"
|
||||||
|
},
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Request parameters (varies by requestType)",
|
||||||
|
"properties": {
|
||||||
|
"rate": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Rate in U/hr for basal requests"
|
||||||
|
},
|
||||||
|
"units": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Units for bolus requests"
|
||||||
|
},
|
||||||
|
"duration": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Duration in seconds for temp basal"
|
||||||
|
},
|
||||||
|
"bolusType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["normal", "square", "dual", "micro"],
|
||||||
|
"description": "Type of bolus if applicable"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"requestedBy": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["issuerType", "issuerId"],
|
||||||
|
"properties": {
|
||||||
|
"issuerType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["human", "controller", "agent", "caregiver"]
|
||||||
|
},
|
||||||
|
"issuerId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"basedOn": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "What informed this request",
|
||||||
|
"properties": {
|
||||||
|
"policyCompositionId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "PolicyComposition that was in force"
|
||||||
|
},
|
||||||
|
"algorithmSuggestion": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Algorithm output that led to this request",
|
||||||
|
"properties": {
|
||||||
|
"suggestedAt": { "type": "string", "format": "date-time" },
|
||||||
|
"reason": { "type": "string" },
|
||||||
|
"predictedBG": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "number" }
|
||||||
|
},
|
||||||
|
"iob": { "type": "number" },
|
||||||
|
"cob": { "type": "number" },
|
||||||
|
"eventualBG": { "type": "number" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"requestedAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "When this request was created"
|
||||||
|
},
|
||||||
|
"expiresAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "Request is stale/invalid after this time"
|
||||||
|
},
|
||||||
|
"priority": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["normal", "high", "urgent"],
|
||||||
|
"default": "normal",
|
||||||
|
"description": "Request priority for queuing"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://nightscout.github.io/schemas/event-envelope.schema.json",
|
||||||
|
"title": "EventEnvelope",
|
||||||
|
"description": "Wrapper for all control plane events, providing consistent ordering, audit, and replay capabilities",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["eventId", "eventType", "issuer", "timestamp", "payload"],
|
||||||
|
"properties": {
|
||||||
|
"eventId": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"description": "Stable UUID for this event"
|
||||||
|
},
|
||||||
|
"eventType": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Dot-notation event type",
|
||||||
|
"enum": [
|
||||||
|
"profile.definition.created",
|
||||||
|
"profile.definition.updated",
|
||||||
|
"profile.selection.changed",
|
||||||
|
"override.definition.created",
|
||||||
|
"override.definition.updated",
|
||||||
|
"override.instance.activated",
|
||||||
|
"override.instance.ended",
|
||||||
|
"override.instance.canceled",
|
||||||
|
"override.instance.superseded",
|
||||||
|
"policy.composition.computed",
|
||||||
|
"delivery.requested",
|
||||||
|
"delivery.observed",
|
||||||
|
"delivery.reconciled",
|
||||||
|
"controller.registered",
|
||||||
|
"controller.deregistered",
|
||||||
|
"capability.snapshot.updated"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"cursor": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Server-assigned monotonic global ordering"
|
||||||
|
},
|
||||||
|
"issuer": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Identifier for the entity that issued this event (controller, user, agent)"
|
||||||
|
},
|
||||||
|
"issuerSeq": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Monotonic sequence number per issuer for ordering"
|
||||||
|
},
|
||||||
|
"idempotencyKey": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Client-provided key for retry deduplication"
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "ISO 8601 timestamp when event was created"
|
||||||
|
},
|
||||||
|
"refs": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "References to related objects",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["refType", "refId"],
|
||||||
|
"properties": {
|
||||||
|
"refType": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Type of referenced object",
|
||||||
|
"enum": [
|
||||||
|
"profileDefinition",
|
||||||
|
"profileSelection",
|
||||||
|
"overrideDefinition",
|
||||||
|
"overrideInstance",
|
||||||
|
"policyComposition",
|
||||||
|
"deliveryRequest",
|
||||||
|
"deliveryObservation",
|
||||||
|
"controllerInstance",
|
||||||
|
"capabilitySnapshot"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"refId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID or content hash of referenced object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"payload": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "The actual event data (schema depends on eventType)"
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Optional extensible metadata",
|
||||||
|
"properties": {
|
||||||
|
"bridgeSource": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "If synthesized from legacy data, the source collection"
|
||||||
|
},
|
||||||
|
"bridgeSourceId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "If synthesized, the source document ID"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://nightscout.github.io/schemas/override-definition.schema.json",
|
||||||
|
"title": "OverrideDefinition",
|
||||||
|
"description": "User-authored reusable template for therapy overrides (exercise, sleep, illness, etc.)",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["definitionId", "overrideType", "title", "createdBy", "createdAt"],
|
||||||
|
"properties": {
|
||||||
|
"definitionId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Unique identifier for this override definition"
|
||||||
|
},
|
||||||
|
"overrideType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["exercise", "sleep", "preMeal", "illness", "highActivity", "hormones", "stress", "travel", "custom"],
|
||||||
|
"description": "Category of override"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Human-readable name for this override"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 500,
|
||||||
|
"description": "Optional longer description of when to use this override"
|
||||||
|
},
|
||||||
|
"defaultDuration": {
|
||||||
|
"type": ["integer", "null"],
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Default duration in seconds; null means indefinite until manually ended"
|
||||||
|
},
|
||||||
|
"effects": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Therapy adjustments when this override is active",
|
||||||
|
"properties": {
|
||||||
|
"targetRange": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Absolute target range override",
|
||||||
|
"properties": {
|
||||||
|
"low": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 60,
|
||||||
|
"description": "Lower target bound in configured units"
|
||||||
|
},
|
||||||
|
"high": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 60,
|
||||||
|
"description": "Upper target bound in configured units"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"targetDelta": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Relative adjustment to existing target (positive = raise target)"
|
||||||
|
},
|
||||||
|
"basalMultiplier": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 5,
|
||||||
|
"description": "Multiplier for basal rate (1.0 = no change, 0.5 = 50%, 1.5 = 150%)"
|
||||||
|
},
|
||||||
|
"maxBasalCeiling": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Maximum basal rate cap in U/hr"
|
||||||
|
},
|
||||||
|
"sensitivityMultiplier": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0.1,
|
||||||
|
"maximum": 5,
|
||||||
|
"description": "Multiplier for ISF (>1 = more sensitive, <1 = more resistant)"
|
||||||
|
},
|
||||||
|
"carbRatioMultiplier": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0.1,
|
||||||
|
"maximum": 5,
|
||||||
|
"description": "Multiplier for carb ratio"
|
||||||
|
},
|
||||||
|
"automationAggressiveness": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 1,
|
||||||
|
"description": "Scaling factor for automation aggressiveness if controller supports"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"createdBy": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["issuerType", "issuerId"],
|
||||||
|
"properties": {
|
||||||
|
"issuerType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["human", "controller", "agent", "caregiver"]
|
||||||
|
},
|
||||||
|
"issuerId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"isArchived": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Soft delete - archived definitions cannot be instantiated"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://nightscout.github.io/schemas/override-instance.schema.json",
|
||||||
|
"title": "OverrideInstance",
|
||||||
|
"description": "A concrete activation of an override, representing runtime intent to modify therapy parameters",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["instanceId", "start", "status", "requestedBy"],
|
||||||
|
"properties": {
|
||||||
|
"instanceId": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"description": "Unique identifier for this override activation"
|
||||||
|
},
|
||||||
|
"definitionId": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"description": "Reference to OverrideDefinition if using a template; null for ad-hoc overrides"
|
||||||
|
},
|
||||||
|
"start": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "When this override becomes active"
|
||||||
|
},
|
||||||
|
"duration": {
|
||||||
|
"type": ["integer", "null"],
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Duration in seconds; null means indefinite"
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "When this override ends (computed from start+duration or explicit)"
|
||||||
|
},
|
||||||
|
"effectiveEffects": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Resolved effects for this instance (may differ from definition due to overrides)",
|
||||||
|
"properties": {
|
||||||
|
"targetRange": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"low": { "type": "number" },
|
||||||
|
"high": { "type": "number" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"basalMultiplier": { "type": "number" },
|
||||||
|
"sensitivityMultiplier": { "type": "number" },
|
||||||
|
"carbRatioMultiplier": { "type": "number" },
|
||||||
|
"maxBasalCeiling": { "type": "number" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"requestedBy": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["issuerType", "issuerId", "authority"],
|
||||||
|
"properties": {
|
||||||
|
"issuerType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["human", "controller", "agent", "caregiver"]
|
||||||
|
},
|
||||||
|
"issuerId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"authority": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["primary", "delegated", "automated"],
|
||||||
|
"description": "Authority level of the requester"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["pending", "active", "ended", "canceled", "superseded"],
|
||||||
|
"description": "Current status of this override instance"
|
||||||
|
},
|
||||||
|
"supersededBy": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"format": "uuid",
|
||||||
|
"description": "If superseded, the instanceId of the override that replaced this one"
|
||||||
|
},
|
||||||
|
"endedBy": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Who ended this override (if manually ended)",
|
||||||
|
"properties": {
|
||||||
|
"issuerType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["human", "controller", "agent", "caregiver"]
|
||||||
|
},
|
||||||
|
"issuerId": { "type": "string" },
|
||||||
|
"endedAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reason": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 500,
|
||||||
|
"description": "Why this override was activated"
|
||||||
|
},
|
||||||
|
"annotations": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Extensible metadata",
|
||||||
|
"properties": {
|
||||||
|
"externalContext": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "External data that informed this override",
|
||||||
|
"properties": {
|
||||||
|
"hormoneCycleDay": { "type": "integer" },
|
||||||
|
"activityLevel": { "type": "string" },
|
||||||
|
"geolocation": { "type": "string" },
|
||||||
|
"stressIndicator": { "type": "number" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"agentReasoning": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "If agent-initiated, explanation of reasoning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://nightscout.github.io/schemas/policy-composition.schema.json",
|
||||||
|
"title": "PolicyComposition",
|
||||||
|
"description": "Materialized view of effective therapy parameters computed from active profile and overrides",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["compositionId", "references", "effectiveParameters", "computedBy", "validFrom"],
|
||||||
|
"properties": {
|
||||||
|
"compositionId": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"description": "Unique identifier for this composition snapshot"
|
||||||
|
},
|
||||||
|
"references": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["profileId", "profileHash"],
|
||||||
|
"description": "References to source objects used in this composition",
|
||||||
|
"properties": {
|
||||||
|
"profileId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Active ProfileDefinition ID"
|
||||||
|
},
|
||||||
|
"profileHash": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-f0-9]{64}$",
|
||||||
|
"description": "Content hash of active profile for verification"
|
||||||
|
},
|
||||||
|
"activeOverrideInstanceIds": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"description": "Currently active OverrideInstance IDs"
|
||||||
|
},
|
||||||
|
"capabilitySnapshotId": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"description": "Reference to CapabilitySnapshot if available"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"effectiveParameters": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Computed therapy parameters currently in force",
|
||||||
|
"properties": {
|
||||||
|
"targetRange": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["low", "high"],
|
||||||
|
"properties": {
|
||||||
|
"low": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Effective lower target bound"
|
||||||
|
},
|
||||||
|
"high": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Effective upper target bound"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"effectiveISF": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Effective insulin sensitivity factor for current time"
|
||||||
|
},
|
||||||
|
"effectiveCR": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Effective carb ratio for current time"
|
||||||
|
},
|
||||||
|
"effectiveBasal": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Effective basal rate for current time (after multipliers)"
|
||||||
|
},
|
||||||
|
"scheduledBasal": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Scheduled basal rate from profile (before multipliers)"
|
||||||
|
},
|
||||||
|
"basalMultiplier": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Aggregate basal multiplier from overrides"
|
||||||
|
},
|
||||||
|
"sensitivityMultiplier": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Aggregate sensitivity multiplier from overrides"
|
||||||
|
},
|
||||||
|
"maxBasalAllowed": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Maximum basal rate allowed by constraints"
|
||||||
|
},
|
||||||
|
"maxBolusAllowed": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Maximum bolus allowed by constraints"
|
||||||
|
},
|
||||||
|
"maxIOB": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Maximum IOB allowed by constraints"
|
||||||
|
},
|
||||||
|
"automationEnabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether closed-loop automation is active"
|
||||||
|
},
|
||||||
|
"automationMode": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["closedLoop", "openLoop", "suspended", "manual"],
|
||||||
|
"description": "Current automation mode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"computedBy": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["computedAt"],
|
||||||
|
"properties": {
|
||||||
|
"controllerKind": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Controller type that computed this (loop, trio, aaps, nightscout)"
|
||||||
|
},
|
||||||
|
"controllerVersion": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Controller version"
|
||||||
|
},
|
||||||
|
"controllerInstanceId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Specific controller instance ID"
|
||||||
|
},
|
||||||
|
"computedAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "When this composition was computed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"validFrom": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "When this composition became valid"
|
||||||
|
},
|
||||||
|
"validTo": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "When this composition was superseded; null if current"
|
||||||
|
},
|
||||||
|
"cursor": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Global ordering cursor for event stream"
|
||||||
|
},
|
||||||
|
"previousCompositionId": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"description": "Previous composition for audit chain"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://nightscout.github.io/schemas/profile-definition.schema.json",
|
||||||
|
"title": "ProfileDefinition",
|
||||||
|
"description": "User-authored, versioned profile configuration containing basal rates, ISF, CR, and targets",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["profileId", "contentHash", "title", "timezone", "units", "schedules", "createdBy", "createdAt"],
|
||||||
|
"properties": {
|
||||||
|
"profileId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Stable unique identifier for this profile"
|
||||||
|
},
|
||||||
|
"contentHash": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-f0-9]{64}$",
|
||||||
|
"description": "SHA-256 hash of canonicalized profile content for deduplication and verification"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Human-readable profile name"
|
||||||
|
},
|
||||||
|
"timezone": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "IANA timezone identifier (e.g., 'America/New_York')"
|
||||||
|
},
|
||||||
|
"units": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["mg/dL", "mmol/L"],
|
||||||
|
"description": "Blood glucose units"
|
||||||
|
},
|
||||||
|
"schedules": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["basal", "isf", "cr", "target"],
|
||||||
|
"properties": {
|
||||||
|
"basal": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"description": "Time-based basal rate schedule",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["time", "rate"],
|
||||||
|
"properties": {
|
||||||
|
"time": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^([01]?[0-9]|2[0-3]):[0-5][0-9]$",
|
||||||
|
"description": "Start time in HH:MM format"
|
||||||
|
},
|
||||||
|
"rate": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 35,
|
||||||
|
"description": "Basal rate in U/hr"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isf": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"description": "Insulin sensitivity factor schedule",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["time", "value"],
|
||||||
|
"properties": {
|
||||||
|
"time": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^([01]?[0-9]|2[0-3]):[0-5][0-9]$"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 1,
|
||||||
|
"description": "ISF in configured units per 1U insulin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cr": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"description": "Carb ratio schedule",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["time", "value"],
|
||||||
|
"properties": {
|
||||||
|
"time": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^([01]?[0-9]|2[0-3]):[0-5][0-9]$"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 1,
|
||||||
|
"description": "Grams of carbs per 1U insulin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"description": "Target glucose range schedule",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["time", "low", "high"],
|
||||||
|
"properties": {
|
||||||
|
"time": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^([01]?[0-9]|2[0-3]):[0-5][0-9]$"
|
||||||
|
},
|
||||||
|
"low": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 60,
|
||||||
|
"description": "Lower target bound"
|
||||||
|
},
|
||||||
|
"high": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 60,
|
||||||
|
"description": "Upper target bound"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"insulinModel": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Insulin action curve parameters",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["rapid", "fiasp", "lyumjev", "afrezza", "custom"],
|
||||||
|
"description": "Insulin model type"
|
||||||
|
},
|
||||||
|
"dia": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 2,
|
||||||
|
"maximum": 10,
|
||||||
|
"description": "Duration of insulin action in hours"
|
||||||
|
},
|
||||||
|
"peakTime": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 15,
|
||||||
|
"maximum": 180,
|
||||||
|
"description": "Time to peak action in minutes"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"createdBy": {
|
||||||
|
"$ref": "#/$defs/issuerRef"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"legacyProfileName": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "For backward compatibility mapping to legacy profile name field"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$defs": {
|
||||||
|
"issuerRef": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["issuerType", "issuerId"],
|
||||||
|
"properties": {
|
||||||
|
"issuerType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["human", "controller", "agent", "caregiver"]
|
||||||
|
},
|
||||||
|
"issuerId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://nightscout.github.io/schemas/profile-selection.schema.json",
|
||||||
|
"title": "ProfileSelection",
|
||||||
|
"description": "Runtime event indicating which profile is intended to be active",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["selectionId", "selectedProfileId", "effectiveAt", "selectedBy"],
|
||||||
|
"properties": {
|
||||||
|
"selectionId": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"description": "Unique identifier for this selection event"
|
||||||
|
},
|
||||||
|
"selectedProfileId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of the ProfileDefinition being activated"
|
||||||
|
},
|
||||||
|
"selectedProfileHash": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-f0-9]{64}$",
|
||||||
|
"description": "Content hash of the selected profile for verification"
|
||||||
|
},
|
||||||
|
"effectiveAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "When this profile selection takes effect"
|
||||||
|
},
|
||||||
|
"selectedBy": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["issuerType", "issuerId", "authority"],
|
||||||
|
"properties": {
|
||||||
|
"issuerType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["human", "controller", "agent", "caregiver"]
|
||||||
|
},
|
||||||
|
"issuerId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"authority": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["primary", "delegated", "automated"],
|
||||||
|
"description": "Authority level of the selector"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reason": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 500,
|
||||||
|
"description": "Optional annotation explaining why this profile was selected"
|
||||||
|
},
|
||||||
|
"previousSelectionId": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"description": "ID of the selection this replaces, for audit chain"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://nightscout.github.io/schemas/reconciliation.schema.json",
|
||||||
|
"title": "Reconciliation",
|
||||||
|
"description": "Matching of DeliveryRequest to DeliveryObservation - intent vs reality",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["reconciliationId", "outcome", "reconciledAt"],
|
||||||
|
"properties": {
|
||||||
|
"reconciliationId": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"description": "Unique identifier for this reconciliation"
|
||||||
|
},
|
||||||
|
"requestId": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"format": "uuid",
|
||||||
|
"description": "The DeliveryRequest being reconciled (null for spontaneous observations)"
|
||||||
|
},
|
||||||
|
"observationId": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"format": "uuid",
|
||||||
|
"description": "The DeliveryObservation (null for unobserved requests)"
|
||||||
|
},
|
||||||
|
"outcome": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["matched", "partial", "blocked", "failed", "expired", "unknown", "spontaneous"],
|
||||||
|
"description": "Result of reconciliation"
|
||||||
|
},
|
||||||
|
"discrepancy": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Details of any mismatch between request and observation",
|
||||||
|
"properties": {
|
||||||
|
"requestedUnits": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "What was requested"
|
||||||
|
},
|
||||||
|
"deliveredUnits": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "What was actually delivered"
|
||||||
|
},
|
||||||
|
"delta": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Difference (delivered - requested)"
|
||||||
|
},
|
||||||
|
"requestedRate": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Requested rate for temp basal"
|
||||||
|
},
|
||||||
|
"deliveredRate": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Actual rate delivered"
|
||||||
|
},
|
||||||
|
"requestedDuration": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Requested duration in seconds"
|
||||||
|
},
|
||||||
|
"actualDuration": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Actual duration in seconds"
|
||||||
|
},
|
||||||
|
"reason": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"capped_by_max_basal",
|
||||||
|
"capped_by_max_iob",
|
||||||
|
"capped_by_max_bolus",
|
||||||
|
"comm_failure",
|
||||||
|
"pump_error",
|
||||||
|
"user_canceled",
|
||||||
|
"pump_busy",
|
||||||
|
"reservoir_low",
|
||||||
|
"battery_low",
|
||||||
|
"occlusion",
|
||||||
|
"expired",
|
||||||
|
"unknown"
|
||||||
|
],
|
||||||
|
"description": "Reason for discrepancy"
|
||||||
|
},
|
||||||
|
"reasonDetails": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Additional details about the discrepancy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reconciledAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "When this reconciliation was performed"
|
||||||
|
},
|
||||||
|
"reconciledBy": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"issuerType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["controller", "agent", "system"]
|
||||||
|
},
|
||||||
|
"issuerId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"latency": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Timing metrics",
|
||||||
|
"properties": {
|
||||||
|
"requestToObservationMs": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Time from request to observation in milliseconds"
|
||||||
|
},
|
||||||
|
"requestToReconciliationMs": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Time from request to reconciliation in milliseconds"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -145,7 +145,45 @@ Modern REST API with OpenAPI 3.0 spec.
|
|||||||
- `lib/api3/doc/alarmsockets.md` - Alarm socket
|
- `lib/api3/doc/alarmsockets.md` - Alarm socket
|
||||||
- `lib/api3/doc/tutorial.md` - API tutorial
|
- `lib/api3/doc/tutorial.md` - API tutorial
|
||||||
|
|
||||||
|
## Agentic Control Plane Proposal (RFC)
|
||||||
|
|
||||||
|
A proposal for extending Nightscout with a clean separation between control plane (policy, configuration, intent) and data plane (observations, telemetry, delivery) to enable AI agent collaboration with AID systems.
|
||||||
|
|
||||||
|
### Proposal Documentation
|
||||||
|
| Document | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `docs/proposals/agent-control-plane-rfc.md` | Main RFC document with full architecture |
|
||||||
|
| `docs/proposals/integration-questionnaire.md` | Questions for Loop/AAPS/Trio implementers |
|
||||||
|
| `docs/proposals/bridge-rules.md` | Legacy devicestatus → event synthesis rules |
|
||||||
|
| `docs/proposals/conflict-resolution.md` | Multi-writer semantics and authority model |
|
||||||
|
|
||||||
|
### JSON Schemas (draft-2020-12)
|
||||||
|
Located in `docs/proposals/schemas/`:
|
||||||
|
|
||||||
|
| Schema | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `event-envelope.schema.json` | Wrapper for all control plane events |
|
||||||
|
| `profile-definition.schema.json` | User-authored profile configuration |
|
||||||
|
| `profile-selection.schema.json` | Profile activation events |
|
||||||
|
| `override-definition.schema.json` | Reusable override templates |
|
||||||
|
| `override-instance.schema.json` | Concrete override activations |
|
||||||
|
| `policy-composition.schema.json` | Materialized effective parameters |
|
||||||
|
| `delivery-request.schema.json` | Intent to deliver insulin |
|
||||||
|
| `delivery-observation.schema.json` | Confirmed delivery records |
|
||||||
|
| `reconciliation.schema.json` | Request/observation matching |
|
||||||
|
| `controller-kind-definition.schema.json` | Controller type capabilities |
|
||||||
|
| `controller-instance-registration.schema.json` | Controller instance registry |
|
||||||
|
| `capability-snapshot.schema.json` | Real-time controller state |
|
||||||
|
|
||||||
|
### Key Concepts
|
||||||
|
- **Config vs Runtime vs Computed** - Separate user-authored config from runtime activations from computed state
|
||||||
|
- **Events over Snapshots** - Append-only event streams with cursor-based sync
|
||||||
|
- **Authority Hierarchy** - Human > Agent > Controller for conflict resolution
|
||||||
|
- **Bridge Mode** - Synthesize events from legacy devicestatus uploads
|
||||||
|
- **MDI as First-Class** - Manual injections are always valid
|
||||||
|
|
||||||
## Recent Changes
|
## Recent Changes
|
||||||
|
- 2026-01-01: Added Agentic Control Plane RFC and JSON schemas
|
||||||
- 2025-12-31: Updated to version 15.0.4 (dev branch)
|
- 2025-12-31: Updated to version 15.0.4 (dev branch)
|
||||||
- Configured for Replit with INSECURE_USE_HTTP=true
|
- Configured for Replit with INSECURE_USE_HTTP=true
|
||||||
- MongoDB 3.6.x driver
|
- MongoDB 3.6.x driver
|
||||||
|
|||||||
Reference in New Issue
Block a user