Skip to content

Commit 6e21496

Browse files
committed
Added session context to graph and dashboard md
1 parent 1eb1cd9 commit 6e21496

6 files changed

Lines changed: 437 additions & 14 deletions

File tree

.claude/settings.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"permissions": {
3+
"additionalDirectories": [
4+
"/Users/patrick/ccmemory"
5+
]
6+
}
7+
}

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ The vision doc synthesizes these articles with Patrick's thinking. If vision doc
3131
- Only rebuild/redeploy docker images when debugging container-specific issues
3232
- Use `uv` for all Python commands (e.g., `uv run pytest`, `uv pip install`)
3333
- **AS_BUILT.md**: Read `doc/AS_BUILT.md` before making changes. **ALWAYS update it after completing implementation work** — this is mandatory, not optional
34+
- **DASHBOARD.md**: Update `doc/DASHBOARD.md` when work queue items change status (completed, new items discovered, priorities shift). This is the user's return-to-project reference.
3435

3536
## Session Startup Requirement
3637

dashboard/app.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,161 @@ def retrievals():
635635
return jsonify([serialize_node(dict(r["r"])) for r in result])
636636

637637

638+
@app.route("/api/session-context")
639+
def session_context():
640+
project = request.args.get("project", "")
641+
if not project:
642+
return jsonify({"error": "Project required"}), 400
643+
644+
driver = getDriver()
645+
parts = []
646+
ids = []
647+
648+
with driver.session() as session:
649+
# Project Facts
650+
facts_result = session.run(
651+
"""
652+
MATCH (pf:ProjectFact {project: $project})
653+
RETURN pf ORDER BY pf.timestamp DESC LIMIT 15
654+
""",
655+
project=project,
656+
)
657+
facts = [dict(r["pf"]) for r in facts_result]
658+
659+
if facts:
660+
parts.append(
661+
"## Project Rules (from context graph — treat as custom instructions)"
662+
)
663+
parts.append("")
664+
665+
by_category = {}
666+
for f in facts:
667+
if f.get("id"):
668+
ids.append(f["id"])
669+
cat = f.get("category", "general")
670+
if cat not in by_category:
671+
by_category[cat] = []
672+
by_category[cat].append(f.get("fact", ""))
673+
674+
for cat, items in by_category.items():
675+
parts.append(f"### {cat.title()}")
676+
for item in items[:5]:
677+
parts.append(f"- {item[:120]}")
678+
parts.append("")
679+
680+
# Recent items (union query)
681+
recent_result = session.run(
682+
"""
683+
CALL {
684+
MATCH (n:Decision {project: $project})
685+
RETURN n, 'Decision' as node_type, n.timestamp as ts
686+
UNION ALL
687+
MATCH (n:Correction {project: $project})
688+
RETURN n, 'Correction' as node_type, n.timestamp as ts
689+
UNION ALL
690+
MATCH (n:Insight {project: $project})
691+
RETURN n, 'Insight' as node_type, n.timestamp as ts
692+
UNION ALL
693+
MATCH (n:Exception {project: $project})
694+
RETURN n, 'Exception' as node_type, n.timestamp as ts
695+
UNION ALL
696+
MATCH (n:FailedApproach {project: $project})
697+
RETURN n, 'FailedApproach' as node_type, n.timestamp as ts
698+
}
699+
RETURN n, node_type ORDER BY ts DESC LIMIT 15
700+
""",
701+
project=project,
702+
)
703+
recent = [{"n": dict(r["n"]), "node_type": r["node_type"]} for r in recent_result]
704+
705+
if recent:
706+
parts.append("## Recent Decisions")
707+
for item in recent[:10]:
708+
node = item.get("n", {})
709+
node_type = item.get("node_type", "")
710+
if not node:
711+
continue
712+
if node.get("id"):
713+
ids.append(node["id"])
714+
715+
topics = node.get("topics", [])
716+
topic_str = f"[{', '.join(topics[:2])}] " if topics else ""
717+
718+
if node_type == "Decision":
719+
parts.append(
720+
f"- {topic_str}{str(node.get('description', ''))[:100]}"
721+
)
722+
elif node_type == "Correction":
723+
parts.append(
724+
f"- CORRECTION: {topic_str}{str(node.get('right_belief', ''))[:100]}"
725+
)
726+
elif node_type == "Insight":
727+
parts.append(
728+
f"- Insight: {topic_str}{str(node.get('summary', ''))[:100]}"
729+
)
730+
elif node_type == "Exception":
731+
parts.append(
732+
f"- Exception to '{node.get('rule_broken', '')[:40]}': {str(node.get('justification', ''))[:60]}"
733+
)
734+
parts.append("")
735+
736+
# Failed approaches
737+
failed_result = session.run(
738+
"""
739+
MATCH (f:FailedApproach {project: $project})
740+
RETURN f ORDER BY f.timestamp DESC LIMIT 5
741+
""",
742+
project=project,
743+
)
744+
failed = [dict(r["f"]) for r in failed_result]
745+
746+
if failed:
747+
parts.append("## Things That Didn't Work (Don't Repeat)")
748+
for f in failed[:5]:
749+
if f.get("id"):
750+
ids.append(f["id"])
751+
approach = str(f.get("approach", ""))[:50]
752+
lesson = str(f.get("lesson", ""))[:60]
753+
parts.append(f"- **{approach}**: {lesson}")
754+
parts.append("")
755+
756+
# Stale decisions
757+
stale_result = session.run(
758+
"""
759+
MATCH (d:Decision {project: $project})
760+
WHERE d.status = 'developmental'
761+
AND d.timestamp < datetime() - duration({days: 30})
762+
RETURN d ORDER BY d.timestamp DESC LIMIT 3
763+
""",
764+
project=project,
765+
)
766+
stale = [dict(r["d"]) for r in stale_result]
767+
768+
if stale:
769+
parts.append("## Decisions Needing Review")
770+
for d in stale[:3]:
771+
if d.get("id"):
772+
ids.append(d["id"])
773+
desc = str(d.get("description", ""))[:80]
774+
parts.append(f"- {desc} *(developmental, may need revisit)*")
775+
parts.append("")
776+
777+
# Empty state
778+
if not facts and not recent and not stale and not failed:
779+
parts.append("# Context Graph")
780+
parts.append(f"Project: {project}")
781+
parts.append("")
782+
parts.append(
783+
"No prior context. Project facts, decisions, and corrections will be captured automatically."
784+
)
785+
786+
context_text = "\n".join(parts)
787+
return jsonify({
788+
"context": context_text,
789+
"retrieved_count": len(ids),
790+
})
791+
792+
638793
@app.route("/api/projects")
639794
def projects():
640795
driver = getDriver()

dashboard/templates/dashboard.html

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@
6161
#node-detail { max-height: 200px; overflow-y: auto; display: none; }
6262
#node-detail.is-visible { display: block; }
6363
.node-detail-label { font-weight: 600; }
64+
.msg-compact { padding: 0.75rem; }
65+
#context-preview-content { white-space: pre-wrap; word-break: break-word; }
66+
#context-preview-box { max-height: 400px; overflow-y: auto; }
6467
</style>
6568
<script src="https://unpkg.com/cytoscape@3.28.1/dist/cytoscape.min.js"></script>
6669
</head>
@@ -210,6 +213,34 @@ <h2 class="title is-4">Knowledge Graph</h2>
210213
</div>
211214
</div>
212215
</div>
216+
<!-- Session Context Preview (collapsible) -->
217+
<details class="mt-4" id="context-preview-details">
218+
<summary class="is-clickable has-text-weight-semibold">
219+
Session Context Preview
220+
<span class="tag is-info is-light ml-2" id="context-item-count">0 items</span>
221+
</summary>
222+
<div class="mt-3">
223+
<article class="message is-info is-small">
224+
<div class="message-body msg-compact">
225+
<strong>When is this injected?</strong> This context is automatically injected at the start of every Claude Code session via the <code>session_start.sh</code> hook. Claude receives this as additional system context before your first message.
226+
</div>
227+
</article>
228+
<article class="message is-small">
229+
<div class="message-body msg-compact">
230+
<strong>How is it synthesized?</strong> The context is assembled from the graph in priority order:
231+
<ol class="ml-4 mt-1 is-size-7">
232+
<li><strong>Project Facts</strong> (up to 15) — grouped by category, treated as binding instructions</li>
233+
<li><strong>Recent Items</strong> (up to 10) — Decisions, Corrections, Insights, Exceptions sorted by timestamp</li>
234+
<li><strong>Failed Approaches</strong> (up to 5) — explicitly surfaced to prevent repeated mistakes</li>
235+
<li><strong>Stale Decisions</strong> (up to 3) — developmental decisions older than 30 days flagged for review</li>
236+
</ol>
237+
</div>
238+
</article>
239+
<div class="box" id="context-preview-box">
240+
<pre id="context-preview-content" class="is-size-7">Loading...</pre>
241+
</div>
242+
</div>
243+
</details>
213244
</div>
214245
</div>
215246
</div>
@@ -654,6 +685,25 @@ <h2 class="title is-5 mb-0">Developer Console</h2>
654685
if (e.key === 'Enter') doSearch();
655686
});
656687

688+
async function loadContextPreview() {
689+
try {
690+
const response = await fetch(`/api/session-context?project=${project}`);
691+
const data = await response.json();
692+
document.getElementById('context-preview-content').textContent = data.context || 'No context available';
693+
document.getElementById('context-item-count').textContent = `${data.retrieved_count || 0} items`;
694+
} catch (error) {
695+
console.error('Failed to load context preview:', error);
696+
document.getElementById('context-preview-content').textContent = 'Failed to load context';
697+
}
698+
}
699+
700+
// Load context when details is opened (lazy load)
701+
document.getElementById('context-preview-details').addEventListener('toggle', function() {
702+
if (this.open) {
703+
loadContextPreview();
704+
}
705+
});
706+
657707
loadMetrics();
658708
updateLegend();
659709
initGraph();
@@ -719,6 +769,18 @@ <h2 class="title is-5 mb-0">Developer Console</h2>
719769
try {
720770
const log = JSON.parse(e.data);
721771
const cat = log.cat || 'mcp';
772+
773+
// Auto-refresh graph/context when nodes are created for this project
774+
if (log.event === 'node_created' && log.project === project) {
775+
initGraph();
776+
loadMetrics();
777+
// Refresh context preview if it's open
778+
const details = document.getElementById('context-preview-details');
779+
if (details.open) {
780+
loadContextPreview();
781+
}
782+
}
783+
722784
if (!filters[cat]) return;
723785

724786
const div = document.createElement('div');

0 commit comments

Comments
 (0)