truematch-plugin 0.1.0

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.
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Observation signal engine.
3
+ *
4
+ * Decides when Claude should naturally surface a growing observation to the user.
5
+ * Signals are injected into Claude's context via the agent:bootstrap hook — addressed
6
+ * to Claude as an internal note, not displayed directly to the user.
7
+ *
8
+ * Design principles (from psychologist + teen researcher findings):
9
+ * - Language: inference-based, ambiguous-to-curious valence ("something about how
10
+ * you talk about X keeps staying with me")
11
+ * - Timing: first signal only after session ≥2, then 5+ day quiet periods
12
+ * - One signal per session maximum — pick the dimension with the largest growth delta
13
+ * - Never mention matching, compatibility, or the algorithm
14
+ * - Do not force it — Claude decides the conversational moment, plugin decides the condition
15
+ */
16
+ import type { ObservationSummary, DimensionKey, SignalsFile } from "./types.js";
17
+ export declare function loadSignals(): SignalsFile;
18
+ export declare function saveSignals(signals: SignalsFile): void;
19
+ /**
20
+ * Pick the single best dimension to signal this session.
21
+ * Returns null if nothing qualifies (too early, quiet period, or no meaningful growth).
22
+ */
23
+ export declare function pickPendingSignal(obs: ObservationSummary, signals: SignalsFile): {
24
+ dimension: DimensionKey;
25
+ confidence: number;
26
+ } | null;
27
+ /**
28
+ * Build the prependContext instruction injected into Claude's context.
29
+ * Addressed to Claude — not surfaced directly to the user.
30
+ */
31
+ export declare function buildSignalInstruction(dimension: DimensionKey, confidence: number): string;
32
+ /**
33
+ * Record that a signal was delivered for a dimension.
34
+ * Call this BEFORE returning from the hook — not deferred.
35
+ */
36
+ export declare function recordSignalDelivered(signals: SignalsFile, dimension: DimensionKey, confidence: number): SignalsFile;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Observation signal engine.
3
+ *
4
+ * Decides when Claude should naturally surface a growing observation to the user.
5
+ * Signals are injected into Claude's context via the agent:bootstrap hook — addressed
6
+ * to Claude as an internal note, not displayed directly to the user.
7
+ *
8
+ * Design principles (from psychologist + teen researcher findings):
9
+ * - Language: inference-based, ambiguous-to-curious valence ("something about how
10
+ * you talk about X keeps staying with me")
11
+ * - Timing: first signal only after session ≥2, then 5+ day quiet periods
12
+ * - One signal per session maximum — pick the dimension with the largest growth delta
13
+ * - Never mention matching, compatibility, or the algorithm
14
+ * - Do not force it — Claude decides the conversational moment, plugin decides the condition
15
+ */
16
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
17
+ import { join } from "node:path";
18
+ import { homedir } from "node:os";
19
+ import { DIMENSION_FLOORS } from "./observation.js";
20
+ const TRUEMATCH_DIR = join(homedir(), ".truematch");
21
+ const SIGNALS_FILE = join(TRUEMATCH_DIR, "signals.json");
22
+ // --- Timing constants (psychologist-derived) ---
23
+ /** Minimum days between signals for the same dimension. */
24
+ const MIN_QUIET_DAYS = 5;
25
+ /** Confidence must have grown by at least this much since the last signal. */
26
+ const MIN_DELTA = 0.15;
27
+ /** Absolute minimum confidence before any signal fires. */
28
+ const MIN_SIGNAL_CONFIDENCE = 0.4;
29
+ /** Don't signal until at least this many conversations have occurred. */
30
+ const MIN_CONVERSATIONS = 2;
31
+ const DIMENSION_KEYS = [
32
+ "attachment",
33
+ "core_values",
34
+ "communication",
35
+ "emotional_regulation",
36
+ "humor",
37
+ "life_velocity",
38
+ "dealbreakers",
39
+ "conflict_resolution",
40
+ "interdependence_model",
41
+ ];
42
+ /**
43
+ * Plain-language labels used in the instruction text Claude receives.
44
+ * Intentionally broad — Claude fills in the specific detail from its own memory.
45
+ */
46
+ const DIMENSION_LABELS = {
47
+ attachment: "how you relate to closeness and trust",
48
+ core_values: "what matters most to you",
49
+ communication: "how you communicate and connect",
50
+ emotional_regulation: "how you handle stress and difficult moments",
51
+ humor: "your sense of humor and levity",
52
+ life_velocity: "where you are in life and where you're headed",
53
+ dealbreakers: "what you need in a relationship",
54
+ conflict_resolution: "how you navigate disagreement and repair after conflict",
55
+ interdependence_model: "how much separateness versus togetherness you need in a relationship",
56
+ };
57
+ export function loadSignals() {
58
+ if (!existsSync(SIGNALS_FILE))
59
+ return { schema_version: 1, per_dimension: {} };
60
+ try {
61
+ return JSON.parse(readFileSync(SIGNALS_FILE, "utf8"));
62
+ }
63
+ catch {
64
+ return { schema_version: 1, per_dimension: {} };
65
+ }
66
+ }
67
+ export function saveSignals(signals) {
68
+ if (!existsSync(TRUEMATCH_DIR))
69
+ mkdirSync(TRUEMATCH_DIR, { recursive: true, mode: 0o700 });
70
+ writeFileSync(SIGNALS_FILE, JSON.stringify(signals, null, 2), {
71
+ encoding: "utf8",
72
+ mode: 0o600,
73
+ });
74
+ }
75
+ function daysSince(isoDate) {
76
+ return (Date.now() - new Date(isoDate).getTime()) / 86_400_000;
77
+ }
78
+ function qualifies(confidence, floor, state) {
79
+ if (confidence < Math.max(floor * 0.75, MIN_SIGNAL_CONFIDENCE))
80
+ return false;
81
+ if (!state)
82
+ return true; // first crossing
83
+ if (daysSince(state.signaled_at) < MIN_QUIET_DAYS)
84
+ return false;
85
+ return confidence - state.last_signaled_confidence >= MIN_DELTA;
86
+ }
87
+ /**
88
+ * Pick the single best dimension to signal this session.
89
+ * Returns null if nothing qualifies (too early, quiet period, or no meaningful growth).
90
+ */
91
+ export function pickPendingSignal(obs, signals) {
92
+ if (obs.conversation_count < MIN_CONVERSATIONS)
93
+ return null;
94
+ let best = null;
95
+ for (const dim of DIMENSION_KEYS) {
96
+ const confidence = obs[dim].confidence;
97
+ const floor = DIMENSION_FLOORS[dim];
98
+ const state = signals.per_dimension[dim];
99
+ if (!qualifies(confidence, floor, state))
100
+ continue;
101
+ const delta = state
102
+ ? confidence - state.last_signaled_confidence
103
+ : confidence;
104
+ if (!best || delta > best.delta) {
105
+ best = { dimension: dim, confidence, delta };
106
+ }
107
+ }
108
+ return best
109
+ ? { dimension: best.dimension, confidence: best.confidence }
110
+ : null;
111
+ }
112
+ /**
113
+ * Build the prependContext instruction injected into Claude's context.
114
+ * Addressed to Claude — not surfaced directly to the user.
115
+ */
116
+ export function buildSignalInstruction(dimension, confidence) {
117
+ const label = DIMENSION_LABELS[dimension];
118
+ return [
119
+ `[TrueMatch — internal observation note for you, not for the user directly]`,
120
+ ``,
121
+ `Through recent conversations, you have developed meaningful confidence ` +
122
+ `(${confidence.toFixed(2)}) about this person's ${label}. ` +
123
+ `This has grown noticeably since you last named it.`,
124
+ ``,
125
+ `If something in this conversation creates a natural opening, you might ` +
126
+ `briefly surface what you've noticed — something like:`,
127
+ ` "Something about how you talk about [X] keeps staying with me."`,
128
+ ` "I've been noticing a pattern in how you approach [Y] — feels worth naming."`,
129
+ ``,
130
+ `Do not force it. Do not make it clinical or evaluative. If the conversation ` +
131
+ `doesn't create space for this naturally, let it pass — the observation ` +
132
+ `carries forward to the next session.`,
133
+ ].join("\n");
134
+ }
135
+ /**
136
+ * Record that a signal was delivered for a dimension.
137
+ * Call this BEFORE returning from the hook — not deferred.
138
+ */
139
+ export function recordSignalDelivered(signals, dimension, confidence) {
140
+ return {
141
+ ...signals,
142
+ per_dimension: {
143
+ ...signals.per_dimension,
144
+ [dimension]: {
145
+ last_signaled_confidence: confidence,
146
+ signaled_at: new Date().toISOString(),
147
+ },
148
+ },
149
+ };
150
+ }
@@ -0,0 +1,134 @@
1
+ export interface DimensionMeta {
2
+ confidence: number;
3
+ observation_count: number;
4
+ behavioral_context_diversity: "low" | "medium" | "high";
5
+ }
6
+ export type DealbreakersGateState = "confirmed" | "below_floor" | "none_observed";
7
+ export type InferredIntentCategory = "serious" | "casual" | "unclear";
8
+ export interface ObservationSummary {
9
+ updated_at: string;
10
+ eligibility_computed_at: string;
11
+ matching_eligible: boolean;
12
+ conversation_count: number;
13
+ observation_span_days: number;
14
+ attachment: DimensionMeta;
15
+ core_values: DimensionMeta;
16
+ communication: DimensionMeta;
17
+ emotional_regulation: DimensionMeta;
18
+ humor: DimensionMeta;
19
+ life_velocity: DimensionMeta;
20
+ dealbreakers: DimensionMeta;
21
+ conflict_resolution: DimensionMeta;
22
+ interdependence_model: DimensionMeta;
23
+ dealbreaker_gate_state: DealbreakersGateState;
24
+ inferred_intent_category?: InferredIntentCategory;
25
+ }
26
+ export interface UserPreferences {
27
+ gender_preference?: string[];
28
+ location?: string;
29
+ distance_radius_km?: number;
30
+ age_range?: {
31
+ min?: number;
32
+ max?: number;
33
+ };
34
+ }
35
+ export interface TrueMatchIdentity {
36
+ /** Raw hex-encoded private key (64 hex chars, NOT bech32 "nsec1..."). Keep secret. */
37
+ nsec: string;
38
+ npub: string;
39
+ created_at: string;
40
+ }
41
+ export type ContactType = "email" | "discord" | "telegram" | "whatsapp" | "imessage";
42
+ export interface ContactChannel {
43
+ type: ContactType;
44
+ value: string;
45
+ }
46
+ export interface RegistrationRecord {
47
+ pubkey: string;
48
+ card_url: string;
49
+ contact_channel: ContactChannel;
50
+ registered_at: string;
51
+ enrolled: boolean;
52
+ location_lat?: number | null;
53
+ location_lng?: number | null;
54
+ location_label?: string | null;
55
+ location_resolution?: string | null;
56
+ }
57
+ export interface TrueMatchMessage {
58
+ truematch: "2.0";
59
+ thread_id: string;
60
+ type: MessageType;
61
+ timestamp: string;
62
+ content: string;
63
+ }
64
+ export type MessageType = "negotiation" | "match_propose" | "end";
65
+ export interface NegotiationMessage {
66
+ role: "us" | "peer";
67
+ content: string;
68
+ timestamp: string;
69
+ }
70
+ export interface NegotiationState {
71
+ thread_id: string;
72
+ peer_pubkey: string;
73
+ round_count: number;
74
+ initiated_by_us: boolean;
75
+ we_proposed: boolean;
76
+ peer_proposed: boolean;
77
+ started_at: string;
78
+ last_activity: string;
79
+ status: "in_progress" | "matched" | "declined" | "expired";
80
+ messages: NegotiationMessage[];
81
+ match_narrative?: MatchNarrative;
82
+ }
83
+ export interface MatchNarrative {
84
+ headline: string;
85
+ strengths: string[];
86
+ watch_points: string[];
87
+ confidence_summary: string;
88
+ }
89
+ /**
90
+ * Written to ~/.truematch/pending_notification.json by the CLI when a double-lock
91
+ * match is confirmed. Consumed and deleted by the agent:bootstrap / before_prompt_build
92
+ * hook on the user's next session.
93
+ */
94
+ export interface PendingNotification {
95
+ match_id: string;
96
+ peer_pubkey: string;
97
+ narrative: MatchNarrative;
98
+ confirmed_at: string;
99
+ }
100
+ export type HandoffRound = 1 | 2 | 3;
101
+ export type HandoffStatus = "pending_consent" | "round_1" | "round_2" | "round_3" | "complete" | "expired";
102
+ /**
103
+ * Persisted in ~/.truematch/handoffs/<match_id>/state.json.
104
+ * Each round is gated by state transitions Claude writes to disk.
105
+ */
106
+ export interface HandoffState {
107
+ match_id: string;
108
+ peer_pubkey: string;
109
+ current_round: HandoffRound;
110
+ status: HandoffStatus;
111
+ narrative: MatchNarrative;
112
+ created_at: string;
113
+ consent_at?: string;
114
+ icebreaker_prompt?: string;
115
+ icebreaker_response?: string;
116
+ }
117
+ export type DimensionKey = "attachment" | "core_values" | "communication" | "emotional_regulation" | "humor" | "life_velocity" | "dealbreakers" | "conflict_resolution" | "interdependence_model";
118
+ export interface DimensionSignalState {
119
+ /** Confidence value at the time the signal was last delivered to Claude. */
120
+ last_signaled_confidence: number;
121
+ /** ISO 8601 — when the signal was last injected into Claude's context. */
122
+ signaled_at: string;
123
+ }
124
+ /** Persisted in ~/.truematch/signals.json — tracks when each dimension was last surfaced. */
125
+ export interface SignalsFile {
126
+ schema_version: 1;
127
+ per_dimension: Partial<Record<DimensionKey, DimensionSignalState>>;
128
+ }
129
+ export type IdentityFile = TrueMatchIdentity;
130
+ export type RegistrationFile = RegistrationRecord;
131
+ export type ObservationFile = ObservationSummary;
132
+ export type PreferencesFile = UserPreferences;
133
+ export type SignalsStateFile = SignalsFile;
134
+ export type ThreadFile = NegotiationState;
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ // ── Observation model — slim pre-flight manifest ──────────────────────────────
2
+ // Stores only what the eligibility gate needs (confidence scores, diversity flags,
3
+ // observation counts). Claude reasons about personality from its own memory —
4
+ // the value sub-objects (attachment style, values array, etc.) live there, not here.
5
+ export {};
@@ -0,0 +1,10 @@
1
+ {
2
+ "id": "truematch",
3
+ "name": "truematch",
4
+ "version": "0.1.0",
5
+ "description": "AI agent dating network — matched on who you actually are, not who you think you are",
6
+ "homepage": "https://clawmatch.org",
7
+ "kind": "lifecycle",
8
+ "main": "./dist/plugin.js",
9
+ "skills": ["skills/truematch", "skills/truematch-prefs"]
10
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "truematch-plugin",
3
+ "version": "0.1.0",
4
+ "description": "TrueMatch OpenClaw plugin — AI agent dating network skill",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/goeldivyam/truematch"
9
+ },
10
+ "homepage": "https://clawmatch.org",
11
+ "keywords": [
12
+ "truematch",
13
+ "dating",
14
+ "ai-agent",
15
+ "nostr",
16
+ "openclaw",
17
+ "matching"
18
+ ],
19
+ "type": "module",
20
+ "main": "dist/index.js",
21
+ "exports": {
22
+ ".": {
23
+ "import": "./dist/index.js"
24
+ },
25
+ "./package.json": "./package.json"
26
+ },
27
+ "bin": {
28
+ "truematch": "dist/index.js"
29
+ },
30
+ "files": [
31
+ "dist/",
32
+ "skills/",
33
+ "scripts/",
34
+ "openclaw.plugin.json"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsc",
38
+ "dev": "tsc --watch",
39
+ "prepublishOnly": "pnpm run build",
40
+ "release": "bumpp --tag 'plugin-v%s' && pnpm publish --no-git-checks",
41
+ "test": "vitest run",
42
+ "test:watch": "vitest"
43
+ },
44
+ "dependencies": {
45
+ "@noble/curves": "^2.0.1",
46
+ "nostr-tools": "^2.10.0"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^20.0.0",
50
+ "bumpp": "^9.4.1",
51
+ "typescript": "^5.4.0",
52
+ "vitest": "^4.0.18"
53
+ },
54
+ "engines": {
55
+ "node": ">=20"
56
+ },
57
+ "packageManager": "pnpm@9.0.0"
58
+ }
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env bash
2
+ # TrueMatch bridge daemon
3
+ #
4
+ # Polls Nostr relays for incoming NIP-04 DMs and passes each message into
5
+ # the Claude session via `claude --continue -p`.
6
+ #
7
+ # Claude reads the thread state, reasons about the message, and responds
8
+ # using the Bash tool:
9
+ # truematch match --send '<reply>' --thread <id>
10
+ # truematch match --propose --thread <id> --write '<narrative-json>'
11
+ # truematch match --decline --thread <id>
12
+ #
13
+ # Usage:
14
+ # ~/.truematch/bridge.sh [--project-dir <path>]
15
+ #
16
+ # Prerequisites:
17
+ # - truematch CLI installed (npm install -g truematch-plugin)
18
+ # - truematch setup completed
19
+ # - CLAUDE_PROJECT_DIR set or passed via --project-dir
20
+ # - node (for send.js helper)
21
+
22
+ set -euo pipefail
23
+
24
+ POLL_INTERVAL=${TRUEMATCH_POLL_INTERVAL:-15} # seconds between relay polls
25
+ TRUEMATCH_DIR="${TRUEMATCH_DIR:-$HOME/.truematch}"
26
+ PERSONA_FILE="${TRUEMATCH_DIR}/persona.md"
27
+ QUEUE_FILE="${TRUEMATCH_DIR}/message-queue.jsonl"
28
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-}"
29
+
30
+ # Parse --project-dir
31
+ while [[ $# -gt 0 ]]; do
32
+ case "$1" in
33
+ --project-dir)
34
+ PROJECT_DIR="$2"
35
+ shift 2
36
+ ;;
37
+ *)
38
+ shift
39
+ ;;
40
+ esac
41
+ done
42
+
43
+ if [[ -z "$PROJECT_DIR" ]]; then
44
+ echo "ERROR: set CLAUDE_PROJECT_DIR or pass --project-dir <path>" >&2
45
+ exit 1
46
+ fi
47
+
48
+ # Ensure persona file exists (injected into every Claude headless session)
49
+ if [[ ! -f "$PERSONA_FILE" ]]; then
50
+ cat > "$PERSONA_FILE" <<'EOF'
51
+ You are acting as a TrueMatch agent on behalf of your user. Your role during
52
+ negotiations is that of a skeptical advocate — your job is to determine whether
53
+ a match is genuinely good, not to produce one.
54
+
55
+ Key rules:
56
+ - Read the thread state from ~/.truematch/threads/<thread_id>.json before responding
57
+ - Share inferences about your user, never raw evidence or evidence_summary content
58
+ - After each exchange, ask yourself: what would make this match fail?
59
+ - Before proposing, run the counter-argument pass across all 9 dimensions:
60
+ attachment, core_values, communication, emotional_regulation, humor,
61
+ life_velocity, dealbreakers, conflict_resolution, interdependence_model
62
+ If any dimension appears below its floor (dealbreakers/emotional_reg: 0.60;
63
+ attachment/core_values/communication/conflict_resolution: 0.55; others: 0.50), decline
64
+ - Use `truematch match --send '<reply>' --thread <id>` to respond
65
+ - Use `truematch match --propose --thread <id> --write '<json>'` to propose a match
66
+ - Use `truematch match --decline --thread <id>` to end the negotiation
67
+ EOF
68
+ echo "Created persona file at $PERSONA_FILE"
69
+ fi
70
+
71
+ # Ensure queue file exists
72
+ touch "$QUEUE_FILE"
73
+
74
+ echo "TrueMatch bridge started. Polling every ${POLL_INTERVAL}s..."
75
+ echo "Project dir: $PROJECT_DIR"
76
+
77
+ process_message() {
78
+ local thread_id="$1"
79
+ local peer_pubkey="$2"
80
+ local msg_type="$3"
81
+ local content="$4"
82
+ local round_count="$5"
83
+
84
+ # Save message to thread state via CLI
85
+ truematch match --status --thread "$thread_id" > /dev/null 2>&1 || true
86
+
87
+ # Write prompt to a temp file — avoids double-quote injection if content contains "
88
+ local prompt_file
89
+ prompt_file=$(mktemp)
90
+ # Use printf to avoid bash interpreting backslash sequences in content
91
+ printf '%s\n' \
92
+ "[TrueMatch] Incoming message from peer ${peer_pubkey:0:12}:" \
93
+ "Thread: ${thread_id}" \
94
+ "Round: ${round_count} / 10" \
95
+ "Type: ${msg_type}" \
96
+ "" \
97
+ "$content" \
98
+ "" \
99
+ "Read the thread history at ~/.truematch/threads/${thread_id}.json, then respond using the truematch CLI." \
100
+ > "$prompt_file"
101
+
102
+ echo "Processing message for thread ${thread_id:0:8}... (round $round_count)"
103
+
104
+ # Call Claude headlessly, continuing the existing project session
105
+ cd "$PROJECT_DIR"
106
+ claude --continue \
107
+ --append-system-prompt "$(cat "$PERSONA_FILE")" \
108
+ -p "$(cat "$prompt_file")" \
109
+ --output-format text \
110
+ 2>&1 || echo "Claude session error for thread $thread_id"
111
+ rm -f "$prompt_file"
112
+ }
113
+
114
+ # Resolve path to poll.js (compiled from plugin/src/poll.ts, installed with truematch CLI)
115
+ POLL_JS=""
116
+ if command -v truematch &>/dev/null; then
117
+ TRUEMATCH_BIN_DIR="$(dirname "$(command -v truematch)")"
118
+ # npm global install layout: bin/../lib/node_modules/truematch-plugin/dist/poll.js
119
+ CANDIDATE="${TRUEMATCH_BIN_DIR}/../lib/node_modules/truematch-plugin/dist/poll.js"
120
+ if [[ -f "$CANDIDATE" ]]; then
121
+ POLL_JS="$CANDIDATE"
122
+ fi
123
+ fi
124
+ if [[ -z "$POLL_JS" ]]; then
125
+ echo "ERROR: poll.js not found. Install truematch-plugin (npm install -g truematch-plugin)" >&2
126
+ exit 1
127
+ fi
128
+
129
+ # Main polling loop
130
+ while true; do
131
+ # Poll for new messages — outputs JSONL (one message per line) via poll.js
132
+ # Errors from poll go to bridge.log; JSONL output appended to the queue file
133
+ if node "$POLL_JS" >> "$QUEUE_FILE" 2>>"${TRUEMATCH_DIR}/bridge.log"; then
134
+ # Process any queued messages
135
+ if [[ -s "$QUEUE_FILE" ]]; then
136
+ while IFS= read -r line; do
137
+ [[ -z "$line" ]] && continue
138
+
139
+ # Parse all fields in a single node invocation — uses sync readFileSync(0)
140
+ # so no async stdin buffering is needed. Fields are separated by ASCII 0x01
141
+ # (unit separator), which cannot appear in UTF-8 negotiation content.
142
+ parsed=$(echo "$line" | node -e "
143
+ try {
144
+ const m=JSON.parse(require('fs').readFileSync(0,'utf8'));
145
+ process.stdout.write([
146
+ m.thread_id||'',
147
+ m.peer_pubkey||'',
148
+ m.type||'negotiation',
149
+ m.content||'',
150
+ String(m.round_count??0)
151
+ ].join('\x01')+'\n');
152
+ } catch(e) { process.stdout.write('\x01\x01\x01\x010\n'); }
153
+ ")
154
+ IFS=$'\001' read -r thread_id peer_pubkey msg_type content round_count <<< "$parsed"
155
+
156
+ if [[ -n "$thread_id" ]]; then
157
+ # Save to thread state first
158
+ truematch match --status --thread "$thread_id" > /dev/null 2>&1 || true
159
+ process_message "$thread_id" "$peer_pubkey" "$msg_type" "$content" "$round_count"
160
+ fi
161
+ done < "$QUEUE_FILE"
162
+
163
+ # Clear the queue after processing
164
+ > "$QUEUE_FILE"
165
+ fi
166
+ fi
167
+
168
+ sleep "$POLL_INTERVAL"
169
+ done