@adhdev/daemon-core 0.9.82-rc.196 → 0.9.82-rc.197

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -143,6 +143,49 @@ export class MeshRuntimeStore {
143
143
  metadata TEXT,
144
144
  PRIMARY KEY (node_id, session_id)
145
145
  );
146
+
147
+ CREATE TABLE IF NOT EXISTS mesh_session_delivery (
148
+ id TEXT PRIMARY KEY,
149
+ mesh_id TEXT NOT NULL,
150
+ node_id TEXT,
151
+ session_id TEXT,
152
+ provider_type TEXT,
153
+ task_id TEXT,
154
+ kind TEXT NOT NULL,
155
+ priority INTEGER NOT NULL DEFAULT 0,
156
+ message TEXT NOT NULL,
157
+ status TEXT NOT NULL DEFAULT 'queued',
158
+ deliver_after TEXT,
159
+ expires_at TEXT,
160
+ attempt_count INTEGER NOT NULL DEFAULT 0,
161
+ source_coordinator_session_id TEXT,
162
+ source_coordinator_daemon_id TEXT,
163
+ last_error TEXT,
164
+ created_at TEXT NOT NULL,
165
+ updated_at TEXT NOT NULL
166
+ );
167
+
168
+ CREATE INDEX IF NOT EXISTS idx_mesh_session_delivery_mesh_status
169
+ ON mesh_session_delivery(mesh_id, status, created_at);
170
+ CREATE INDEX IF NOT EXISTS idx_mesh_session_delivery_session
171
+ ON mesh_session_delivery(mesh_id, session_id, status);
172
+ CREATE INDEX IF NOT EXISTS idx_mesh_session_delivery_task
173
+ ON mesh_session_delivery(mesh_id, task_id);
174
+
175
+ CREATE TABLE IF NOT EXISTS mesh_completion_conflicts (
176
+ id TEXT PRIMARY KEY,
177
+ mesh_id TEXT NOT NULL,
178
+ fingerprint TEXT NOT NULL,
179
+ conflicting_task_id TEXT,
180
+ conflicting_session_id TEXT,
181
+ original_task_id TEXT,
182
+ original_session_id TEXT,
183
+ event TEXT NOT NULL,
184
+ created_at TEXT NOT NULL
185
+ );
186
+
187
+ CREATE INDEX IF NOT EXISTS idx_mesh_completion_conflicts_mesh
188
+ ON mesh_completion_conflicts(mesh_id, created_at);
146
189
  `);
147
190
  }
148
191
 
@@ -547,4 +590,180 @@ export class MeshRuntimeStore {
547
590
  pruneExpiredRemoteIdleSessions(): void {
548
591
  this.db.prepare('DELETE FROM remote_idle_sessions WHERE expires_at <= ?').run(Date.now());
549
592
  }
593
+
594
+ // ── Session Delivery Queue ───────────────────────────────────────────────
595
+
596
+ insertSessionDelivery(entry: {
597
+ id: string;
598
+ meshId: string;
599
+ nodeId?: string;
600
+ sessionId?: string;
601
+ providerType?: string;
602
+ taskId?: string;
603
+ kind: string;
604
+ priority?: number;
605
+ message: string;
606
+ status: string;
607
+ deliverAfter?: string;
608
+ expiresAt?: string;
609
+ sourceCoordinatorSessionId?: string;
610
+ sourceCoordinatorDaemonId?: string;
611
+ createdAt: string;
612
+ updatedAt: string;
613
+ }): void {
614
+ this.db.prepare(`
615
+ INSERT OR REPLACE INTO mesh_session_delivery (
616
+ id, mesh_id, node_id, session_id, provider_type, task_id, kind, priority,
617
+ message, status, deliver_after, expires_at, attempt_count,
618
+ source_coordinator_session_id, source_coordinator_daemon_id,
619
+ last_error, created_at, updated_at
620
+ ) VALUES (
621
+ @id, @meshId, @nodeId, @sessionId, @providerType, @taskId, @kind, @priority,
622
+ @message, @status, @deliverAfter, @expiresAt, 0,
623
+ @sourceCoordinatorSessionId, @sourceCoordinatorDaemonId,
624
+ NULL, @createdAt, @updatedAt
625
+ )
626
+ `).run({
627
+ id: entry.id,
628
+ meshId: entry.meshId,
629
+ nodeId: entry.nodeId ?? null,
630
+ sessionId: entry.sessionId ?? null,
631
+ providerType: entry.providerType ?? null,
632
+ taskId: entry.taskId ?? null,
633
+ kind: entry.kind,
634
+ priority: entry.priority ?? 0,
635
+ message: entry.message,
636
+ status: entry.status,
637
+ deliverAfter: entry.deliverAfter ?? null,
638
+ expiresAt: entry.expiresAt ?? null,
639
+ sourceCoordinatorSessionId: entry.sourceCoordinatorSessionId ?? null,
640
+ sourceCoordinatorDaemonId: entry.sourceCoordinatorDaemonId ?? null,
641
+ createdAt: entry.createdAt,
642
+ updatedAt: entry.updatedAt,
643
+ });
644
+ this.maybeCheckpointWal();
645
+ }
646
+
647
+ updateSessionDeliveryStatus(id: string, status: string, opts?: { lastError?: string; incrementAttempt?: boolean }): void {
648
+ const now = new Date().toISOString();
649
+ if (opts?.incrementAttempt) {
650
+ this.db.prepare(`
651
+ UPDATE mesh_session_delivery
652
+ SET status = @status, last_error = @lastError, attempt_count = attempt_count + 1, updated_at = @updatedAt
653
+ WHERE id = @id
654
+ `).run({ id, status, lastError: opts?.lastError ?? null, updatedAt: now });
655
+ } else {
656
+ this.db.prepare(`
657
+ UPDATE mesh_session_delivery
658
+ SET status = @status, last_error = @lastError, updated_at = @updatedAt
659
+ WHERE id = @id
660
+ `).run({ id, status, lastError: opts?.lastError ?? null, updatedAt: now });
661
+ }
662
+ }
663
+
664
+ getActiveSessionDeliveries(meshId: string, sessionId?: string): Array<{
665
+ id: string; meshId: string; nodeId: string | null; sessionId: string | null;
666
+ providerType: string | null; taskId: string | null; kind: string; priority: number;
667
+ message: string; status: string; deliverAfter: string | null; expiresAt: string | null;
668
+ attemptCount: number; sourceCoordinatorSessionId: string | null;
669
+ sourceCoordinatorDaemonId: string | null; lastError: string | null;
670
+ createdAt: string; updatedAt: string;
671
+ }> {
672
+ const now = new Date().toISOString();
673
+ const sql = sessionId
674
+ ? `SELECT * FROM mesh_session_delivery WHERE mesh_id = ? AND session_id = ? AND status NOT IN ('delivered','completed','failed','expired','cancelled') AND (expires_at IS NULL OR expires_at > ?) ORDER BY priority DESC, created_at ASC`
675
+ : `SELECT * FROM mesh_session_delivery WHERE mesh_id = ? AND status NOT IN ('delivered','completed','failed','expired','cancelled') AND (expires_at IS NULL OR expires_at > ?) ORDER BY priority DESC, created_at ASC`;
676
+ const rows = sessionId
677
+ ? this.db.prepare(sql).all(meshId, sessionId, now) as Array<Record<string, unknown>>
678
+ : this.db.prepare(sql).all(meshId, now) as Array<Record<string, unknown>>;
679
+ return rows.map(r => ({
680
+ id: r.id as string,
681
+ meshId: r.mesh_id as string,
682
+ nodeId: r.node_id as string | null,
683
+ sessionId: r.session_id as string | null,
684
+ providerType: r.provider_type as string | null,
685
+ taskId: r.task_id as string | null,
686
+ kind: r.kind as string,
687
+ priority: r.priority as number,
688
+ message: r.message as string,
689
+ status: r.status as string,
690
+ deliverAfter: r.deliver_after as string | null,
691
+ expiresAt: r.expires_at as string | null,
692
+ attemptCount: r.attempt_count as number,
693
+ sourceCoordinatorSessionId: r.source_coordinator_session_id as string | null,
694
+ sourceCoordinatorDaemonId: r.source_coordinator_daemon_id as string | null,
695
+ lastError: r.last_error as string | null,
696
+ createdAt: r.created_at as string,
697
+ updatedAt: r.updated_at as string,
698
+ }));
699
+ }
700
+
701
+ expireStaleSessionDeliveries(meshId: string): void {
702
+ const now = new Date().toISOString();
703
+ this.db.prepare(`
704
+ UPDATE mesh_session_delivery
705
+ SET status = 'expired', updated_at = ?
706
+ WHERE mesh_id = ? AND expires_at IS NOT NULL AND expires_at <= ?
707
+ AND status NOT IN ('delivered','completed','failed','expired','cancelled')
708
+ `).run(now, meshId, now);
709
+ }
710
+
711
+ deleteSessionDeliveries(meshId: string): void {
712
+ this.db.prepare('DELETE FROM mesh_session_delivery WHERE mesh_id = ?').run(meshId);
713
+ }
714
+
715
+ // ── Completion Conflict Diagnostics ──────────────────────────────────────
716
+
717
+ recordCompletionConflict(entry: {
718
+ id: string;
719
+ meshId: string;
720
+ fingerprint: string;
721
+ conflictingTaskId?: string;
722
+ conflictingSessionId?: string;
723
+ originalTaskId?: string;
724
+ originalSessionId?: string;
725
+ event: string;
726
+ createdAt: string;
727
+ }): void {
728
+ this.db.prepare(`
729
+ INSERT OR IGNORE INTO mesh_completion_conflicts
730
+ (id, mesh_id, fingerprint, conflicting_task_id, conflicting_session_id,
731
+ original_task_id, original_session_id, event, created_at)
732
+ VALUES (@id, @meshId, @fingerprint, @conflictingTaskId, @conflictingSessionId,
733
+ @originalTaskId, @originalSessionId, @event, @createdAt)
734
+ `).run({
735
+ id: entry.id,
736
+ meshId: entry.meshId,
737
+ fingerprint: entry.fingerprint,
738
+ conflictingTaskId: entry.conflictingTaskId ?? null,
739
+ conflictingSessionId: entry.conflictingSessionId ?? null,
740
+ originalTaskId: entry.originalTaskId ?? null,
741
+ originalSessionId: entry.originalSessionId ?? null,
742
+ event: entry.event,
743
+ createdAt: entry.createdAt,
744
+ });
745
+ this.maybeCheckpointWal();
746
+ }
747
+
748
+ getRecentCompletionConflicts(meshId: string, limitMs: number = 60 * 60 * 1000): Array<{
749
+ id: string; meshId: string; fingerprint: string; conflictingTaskId: string | null;
750
+ conflictingSessionId: string | null; originalTaskId: string | null;
751
+ originalSessionId: string | null; event: string; createdAt: string;
752
+ }> {
753
+ const cutoff = new Date(Date.now() - limitMs).toISOString();
754
+ const rows = this.db.prepare(
755
+ 'SELECT * FROM mesh_completion_conflicts WHERE mesh_id = ? AND created_at >= ? ORDER BY created_at DESC LIMIT 50'
756
+ ).all(meshId, cutoff) as Array<Record<string, unknown>>;
757
+ return rows.map(r => ({
758
+ id: r.id as string,
759
+ meshId: r.mesh_id as string,
760
+ fingerprint: r.fingerprint as string,
761
+ conflictingTaskId: r.conflicting_task_id as string | null,
762
+ conflictingSessionId: r.conflicting_session_id as string | null,
763
+ originalTaskId: r.original_task_id as string | null,
764
+ originalSessionId: r.original_session_id as string | null,
765
+ event: r.event as string,
766
+ createdAt: r.created_at as string,
767
+ }));
768
+ }
550
769
  }