@geravant/sinain 1.0.1
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.
- package/README.md +183 -0
- package/index.ts +2096 -0
- package/install.js +155 -0
- package/openclaw.plugin.json +59 -0
- package/package.json +21 -0
- package/sinain-memory/common.py +403 -0
- package/sinain-memory/demo_knowledge_transfer.sh +85 -0
- package/sinain-memory/embedder.py +268 -0
- package/sinain-memory/eval/__init__.py +0 -0
- package/sinain-memory/eval/assertions.py +288 -0
- package/sinain-memory/eval/judges/__init__.py +0 -0
- package/sinain-memory/eval/judges/base_judge.py +61 -0
- package/sinain-memory/eval/judges/curation_judge.py +46 -0
- package/sinain-memory/eval/judges/insight_judge.py +48 -0
- package/sinain-memory/eval/judges/mining_judge.py +42 -0
- package/sinain-memory/eval/judges/signal_judge.py +45 -0
- package/sinain-memory/eval/schemas.py +247 -0
- package/sinain-memory/eval_delta.py +109 -0
- package/sinain-memory/eval_reporter.py +642 -0
- package/sinain-memory/feedback_analyzer.py +221 -0
- package/sinain-memory/git_backup.sh +19 -0
- package/sinain-memory/insight_synthesizer.py +181 -0
- package/sinain-memory/memory/2026-03-01.md +11 -0
- package/sinain-memory/memory/playbook-archive/sinain-playbook-2026-03-01-1418.md +15 -0
- package/sinain-memory/memory/playbook-logs/2026-03-01.jsonl +1 -0
- package/sinain-memory/memory/sinain-playbook.md +21 -0
- package/sinain-memory/memory-config.json +39 -0
- package/sinain-memory/memory_miner.py +183 -0
- package/sinain-memory/module_manager.py +695 -0
- package/sinain-memory/playbook_curator.py +225 -0
- package/sinain-memory/requirements.txt +3 -0
- package/sinain-memory/signal_analyzer.py +141 -0
- package/sinain-memory/test_local.py +402 -0
- package/sinain-memory/tests/__init__.py +0 -0
- package/sinain-memory/tests/conftest.py +189 -0
- package/sinain-memory/tests/test_curator_helpers.py +94 -0
- package/sinain-memory/tests/test_embedder.py +210 -0
- package/sinain-memory/tests/test_extract_json.py +124 -0
- package/sinain-memory/tests/test_feedback_computation.py +121 -0
- package/sinain-memory/tests/test_miner_helpers.py +71 -0
- package/sinain-memory/tests/test_module_management.py +458 -0
- package/sinain-memory/tests/test_parsers.py +96 -0
- package/sinain-memory/tests/test_tick_evaluator.py +430 -0
- package/sinain-memory/tests/test_triple_extractor.py +255 -0
- package/sinain-memory/tests/test_triple_ingest.py +191 -0
- package/sinain-memory/tests/test_triple_migrate.py +138 -0
- package/sinain-memory/tests/test_triplestore.py +248 -0
- package/sinain-memory/tick_evaluator.py +392 -0
- package/sinain-memory/triple_extractor.py +402 -0
- package/sinain-memory/triple_ingest.py +290 -0
- package/sinain-memory/triple_migrate.py +275 -0
- package/sinain-memory/triple_query.py +184 -0
- package/sinain-memory/triplestore.py +498 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Phase 3 Step 2/2b: Feedback Analyzer — score feedback + compute effectiveness.
|
|
3
|
+
|
|
4
|
+
Fully mechanical: effectiveness, feedback scores, directive, and interpretation
|
|
5
|
+
are all computed from log data without an LLM call. The previous gpt-5-nano
|
|
6
|
+
integration was removed after 60+ ticks of identical static output (avg 0.4,
|
|
7
|
+
directive 'stability') regardless of context.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python3 feedback_analyzer.py --memory-dir memory/ --session-summary "..."
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
from common import (
|
|
18
|
+
output_json,
|
|
19
|
+
read_recent_logs,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def compute_effectiveness(logs: list[dict]) -> dict:
|
|
24
|
+
"""Mechanically compute effectiveness from playbook-log entries.
|
|
25
|
+
|
|
26
|
+
- outputs: ticks where Step 5 produced output (not skipped)
|
|
27
|
+
- positive: ticks where output was followed by avg compositeScore > 0.2
|
|
28
|
+
- negative: ticks where output was followed by avg compositeScore < -0.1
|
|
29
|
+
- neutral: remainder
|
|
30
|
+
- rate: positive / outputs
|
|
31
|
+
"""
|
|
32
|
+
output_ticks = [e for e in logs if not e.get("skipped", True)]
|
|
33
|
+
outputs = len(output_ticks)
|
|
34
|
+
|
|
35
|
+
if outputs == 0:
|
|
36
|
+
return {"outputs": 0, "positive": 0, "negative": 0, "neutral": 0, "rate": 0.0}
|
|
37
|
+
|
|
38
|
+
positive = 0
|
|
39
|
+
negative = 0
|
|
40
|
+
neutral = 0
|
|
41
|
+
|
|
42
|
+
# Sort by timestamp for sequential analysis
|
|
43
|
+
sorted_logs = sorted(logs, key=lambda e: e.get("ts", ""))
|
|
44
|
+
|
|
45
|
+
for i, entry in enumerate(sorted_logs):
|
|
46
|
+
if entry.get("skipped", True):
|
|
47
|
+
continue
|
|
48
|
+
# Look at next tick's feedback
|
|
49
|
+
if i + 1 < len(sorted_logs):
|
|
50
|
+
next_entry = sorted_logs[i + 1]
|
|
51
|
+
feedback = next_entry.get("feedbackScores", {})
|
|
52
|
+
avg = feedback.get("avg", 0)
|
|
53
|
+
if avg > 0.2:
|
|
54
|
+
positive += 1
|
|
55
|
+
elif avg < -0.1:
|
|
56
|
+
negative += 1
|
|
57
|
+
else:
|
|
58
|
+
neutral += 1
|
|
59
|
+
else:
|
|
60
|
+
neutral += 1 # No next tick yet
|
|
61
|
+
|
|
62
|
+
rate = positive / outputs if outputs > 0 else 0.0
|
|
63
|
+
return {
|
|
64
|
+
"outputs": outputs,
|
|
65
|
+
"positive": positive,
|
|
66
|
+
"negative": negative,
|
|
67
|
+
"neutral": neutral,
|
|
68
|
+
"rate": round(rate, 2),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def extract_feedback_scores(logs: list[dict]) -> dict:
|
|
73
|
+
"""Extract composite scores with time-decay weighting."""
|
|
74
|
+
all_scores = []
|
|
75
|
+
high_patterns = []
|
|
76
|
+
low_patterns = []
|
|
77
|
+
|
|
78
|
+
for i, entry in enumerate(logs): # logs are newest-first
|
|
79
|
+
feedback = entry.get("feedbackScores", {})
|
|
80
|
+
avg = feedback.get("avg")
|
|
81
|
+
if avg is not None:
|
|
82
|
+
# Decay: recent entries weighted more (1.0 for newest, 0.3 for oldest)
|
|
83
|
+
weight = max(0.3, 1.0 - (i / max(len(logs), 1)) * 0.7)
|
|
84
|
+
all_scores.append((avg, weight))
|
|
85
|
+
|
|
86
|
+
for h in feedback.get("high", []):
|
|
87
|
+
if h not in high_patterns: # deduplicate
|
|
88
|
+
high_patterns.append(h)
|
|
89
|
+
for lo in feedback.get("low", []):
|
|
90
|
+
if lo not in low_patterns: # deduplicate
|
|
91
|
+
low_patterns.append(lo)
|
|
92
|
+
|
|
93
|
+
if all_scores:
|
|
94
|
+
weighted_sum = sum(s * w for s, w in all_scores)
|
|
95
|
+
weight_sum = sum(w for _, w in all_scores)
|
|
96
|
+
avg_score = round(weighted_sum / weight_sum, 2)
|
|
97
|
+
else:
|
|
98
|
+
avg_score = 0
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
"avg": avg_score,
|
|
102
|
+
"high": high_patterns[:5],
|
|
103
|
+
"low": low_patterns[:5],
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def determine_directive(effectiveness: dict) -> str:
|
|
108
|
+
"""Determine curate directive from effectiveness metrics."""
|
|
109
|
+
if effectiveness["outputs"] < 5:
|
|
110
|
+
return "insufficient_data"
|
|
111
|
+
rate = effectiveness["rate"]
|
|
112
|
+
if rate < 0.4:
|
|
113
|
+
return "aggressive_prune"
|
|
114
|
+
elif rate > 0.7:
|
|
115
|
+
return "stability"
|
|
116
|
+
return "normal"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def compute_score_trend(logs: list[dict]) -> str:
|
|
120
|
+
"""Detect score trend from recent logs: rising, falling, or flat."""
|
|
121
|
+
scores = []
|
|
122
|
+
for entry in sorted(logs, key=lambda e: e.get("ts", "")):
|
|
123
|
+
fb = entry.get("feedbackScores", {})
|
|
124
|
+
avg = fb.get("avg")
|
|
125
|
+
if avg is not None:
|
|
126
|
+
scores.append(avg)
|
|
127
|
+
if len(scores) < 3:
|
|
128
|
+
return "insufficient"
|
|
129
|
+
# Compare first third vs last third
|
|
130
|
+
third = max(1, len(scores) // 3)
|
|
131
|
+
early_avg = sum(scores[:third]) / third
|
|
132
|
+
late_avg = sum(scores[-third:]) / third
|
|
133
|
+
delta = late_avg - early_avg
|
|
134
|
+
if delta > 0.1:
|
|
135
|
+
return "rising"
|
|
136
|
+
elif delta < -0.1:
|
|
137
|
+
return "falling"
|
|
138
|
+
return "flat"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def generate_interpretation(
|
|
142
|
+
feedback_scores: dict, effectiveness: dict, directive: str, logs: list[dict]
|
|
143
|
+
) -> str:
|
|
144
|
+
"""Heuristic interpretation from mechanical metrics — no LLM needed.
|
|
145
|
+
|
|
146
|
+
Replaces the gpt-5-nano call that was returning static output (avg 0.4,
|
|
147
|
+
directive 'stability') regardless of context across 60+ ticks.
|
|
148
|
+
"""
|
|
149
|
+
parts = []
|
|
150
|
+
|
|
151
|
+
trend = compute_score_trend(logs)
|
|
152
|
+
avg = feedback_scores.get("avg", 0)
|
|
153
|
+
rate = effectiveness.get("rate", 0)
|
|
154
|
+
outputs = effectiveness.get("outputs", 0)
|
|
155
|
+
|
|
156
|
+
# Score summary
|
|
157
|
+
if trend == "rising":
|
|
158
|
+
parts.append(f"Scores trending up (avg {avg})")
|
|
159
|
+
elif trend == "falling":
|
|
160
|
+
parts.append(f"Scores trending down (avg {avg}) — review recent patterns")
|
|
161
|
+
elif trend == "flat":
|
|
162
|
+
parts.append(f"Scores flat at avg {avg}")
|
|
163
|
+
else:
|
|
164
|
+
parts.append(f"Too few data points for trend (avg {avg})")
|
|
165
|
+
|
|
166
|
+
# Effectiveness summary
|
|
167
|
+
pos = effectiveness.get("positive", 0)
|
|
168
|
+
neg = effectiveness.get("negative", 0)
|
|
169
|
+
if outputs > 0:
|
|
170
|
+
parts.append(f"Effectiveness {rate:.0%} ({pos} positive, {neg} negative of {outputs} outputs)")
|
|
171
|
+
|
|
172
|
+
# Pattern highlights
|
|
173
|
+
high = feedback_scores.get("high", [])
|
|
174
|
+
low = feedback_scores.get("low", [])
|
|
175
|
+
if high:
|
|
176
|
+
parts.append(f"Working well: {', '.join(high[:3])}")
|
|
177
|
+
if low:
|
|
178
|
+
parts.append(f"Underperforming: {', '.join(low[:3])}")
|
|
179
|
+
|
|
180
|
+
# Directive rationale
|
|
181
|
+
rationale = {
|
|
182
|
+
"aggressive_prune": "Low effectiveness — pruning weak patterns",
|
|
183
|
+
"normal": "Balanced cycle — standard add/prune",
|
|
184
|
+
"stability": "High effectiveness — preserving current patterns",
|
|
185
|
+
"insufficient_data": "Not enough data for effectiveness tuning",
|
|
186
|
+
}
|
|
187
|
+
parts.append(rationale.get(directive, f"Directive: {directive}"))
|
|
188
|
+
|
|
189
|
+
return ". ".join(parts)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def main():
|
|
193
|
+
parser = argparse.ArgumentParser(description="Phase 3: Feedback analysis")
|
|
194
|
+
parser.add_argument("--memory-dir", required=True, help="Path to memory/ directory")
|
|
195
|
+
parser.add_argument("--session-summary", required=True, help="Brief session summary")
|
|
196
|
+
args = parser.parse_args()
|
|
197
|
+
|
|
198
|
+
logs = read_recent_logs(args.memory_dir, days=7)
|
|
199
|
+
|
|
200
|
+
# Fully mechanical computation — no LLM call
|
|
201
|
+
effectiveness = compute_effectiveness(logs)
|
|
202
|
+
feedback_scores = extract_feedback_scores(logs)
|
|
203
|
+
directive = determine_directive(effectiveness)
|
|
204
|
+
interpretation = generate_interpretation(feedback_scores, effectiveness, directive, logs)
|
|
205
|
+
|
|
206
|
+
# Compute skip rate for proactivity tracking
|
|
207
|
+
total_ticks = len(logs)
|
|
208
|
+
skip_ticks = sum(1 for e in logs if e.get("skipped", True))
|
|
209
|
+
skip_rate = round(skip_ticks / total_ticks, 2) if total_ticks > 0 else 1.0
|
|
210
|
+
|
|
211
|
+
output_json({
|
|
212
|
+
"feedbackScores": feedback_scores,
|
|
213
|
+
"effectiveness": effectiveness,
|
|
214
|
+
"curateDirective": directive,
|
|
215
|
+
"interpretation": interpretation,
|
|
216
|
+
"skipRate": skip_rate,
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
main()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Phase 1: Git backup — commit and push any uncommitted changes in the workspace.
|
|
3
|
+
# Runs from the workspace root. Exits 0 on success or nothing to commit, 1 on push failure.
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
changes=$(git status --porcelain 2>/dev/null || true)
|
|
8
|
+
|
|
9
|
+
if [ -z "$changes" ]; then
|
|
10
|
+
echo "nothing to commit"
|
|
11
|
+
exit 0
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
git add -A
|
|
15
|
+
git commit -m "auto: heartbeat $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
16
|
+
git push origin main
|
|
17
|
+
|
|
18
|
+
# Output the commit hash
|
|
19
|
+
git rev-parse --short HEAD
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Phase 3 Step 5: Insight Synthesizer — produce suggestion + insight for Telegram.
|
|
3
|
+
|
|
4
|
+
Uses the "smart" model (configured in koog-config.json) for higher quality output.
|
|
5
|
+
Reads post-curation playbook + recent logs to generate one Telegram message
|
|
6
|
+
with a practical suggestion and a surprising insight.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python3 insight_synthesizer.py --memory-dir memory/ --session-summary "..." \
|
|
10
|
+
[--curator-changes "TEXT"] [--idle]
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
from common import (
|
|
18
|
+
LLMError,
|
|
19
|
+
call_llm,
|
|
20
|
+
extract_json,
|
|
21
|
+
output_json,
|
|
22
|
+
read_effective_playbook,
|
|
23
|
+
read_recent_logs,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
SYSTEM_PROMPT = """\
|
|
27
|
+
You are the insight synthesizer for sinain, a personal AI assistant.
|
|
28
|
+
Your job: produce ONE high-quality Telegram message with two parts.
|
|
29
|
+
|
|
30
|
+
**Suggestion** (1-2 sentences): A practical, actionable recommendation.
|
|
31
|
+
- MUST reference a specific playbook pattern or concrete observation
|
|
32
|
+
- NOT generic advice — grounded in actual data
|
|
33
|
+
- Could be: workflow improvement, recurring problem to automate, successful pattern to replicate
|
|
34
|
+
|
|
35
|
+
**Insight** (1-2 sentences): A surprising, non-obvious connection from accumulated data.
|
|
36
|
+
- MUST connect 2+ distinct observations that aren't obviously related
|
|
37
|
+
- Cross-domain patterns, unexpected correlations, things the user hasn't noticed
|
|
38
|
+
- Cite specific observations from playbook or logs
|
|
39
|
+
|
|
40
|
+
Quality gate — you MUST skip if:
|
|
41
|
+
- You cannot produce BOTH a genuinely useful suggestion AND a genuinely surprising insight
|
|
42
|
+
- The suggestion would repeat something from recent heartbeat outputs
|
|
43
|
+
- The insight is obvious or doesn't connect distinct observations
|
|
44
|
+
|
|
45
|
+
Proactivity rules:
|
|
46
|
+
- On ACTIVE days, LOWER your skip threshold — the user wants daily engagement
|
|
47
|
+
- If the last 3+ outputs were skipped, you MUST produce output (find something worth saying)
|
|
48
|
+
- Prefer a moderate-quality insight over silence — the user explicitly asked for more proactive insights
|
|
49
|
+
- When idle, focus on mined pattern synthesis and cross-day trend observations
|
|
50
|
+
|
|
51
|
+
Total message MUST be under 500 characters.
|
|
52
|
+
|
|
53
|
+
Respond with ONLY a JSON object. If producing output:
|
|
54
|
+
{
|
|
55
|
+
"skip": false,
|
|
56
|
+
"suggestion": "the suggestion text",
|
|
57
|
+
"insight": "the insight text"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
If skipping (be specific about WHY, citing what you read):
|
|
61
|
+
{
|
|
62
|
+
"skip": true,
|
|
63
|
+
"skipReason": "specific reason citing files/patterns examined — 'no new data' is NOT valid"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
When your suggestion or insight references a pattern from a section marked
|
|
67
|
+
"[Transferred knowledge: ...]", naturally mention this origin (e.g. "Based on
|
|
68
|
+
transferred OCR expertise, ..."). Keep the attribution brief and natural."""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def build_user_prompt(
|
|
72
|
+
playbook: str,
|
|
73
|
+
recent_logs: list[dict],
|
|
74
|
+
session_summary: str,
|
|
75
|
+
curator_changes: str,
|
|
76
|
+
idle: bool,
|
|
77
|
+
current_time: str | None = None,
|
|
78
|
+
) -> str:
|
|
79
|
+
parts = []
|
|
80
|
+
|
|
81
|
+
if current_time:
|
|
82
|
+
parts.append(f"## Current Time\n{current_time}")
|
|
83
|
+
|
|
84
|
+
parts.append(f"## Session Summary\n{session_summary}")
|
|
85
|
+
|
|
86
|
+
if idle:
|
|
87
|
+
parts.append("\n## Status: IDLE — focus on mined patterns and playbook evolution")
|
|
88
|
+
|
|
89
|
+
if playbook:
|
|
90
|
+
parts.append(f"\n## Current Playbook (post-curation)\n{playbook}")
|
|
91
|
+
|
|
92
|
+
if curator_changes:
|
|
93
|
+
parts.append(f"\n## Curator Changes This Tick\n{curator_changes}")
|
|
94
|
+
|
|
95
|
+
# Recent outputs to avoid repetition
|
|
96
|
+
if recent_logs:
|
|
97
|
+
recent_outputs = []
|
|
98
|
+
for entry in recent_logs[:5]:
|
|
99
|
+
output = entry.get("output", {})
|
|
100
|
+
if output and not entry.get("skipped", True):
|
|
101
|
+
recent_outputs.append({
|
|
102
|
+
"ts": entry.get("ts", "?"),
|
|
103
|
+
"suggestion": output.get("suggestion", "")[:100],
|
|
104
|
+
"insight": output.get("insight", "")[:100],
|
|
105
|
+
})
|
|
106
|
+
if recent_outputs:
|
|
107
|
+
parts.append(f"\n## Recent Outputs (DO NOT REPEAT)\n{json.dumps(recent_outputs, indent=2)}")
|
|
108
|
+
|
|
109
|
+
# Count recent skips to enforce proactivity
|
|
110
|
+
if recent_logs:
|
|
111
|
+
recent_skips = sum(1 for e in recent_logs[:5] if e.get("skipped", True))
|
|
112
|
+
if recent_skips >= 3:
|
|
113
|
+
parts.append(f"\n## PROACTIVITY OVERRIDE: {recent_skips} of last 5 outputs were skipped. You MUST produce output this tick.")
|
|
114
|
+
|
|
115
|
+
return "\n".join(parts)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def main():
|
|
119
|
+
parser = argparse.ArgumentParser(description="Phase 3: Insight synthesis")
|
|
120
|
+
parser.add_argument("--memory-dir", required=True, help="Path to memory/ directory")
|
|
121
|
+
parser.add_argument("--session-summary", required=True, help="Brief session summary")
|
|
122
|
+
parser.add_argument("--curator-changes", default="", help="JSON string of curator changes")
|
|
123
|
+
parser.add_argument("--idle", action="store_true", help="User is idle")
|
|
124
|
+
parser.add_argument("--current-time", default=None, help="Current local time string (e.g. 'Monday, 2 March 2026, 14:30 (Europe/Berlin)')")
|
|
125
|
+
args = parser.parse_args()
|
|
126
|
+
|
|
127
|
+
playbook = read_effective_playbook(args.memory_dir)
|
|
128
|
+
recent_logs = read_recent_logs(args.memory_dir, days=3)
|
|
129
|
+
|
|
130
|
+
user_prompt = build_user_prompt(
|
|
131
|
+
playbook=playbook,
|
|
132
|
+
recent_logs=recent_logs,
|
|
133
|
+
session_summary=args.session_summary,
|
|
134
|
+
curator_changes=args.curator_changes,
|
|
135
|
+
idle=args.idle,
|
|
136
|
+
current_time=args.current_time,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
raw = call_llm(SYSTEM_PROMPT, user_prompt, script="insight_synthesizer", json_mode=True)
|
|
141
|
+
result = extract_json(raw)
|
|
142
|
+
except (ValueError, LLMError) as e:
|
|
143
|
+
print(f"[warn] {e}", file=sys.stderr)
|
|
144
|
+
result = {
|
|
145
|
+
"skip": True,
|
|
146
|
+
"skipReason": "LLM response was not parseable JSON",
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# Enforce character limit on non-skip output
|
|
150
|
+
if not result.get("skip", False):
|
|
151
|
+
suggestion = result.get("suggestion", "")
|
|
152
|
+
insight = result.get("insight", "")
|
|
153
|
+
total_chars = len(suggestion) + len(insight)
|
|
154
|
+
|
|
155
|
+
if total_chars > 500:
|
|
156
|
+
# Truncate insight to fit
|
|
157
|
+
max_insight = 500 - len(suggestion) - 10 # buffer
|
|
158
|
+
if max_insight > 50:
|
|
159
|
+
truncated = insight[:max_insight]
|
|
160
|
+
# Cut at last sentence boundary
|
|
161
|
+
last_period = truncated.rfind(".")
|
|
162
|
+
last_excl = truncated.rfind("!")
|
|
163
|
+
last_q = truncated.rfind("?")
|
|
164
|
+
boundary = max(last_period, last_excl, last_q)
|
|
165
|
+
if boundary > max_insight // 2:
|
|
166
|
+
insight = truncated[:boundary + 1]
|
|
167
|
+
else:
|
|
168
|
+
insight = truncated.rstrip() + "..."
|
|
169
|
+
result["insight"] = insight
|
|
170
|
+
total_chars = len(suggestion) + len(insight)
|
|
171
|
+
else:
|
|
172
|
+
result["skip"] = True
|
|
173
|
+
result["skipReason"] = f"Output exceeded 500 chars ({total_chars}) and could not be trimmed"
|
|
174
|
+
|
|
175
|
+
result["totalChars"] = len(result.get("suggestion", "")) + len(result.get("insight", ""))
|
|
176
|
+
|
|
177
|
+
output_json(result)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
if __name__ == "__main__":
|
|
181
|
+
main()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# 2026-03-01 Session Notes
|
|
2
|
+
|
|
3
|
+
## OCR Pipeline
|
|
4
|
+
- Switched from Tesseract to OpenRouter vision API
|
|
5
|
+
- Backpressure issue: camera produces frames faster than API can process
|
|
6
|
+
- Solution: frame dropping with scene-gate (skip similar consecutive frames)
|
|
7
|
+
|
|
8
|
+
## Wearable HUD
|
|
9
|
+
- Testing 3-panel debug interface
|
|
10
|
+
- Camera feed, OCR overlay, and pipeline stats side-by-side
|
|
11
|
+
- Found that JPEG quality 70 is good balance of speed vs readability
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!-- mining-index: 2026-03-01 -->
|
|
2
|
+
# Sinain Playbook
|
|
3
|
+
|
|
4
|
+
## Established Patterns
|
|
5
|
+
- When OCR pipeline stalls, check camera frame queue depth (score: 0.8)
|
|
6
|
+
- When user explores new framework, spawn research agent proactively (score: 0.6)
|
|
7
|
+
|
|
8
|
+
## Observed
|
|
9
|
+
- User prefers concise Telegram messages over detailed ones
|
|
10
|
+
- Late evening sessions tend to be exploratory/research-heavy
|
|
11
|
+
|
|
12
|
+
## Stale
|
|
13
|
+
- Flutter overlay rendering glitch on macOS 15 [since: 2026-02-18]
|
|
14
|
+
|
|
15
|
+
<!-- effectiveness: outputs=8, positive=5, negative=1, neutral=2, rate=0.63, updated=2026-02-21 -->
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"ts": "2026-03-01T14:18:16.183587+00:00", "idle": false, "sessionHistorySummary": "User debugging OCR pipeline", "feedbackScores": {"avg": 0.35, "high": ["OCR fix suggestion"], "low": []}, "actionsConsidered": [{"action": "spawn research", "reason": "Flutter overlay perf", "chosen": false, "skipReason": "not urgent"}], "effectivenessRate": 0.63, "effectivenessAlert": false, "playbookChanges": {"added": [], "pruned": [], "promoted": []}, "output": {"suggestion": "Consider frame batching for OCR pipeline", "insight": "Evening sessions correlate with exploratory work"}, "skipped": false}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<!-- mining-index: 2026-03-01 -->
|
|
2
|
+
|
|
3
|
+
# Sinain Playbook
|
|
4
|
+
|
|
5
|
+
## Established Patterns
|
|
6
|
+
- When OCR pipeline stalls, check camera frame queue depth (score: 0.8)
|
|
7
|
+
- When user explores new framework, spawn research agent proactively (score: 0.6)
|
|
8
|
+
- When OCR backend switches (e.g., Tesseract → vision API), validate latency and cost tradeoffs before committing (score: 0.7)
|
|
9
|
+
|
|
10
|
+
## Observed
|
|
11
|
+
- User prefers concise Telegram messages over detailed ones
|
|
12
|
+
- Late evening sessions tend to be exploratory/research-heavy
|
|
13
|
+
- Scene-gating (drop frames on low scene change) is preferred approach for OCR backpressure
|
|
14
|
+
- JPEG quality tuning is part of wearable pipeline optimization workflow
|
|
15
|
+
- User converging on 3-panel HUD layout for wearable debug interface
|
|
16
|
+
- Flutter overlay on macOS is an active exploration area (not settled)
|
|
17
|
+
|
|
18
|
+
## Stale
|
|
19
|
+
- Flutter overlay rendering glitch on macOS 15 [since: 2026-02-18]
|
|
20
|
+
|
|
21
|
+
<!-- effectiveness: outputs=8, positive=5, negative=1, neutral=2, rate=0.63, updated=2026-02-21 -->
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"models": {
|
|
3
|
+
"fast": "google/gemini-3-flash-preview",
|
|
4
|
+
"smart": "anthropic/claude-sonnet-4.6"
|
|
5
|
+
},
|
|
6
|
+
"scripts": {
|
|
7
|
+
"signal_analyzer": { "model": "fast", "maxTokens": 1500 },
|
|
8
|
+
"feedback_analyzer": { "model": "fast", "maxTokens": 800 },
|
|
9
|
+
"memory_miner": { "model": "smart", "maxTokens": 1500 },
|
|
10
|
+
"playbook_curator": { "model": "fast", "maxTokens": 3000, "timeout": 90 },
|
|
11
|
+
"insight_synthesizer": { "model": "smart", "maxTokens": 800 },
|
|
12
|
+
"module_manager": { "model": "fast", "maxTokens": 2000 },
|
|
13
|
+
"tick_evaluator": { "model": "smart", "maxTokens": 200, "timeout": 30 },
|
|
14
|
+
"eval_reporter": { "model": "smart", "maxTokens": 1000 },
|
|
15
|
+
"triple_extractor": { "model": "fast", "maxTokens": 1500, "timeout": 30 }
|
|
16
|
+
},
|
|
17
|
+
"defaults": { "model": "fast", "maxTokens": 1500 },
|
|
18
|
+
"triplestore": {
|
|
19
|
+
"dbPath": "memory/triplestore.db",
|
|
20
|
+
"maxTriplesPerTx": 100,
|
|
21
|
+
"conceptExtractionMode": "tiered",
|
|
22
|
+
"gcOlderThanDays": 30
|
|
23
|
+
},
|
|
24
|
+
"eval": {
|
|
25
|
+
"level": "sampled",
|
|
26
|
+
"sampleRate": 0.3,
|
|
27
|
+
"judges": {
|
|
28
|
+
"model": "smart",
|
|
29
|
+
"maxTokens": 200,
|
|
30
|
+
"timeout": 30
|
|
31
|
+
},
|
|
32
|
+
"dailyReport": true,
|
|
33
|
+
"regressionThresholds": {
|
|
34
|
+
"assertionPassRate": 0.85,
|
|
35
|
+
"effectivenessRate": 0.4,
|
|
36
|
+
"skipRate": 0.8
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|