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/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
|
||||
- 2026-01-01: Added Agentic Control Plane RFC and JSON schemas
|
||||
- 2025-12-31: Updated to version 15.0.4 (dev branch)
|
||||
- Configured for Replit with INSECURE_USE_HTTP=true
|
||||
- MongoDB 3.6.x driver
|
||||
|
||||
Reference in New Issue
Block a user