fix(channels): use platform message IDs to prevent duplicate memories

Fixes #430 - Prevents duplicate memories after restart by using platform message IDs instead of random UUIDs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Argenis 2026-02-16 19:04:37 -05:00 committed by GitHub
parent c3cc835346
commit e8553a800a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 217 additions and 82 deletions

View file

@ -8,8 +8,8 @@
// Already-paired tokens are persisted in config so restarts don't require
// re-pairing.
use sha2::{Digest, Sha256};
use parking_lot::Mutex;
use sha2::{Digest, Sha256};
use std::collections::HashSet;
use std::time::Instant;
@ -70,9 +70,7 @@ impl PairingGuard {
/// The one-time pairing code (only set when no tokens exist yet).
pub fn pairing_code(&self) -> Option<String> {
self.pairing_code
.lock()
.clone()
self.pairing_code.lock().clone()
}
/// Whether pairing is required at all.
@ -85,10 +83,7 @@ impl PairingGuard {
pub fn try_pair(&self, code: &str) -> Result<Option<String>, u64> {
// Check brute force lockout
{
let attempts = self
.failed_attempts
.lock()
;
let attempts = self.failed_attempts.lock();
if let (count, Some(locked_at)) = &*attempts {
if *count >= MAX_PAIR_ATTEMPTS {
let elapsed = locked_at.elapsed().as_secs();
@ -100,25 +95,16 @@ impl PairingGuard {
}
{
let mut pairing_code = self
.pairing_code
.lock()
;
let mut pairing_code = self.pairing_code.lock();
if let Some(ref expected) = *pairing_code {
if constant_time_eq(code.trim(), expected.trim()) {
// Reset failed attempts on success
{
let mut attempts = self
.failed_attempts
.lock()
;
let mut attempts = self.failed_attempts.lock();
*attempts = (0, None);
}
let token = generate_token();
let mut tokens = self
.paired_tokens
.lock()
;
let mut tokens = self.paired_tokens.lock();
tokens.insert(hash_token(&token));
// Consume the pairing code so it cannot be reused
@ -131,10 +117,7 @@ impl PairingGuard {
// Increment failed attempts
{
let mut attempts = self
.failed_attempts
.lock()
;
let mut attempts = self.failed_attempts.lock();
attempts.0 += 1;
if attempts.0 >= MAX_PAIR_ATTEMPTS {
attempts.1 = Some(Instant::now());
@ -150,28 +133,19 @@ impl PairingGuard {
return true;
}
let hashed = hash_token(token);
let tokens = self
.paired_tokens
.lock()
;
let tokens = self.paired_tokens.lock();
tokens.contains(&hashed)
}
/// Returns true if the gateway is already paired (has at least one token).
pub fn is_paired(&self) -> bool {
let tokens = self
.paired_tokens
.lock()
;
let tokens = self.paired_tokens.lock();
!tokens.is_empty()
}
/// Get all paired token hashes (for persisting to config).
pub fn tokens(&self) -> Vec<String> {
let tokens = self
.paired_tokens
.lock()
;
let tokens = self.paired_tokens.lock();
tokens.iter().cloned().collect()
}
}

View file

@ -1,5 +1,5 @@
use serde::{Deserialize, Serialize};
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::time::Instant;
@ -40,9 +40,7 @@ impl ActionTracker {
/// Record an action and return the current count within the window.
pub fn record(&self) -> usize {
let mut actions = self
.actions
.lock();
let mut actions = self.actions.lock();
let cutoff = Instant::now()
.checked_sub(std::time::Duration::from_secs(3600))
.unwrap_or_else(Instant::now);
@ -53,9 +51,7 @@ impl ActionTracker {
/// Count of actions in the current window without recording.
pub fn count(&self) -> usize {
let mut actions = self
.actions
.lock();
let mut actions = self.actions.lock();
let cutoff = Instant::now()
.checked_sub(std::time::Duration::from_secs(3600))
.unwrap_or_else(Instant::now);
@ -66,9 +62,7 @@ impl ActionTracker {
impl Clone for ActionTracker {
fn clone(&self) -> Self {
let actions = self
.actions
.lock();
let actions = self.actions.lock();
Self {
actions: Mutex::new(actions.clone()),
}