DEV Community

Cover image for How I Designed Invariants for AI-Human Collaboration
Rono
Rono

Posted on • Edited on

How I Designed Invariants for AI-Human Collaboration

The morning it clicked

April 27, 2026. 8:30 AM. I opened my laptop with a specific problem: the AI kept overwriting human corrections, and my team was starting to lose trust in the tool we were building.

Not because the AI was bad. Because the architecture was wrong.

We had built a beautiful real-time collaborative status sheet. AI read weekly summaries and generated project rows. Multiple people could edit simultaneously. WebSockets broadcast changes instantly. It felt like magic.

But every time the AI ran again, it would regenerate the sheet from scratch. Human corrections? Gone. Manual overrides? Vanished. The project manager who spent ten minutes fixing deadlines and adding comments would come back the next day to find the AI's original, wrong version staring back at her.

She stopped correcting. She stopped trusting. She stopped using the tool.

That morning, I wrote three invariants into the code. They took 42 minutes to implement. They have not changed in the months since. And they turned a chaotic prototype into a system people actually rely on.

This article is about those invariants: what they are, why they matter, and how you can apply the same thinking to your own AI-human collaboration systems.


The problem with AI-generated content in collaborative tools

Before we get to the invariants, let me describe the problem precisely.

You have an AI that reads unstructured text (weekly summaries, Daraja Workspace messages, email threads) and extracts structured data (tasks, deadlines, owners, status). You present this data in a collaborative interface where humans can view, edit, and enrich it.

This creates a tension:

The AI is good at Humans are good at
Processing large volumes of text quickly Understanding nuance and context
Extracting obvious structure Catching subtle errors
Working 24/7 with no fatigue Making judgment calls
Being consistent Being correct when it matters

Neither can fully replace the other. The AI will make mistakes. The humans will not have time to fix everything. The system needs both.

But traditional architectures treat AI output as ephemeral and human input as temporary. The AI regenerates on every view. The human corrections are stored in the UI state but not protected from future AI runs. The result is a system where trust never builds because the machine keeps forgetting what it was taught.

Here is what I learned: the problem is not the AI. The problem is the absence of invariants.

Invariants are rules that the system guarantees will always be true. They are not features. They are not configurable. They are the bedrock. When you design invariants for AI-human collaboration, you are answering one question: in any conflict between machine output and human judgment, who wins?

My answer:

The human wins. Always. Immediately. Permanently.

That answer became three lines in the code.


Invariant 1: The POST-once rule

The code

Future<void> _loadExistingStatus() async {
  setState(() => _isLoading = true);

  try {
    final response = await http.get(
      Uri.parse('https://api.rucks.co.ke/unfo/'),
    );

    if (response.statusCode == 200) {
      final List<dynamic> data = jsonDecode(response.body);
      final existingStatus = data.firstWhere(
        (item) => item['username'] == _roomName,
        orElse: () => null,
      );

      if (existingStatus != null && existingStatus['likes'] != null) {
        final savedData = jsonDecode(existingStatus['likes']);
        if (savedData['rows'] != null) {
          _rows = (savedData['rows'] as List)
              .map((row) => StatusRow.fromJson(row))
              .toList();
          _foundOnServer = true; // ✅ Mark as already on server — no POST needed
        }
      }
    }
  } catch (e) {
    // Error handling omitted for clarity
  }

  // Only generate (and POST) if nothing came back from the server
  if (_rows.isEmpty && !_foundOnServer) {
    await _generateNewStatus();
  }

  setState(() => _isLoading = false);
}
Enter fullscreen mode Exit fullscreen mode

What it does

The system performs a GET lookup before ever invoking the AI. Generation — and the subsequent POST to persist results — occurs only when the lookup returns no existing data for the current week.

Why it matters

Most systems treat AI as an on-demand service. Every user, every refresh, every navigation triggers a new call to the model. This is fine for chatbots and search. It is disastrous for collaborative documents.

Here is what happens without this invariant:

  • User A opens the week sheet → AI generates → POST saved
  • User B opens the same week sheet → AI generates again → POST overwrites
  • User A makes corrections → saves locally
  • User C opens the sheet → AI generates again → User A's corrections are gone

The sheet becomes a game of last-write-wins between the AI and every human who opens it. The AI, being faster and more frequent, usually wins.

The POST-once rule changes everything:

Without the rule With the rule
AI runs on every view AI runs once per week
Storage scales with views Storage scales with weeks
Human corrections are fragile Human corrections are permanent
No auditable baseline First AI output is frozen baseline

The deeper insight

The _foundOnServer flag is the key. The system is not just checking if data exists — it is checking if data exists for this specific room name. The room name encodes the week number (week42_status). This means the invariant is scoped to the domain entity, not to the user session.

This is the difference between streaming intelligence (ephemeral, unrepeatable, distrustful) and settled intelligence (auditable, comparable, learnable). The first AI output becomes the fixed point against which all human corrections are measured.

What broke without it

Before this invariant, we had a week where the AI ran 47 times for the same status sheet. Forty-seven. Different users, different times of day, different devices. Each run overwrote the previous state. The sheet was never stable. Users reported that their corrections "disappeared" but could never reproduce the bug because the bug was the architecture itself.

The POST-once rule reduced API calls by 98% and eliminated the disappearing-corrections bug entirely.


Invariant 2: The override-is-sacred rule

The code

class StatusRow {
  final String client;
  final String brand;
  final String project;
  final String team;
  final String activities;
  final String pic;
  final String creative;
  final String timelines;
  final String comments;
  final bool isManuallyEdited;  // ← THE INVARIANT LIVES HERE

  StatusRow({
    required this.client,
    required this.brand,
    required this.project,
    required this.team,
    required this.activities,
    required this.pic,
    required this.creative,
    required this.timelines,
    required this.comments,
    this.isManuallyEdited = false,  // Default: AI-generated
  });

  StatusRow copyWith({
    // ... other fields
    bool? isManuallyEdited,
  }) {
    return StatusRow(
      // ... other fields
      isManuallyEdited: isManuallyEdited ?? this.isManuallyEdited,
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

And the edit handler:

void _editCell(int rowIndex, String column, String currentValue) {
  // ... dialog shown, user edits, newValue received

  // When saving the edit, we mark the row as manually edited
  final updatedRow = _rows[rowIndex].copyWith(
    [column]: newValue,
    isManuallyEdited: true,  // ← FLIP THE FLAG
  );

  setState(() {
    _rows[rowIndex] = updatedRow;
  });

  // Broadcast to other users
  _sendCellEdit(rowIndex, column, currentValue, newValue);
}
Enter fullscreen mode Exit fullscreen mode

What it does

Every StatusRow carries a boolean flag: isManuallyEdited. It starts false for AI-generated rows. The first time a human edits any field in that row, the flag flips to true and never flips back.

The system preserves the original AI values in an audit trail (not shown in the simplified code), but the main view shows only the human-corrected version.

Why it matters

In most AI systems, human feedback is treated as a training signal — something that eventually improves the model after retraining, fine-tuning, or prompt adjustment. The latency between correction and effect can be hours, days, or weeks.

In operational environments, that latency is unacceptable. The human correction must be immediately authoritative. Not "the model will learn from this." Not "we'll update the prompt next sprint." Right now.

The isManuallyEdited flag enforces this at the storage layer, not just the application layer. Even if the AI reruns (which it won't, thanks to Invariant 1), it cannot overwrite a field where isManuallyEdited is true.

The trust multiplier

This invariant creates psychological safety. The AI is allowed to be wrong. No one is blamed for a bad extraction. The system simply accepts the correction and moves on.

I watched this happen in real time. A project manager corrected the same deadline field seventeen times over two months because the AI kept extracting the wrong date from the weekly summaries. (The summaries said "Friday" but the actual deadline was "Wednesday" due to a client change that was never updated in the summaries.)

Without the flag, she would have corrected it seventeen times and been frustrated seventeen times. With the flag, she corrected it once and never saw the wrong date again. The AI did not get smarter — the system simply stopped showing its mistake.

The audit trail pattern

The full implementation preserves the original AI values. The code snippet above does not show this for brevity, but here is the pattern:

class StatusRow {
  // Current values (human-corrected if applicable)
  final String client;
  final String brand;
  // ... other fields

  // Provenance
  final bool isManuallyEdited;
  final StatusRow? originalAiValues;  // Preserved on first edit

  // When a human edits:
  StatusRow humanEdit(String field, String newValue) {
    return StatusRow(
      // ... copy all fields, updating the edited one
      isManuallyEdited: true,
      originalAiValues: this.originalAiValues ?? this,  // Store original on first edit
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

This means you can always answer the question: "What did the AI originally think?" The answer is never lost. It is just demoted from the main view.


Invariant 3: The ephemeral presence boundary

The code

class _RealTimeStatusSheetPageState extends State<RealTimeStatusSheetPage> {
  // Track users in the room — stored in memory only, NEVER persisted
  final Set<String> _activeUsers = {};

  void _handleWebSocketMessage(dynamic message) {
    // ...
    if (messageText.contains('USER_JOINED')) {
      String username = extractUsername(messageText);
      if (username.isNotEmpty && username != widget.mUsername) {
        setState(() => _activeUsers.add(username));
      }
    }
    else if (messageText.contains('USER_LEFT')) {
      String username = extractUsername(messageText);
      if (username.isNotEmpty) {
        setState(() => _activeUsers.remove(username));
      }
    }
  }

  @override
  void dispose() {
    _sendUserLeaveMessage();  // Announce departure
    _channel.sink.close();
    super.dispose();
  }
}
Enter fullscreen mode Exit fullscreen mode

What it does

User presence — who is looking at the status sheet right now — is stored exclusively in client-side memory. It is sent via WebSocket messages but never written to the database. When a user disconnects, they disappear from _activeUsers. When they reconnect, they re-announce themselves and reappear.

Why it matters

Ontologies and data models have a tendency to capture everything, including ephemeral state. This creates two problems:

  1. Storage grows with activity, not with domain complexity. If you persist presence, you generate a record every time someone opens a sheet and every time they close it. For a team of 50 people working 5 days a week, that is 500 presence events per week — all of which are useless the moment they are written.

  2. Queries become cluttered with irrelevant state. If you store presence in the same table as settled facts (like StatusRow values), every query has to filter out "is this person still online?" nonsense.

The invariant is simple: *if the information becomes false the moment you look away, do not write it to disk. *

What belongs in the database vs. what belongs in memory

Information Storage location Why
Status row values (client, project, deadlines) Database Settled truth, persists across sessions
Who created a row Database Historical record, audit requirement
When a row was last edited Database Track freshness, conflict resolution
Who is looking at the sheet right now Memory only Ephemeral, changes by the second
Who edited which cell and when Database Audit trail, compliance
Cursor positions Memory only Transient, session-specific

The reconnection pattern

Because presence is not persisted, the system must handle reconnections gracefully. The pattern is simple:

void _connectWebSocket() {
  _channel = WebSocketChannel.connect(Uri.parse(wsUrl));

  _channel.stream.listen(
    (message) => _handleMessage(message),
    onDone: () {
      // Connection lost
      setState(() => _activeUsers.clear());  // Clear presence
      _reconnect();  // Try to reconnect after delay
    },
  );

  // After reconnection, re-announce presence
  _sendUserJoinMessage();
}
Enter fullscreen mode Exit fullscreen mode

When the WebSocket reconnects (after network blip, laptop sleep, or server restart), the client re-announces its presence. The server does not remember who was online before — it does not need to. The current set of connected clients self-assembles from scratch.

This is simpler, more reliable, and infinitely easier to debug than a persisted presence system with heartbeat timeouts and stale-cleanup jobs.


How the invariants work together

Alone, each invariant is useful. Together, they form a coherent system.

Invariant Problem it solves What it enables
POST-once AI overwrites human work Stable baseline, auditable first draft
Override-is-sacred Human corrections are temporary Trust, psychological safety, immediate authority
Ephemeral presence Storage bloat, stale state Real-time awareness without persistence complexity

Here is how they compose in a real scenario:

  • Monday, 9:00 AM: First user opens Week 42 sheet. POST-once rule triggers AI generation. Sheet saved to database. isManuallyEdited = false on all rows.

  • Monday, 2:00 PM: Project manager opens sheet. Sees wrong deadline. Edits cell. Override-is-sacred rule flips isManuallyEdited = true for that row. Edit broadcasts to all connected users via WebSocket.

  • Tuesday, 10:00 AM: Another user opens sheet. POST-once rule prevents regeneration. Override-is-sacred rule ensures they see the corrected deadline, not the AI's original. Ephemeral presence adds them to _activeUsers (memory only).

  • Tuesday, 3:00 PM: Network blip. All users disconnect. Ephemeral presence clears _activeUsers. No data lost because presence was never the source of truth.

  • Wednesday, 9:00 AM: Users reconnect. Ephemeral presence re-announces them. The corrected deadline is still there because it was written to the database under Invariant 2.

The invariants create a system where the AI is useful but not dominant, human judgment is respected but not overburdened, and real-time awareness is present but not over-engineered.


What these invariants are NOT

Let me be clear about what I am not claiming.

  • This is not CRDT or OT. I am not solving concurrent editing conflicts. If two users edit the same cell simultaneously, last-write-wins. For our use case (project status sheets), this is acceptable. For a Google Docs clone, it is not.

  • This is not a general AI training loop. The isManuallyEdited flag is not yet fed back into the model. The AI does not learn from corrections. That is the next phase. Right now, the flag only protects human edits from being overwritten.

  • This is not production-scale presence. The ephemeral presence system works for dozens of concurrent users. For thousands, you would need a more sophisticated approach (Redis, Ably, Pusher). Our scale is smaller, so the simple solution wins.

  • This is not a replacement for proper auditing. The audit trail preserves original AI values, but there is no per-cell timestamp or username tracking in the simplified code shown here. The production version has those.

I am sharing what worked for us. Your mileage may vary. The principles generalize; the specific implementations will need adaptation to your stack and scale.


How to apply these invariants to your own systems

You do not need to copy the code. You need to copy the thinking.

For the POST-once rule

Ask yourself: what operation am I performing repeatedly that should only happen once per domain entity?

Common candidates:

  • AI generation (obviously)
  • Data enrichment from external APIs
  • Summarization or aggregation
  • Notification sending ("first time someone views X, send an alert")
  • Expensive computation that does not change between views

The pattern is always the same: check before you compute. Store the result. Serve the stored result on subsequent requests. The storage can be a database, a cache, or even a file. The key is the if not exists check.

For the override-is-sacred rule

Ask yourself: what human action should the system never override?

Common candidates:

  • Manual corrections to AI output
  • User preferences and settings
  • Explicit "mark as reviewed" actions
  • Human-entered data in a system that also ingests from external sources

The pattern: add a provenance flag. When the human acts, flip the flag. On subsequent automated updates, check the flag. If it is true, skip that field or that entity. Store the original automated value elsewhere if needed for audit.

For the ephemeral presence boundary

Ask yourself: what information is only true at this exact moment?

Common candidates:

  • Who is online right now
  • Cursor positions
  • Typing indicators
  • Scroll positions
  • Temporary selections or highlights

The pattern: keep it in memory. Send it via real-time channel (WebSocket, SSE, MQTT). Never write it to a persistent store. On reconnection, re-establish from scratch. Do not try to remember the past.


The test of time

That code has been running since April 27, 2026. The invariants have not changed.

Not because I am stubborn — because they have not needed to change.

  • The POST-once rule survived a hundred-page-refresh stress test where we simulated 50 users opening the same week sheet simultaneously. One API call. One database write. Perfect consistency.

  • The override-is-sacred rule survived a project manager correcting the same field seventeen times across two months. The AI was very wrong about that deadline. The flag never failed.

  • The ephemeral presence boundary survived a WebSocket server restart with zero data corruption. Presence reset. All other data intact. No cleanup jobs needed.

I wrote these rules in 42 minutes on a Tuesday morning. They have saved hundreds of hours since. Not in code complexity — in trust. The team trusts the sheet. The project managers trust the data. The stakeholders trust the status.

That is the real metric. Not API latency. Not database size. Trust.


The code comment

If you look at the original file, there is a comment at the top:

//worked well 27th April 2026 - 08:30 AM - 09:12 AM
Enter fullscreen mode Exit fullscreen mode

That is not a boast. It is a reminder.

Good ideas arrive in focused windows. The trick is to recognize them and write them down as invariants before they slip away. Features can be added later. Invariants are structural. Get them right once, and the system becomes something you can build on for years.

That morning, I stopped adding features. I wrote three rules. I have not regretted it since.


Kiprono Ngetich builds AI-assisted collaboration tools. He thinks too much about data provenance and WebSocket protocols.


Top comments (0)