Skip to content

Commit ea9d9d0

Browse files
committed
fix: audit fixes — heuristic dedup, config paths, message atomicity, toggle defaults, memory counts, session cleanup
1 parent 4ebc004 commit ea9d9d0

10 files changed

Lines changed: 98 additions & 32 deletions

File tree

packages/dashboard/src-tauri/src/config.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,22 @@ pub fn resolve_user_config_path() -> PathBuf {
99
config_dir.join("opencode").join("magic-context.jsonc")
1010
}
1111

12+
/// Resolve the active magic-context config path for a project.
13+
/// Checks root first, then `.opencode/` alt path. Returns the first that exists,
14+
/// or root path as default for new config creation.
1215
pub fn resolve_project_config_path(project_path: &str) -> PathBuf {
13-
PathBuf::from(project_path).join("magic-context.jsonc")
16+
let root_config = PathBuf::from(project_path).join("magic-context.jsonc");
17+
if root_config.exists() {
18+
return root_config;
19+
}
20+
let alt_config = PathBuf::from(project_path)
21+
.join(".opencode")
22+
.join("magic-context.jsonc");
23+
if alt_config.exists() {
24+
return alt_config;
25+
}
26+
// Default to root path for new configs
27+
root_config
1428
}
1529

1630
#[derive(Debug, Serialize, Deserialize, Clone)]

packages/dashboard/src-tauri/src/db.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ pub fn open_readwrite(path: &PathBuf) -> Result<Connection, rusqlite::Error> {
7070
let conn = Connection::open(path)?;
7171
conn.pragma_update(None, "journal_mode", "WAL")?;
7272
conn.pragma_update(None, "busy_timeout", 5000)?;
73+
conn.pragma_update(None, "foreign_keys", "ON")?;
7374
Ok(conn)
7475
}
7576

packages/dashboard/src/components/ConfigEditor/ConfigEditor.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -622,11 +622,11 @@ function ConfigForm(props: {
622622
<label class="toggle-switch">
623623
<input
624624
type="checkbox"
625-
checked={getNestedValue(formData(), "dreamer.enabled") as boolean ?? true}
625+
checked={getNestedValue(formData(), "dreamer.enabled") as boolean ?? false}
626626
onChange={(e) => handleFieldChange("dreamer.enabled", e.currentTarget.checked)}
627627
/>
628628
<span class="toggle-slider" />
629-
<span class="toggle-label">{(getNestedValue(formData(), "dreamer.enabled") as boolean ?? true) ? "Enabled" : "Disabled"}</span>
629+
<span class="toggle-label">{(getNestedValue(formData(), "dreamer.enabled") as boolean ?? false) ? "Enabled" : "Disabled"}</span>
630630
</label>
631631
</div>
632632

@@ -652,11 +652,11 @@ function ConfigForm(props: {
652652
<label class="toggle-switch">
653653
<input
654654
type="checkbox"
655-
checked={getNestedValue(formData(), "dreamer.inject_docs") as boolean ?? true}
655+
checked={getNestedValue(formData(), "dreamer.inject_docs") as boolean ?? false}
656656
onChange={(e) => handleFieldChange("dreamer.inject_docs", e.currentTarget.checked)}
657657
/>
658658
<span class="toggle-slider" />
659-
<span class="toggle-label">{(getNestedValue(formData(), "dreamer.inject_docs") as boolean ?? true) ? "Enabled" : "Disabled"}</span>
659+
<span class="toggle-label">{(getNestedValue(formData(), "dreamer.inject_docs") as boolean ?? false) ? "Enabled" : "Disabled"}</span>
660660
</label>
661661
</div>
662662
</div>
@@ -739,11 +739,11 @@ function ConfigForm(props: {
739739
<label class="toggle-switch">
740740
<input
741741
type="checkbox"
742-
checked={getNestedValue(formData(), "sidekick.enabled") as boolean ?? true}
742+
checked={getNestedValue(formData(), "sidekick.enabled") as boolean ?? false}
743743
onChange={(e) => handleFieldChange("sidekick.enabled", e.currentTarget.checked)}
744744
/>
745745
<span class="toggle-slider" />
746-
<span class="toggle-label">{(getNestedValue(formData(), "sidekick.enabled") as boolean ?? true) ? "Enabled" : "Disabled"}</span>
746+
<span class="toggle-label">{(getNestedValue(formData(), "sidekick.enabled") as boolean ?? false) ? "Enabled" : "Disabled"}</span>
747747
</label>
748748
</div>
749749

packages/plugin/src/features/magic-context/memory/storage-memory.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -619,19 +619,18 @@ export function getMemoryCountsByStatus(db: Database, projectPath: string): Memo
619619
for (const row of rows) {
620620
counts.ids.push(row.id);
621621

622-
if (row.status === "active") {
622+
// Count merged memories separately — they should not also count as archived
623+
if (typeof row.superseded_by_memory_id === "number") {
624+
counts.merged += 1;
625+
counts.mergedIds.push(row.id);
626+
} else if (row.status === "active") {
623627
counts.active += 1;
624628
} else if (row.status === "permanent") {
625629
counts.permanent += 1;
626630
} else {
627631
counts.archived += 1;
628632
counts.archivedIds.push(row.id);
629633
}
630-
631-
if (typeof row.superseded_by_memory_id === "number") {
632-
counts.merged += 1;
633-
counts.mergedIds.push(row.id);
634-
}
635634
}
636635

637636
counts.ids.sort((left, right) => left - right);

packages/plugin/src/features/magic-context/plugin-messages.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import type { Database } from "bun:sqlite";
2121

2222
export type MessageDirection = "server_to_tui" | "tui_to_server";
2323

24-
export type MessageType = "toast" | "dialog_confirm" | "dialog_result" | "state_update";
24+
export type MessageType = "toast" | "dialog_confirm" | "dialog_result" | "state_update" | "action";
2525

2626
export interface PluginMessage {
2727
id: number;
@@ -136,17 +136,23 @@ export function consumeMessages(
136136
}
137137

138138
const query = `SELECT * FROM plugin_messages WHERE ${conditions.join(" AND ")} ORDER BY created_at ASC`;
139-
const rows = db.prepare(query).all(...params);
140-
const messages = rows.filter(isPluginMessageRow).map(toPluginMessage);
141-
142-
if (messages.length > 0) {
143-
const ids = messages.map((m) => m.id);
144-
db.prepare(
145-
`UPDATE plugin_messages SET consumed_at = ? WHERE id IN (${ids.map(() => "?").join(",")})`,
146-
).run(now, ...ids);
147-
}
148139

149-
// Periodic cleanup of old messages
140+
// Atomic read+mark: transaction prevents TUI and server from consuming the same messages
141+
const messages = db.transaction(() => {
142+
const rows = db.prepare(query).all(...params);
143+
const result = rows.filter(isPluginMessageRow).map(toPluginMessage);
144+
145+
if (result.length > 0) {
146+
const ids = result.map((m) => m.id);
147+
db.prepare(
148+
`UPDATE plugin_messages SET consumed_at = ? WHERE id IN (${ids.map(() => "?").join(",")})`,
149+
).run(now, ...ids);
150+
}
151+
152+
return result;
153+
})();
154+
155+
// Periodic cleanup of old messages (outside transaction — non-critical)
150156
db.prepare("DELETE FROM plugin_messages WHERE created_at < ?").run(now - CLEANUP_THRESHOLD_MS);
151157

152158
return messages;

packages/plugin/src/features/magic-context/storage-meta-session.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export function clearSession(db: Database, sessionId: string): void {
7676
db.prepare("DELETE FROM notes WHERE session_id = ? AND type = 'session'").run(sessionId);
7777
db.prepare("DELETE FROM recomp_compartments WHERE session_id = ?").run(sessionId);
7878
db.prepare("DELETE FROM recomp_facts WHERE session_id = ?").run(sessionId);
79+
db.prepare("DELETE FROM user_memory_candidates WHERE session_id = ?").run(sessionId);
7980
clearIndexedMessages(db, sessionId);
8081
})();
8182
}

packages/plugin/src/features/magic-context/storage-meta.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ describe("storage-meta", () => {
8383
//#then
8484
// 2 transactions: outer clearSession + nested clearIndexedMessages
8585
expect(db.transaction).toHaveBeenCalledTimes(2);
86-
expect(db.prepare).toHaveBeenCalledTimes(12);
86+
expect(db.prepare).toHaveBeenCalledTimes(13);
8787
});
8888
});
8989
});

packages/plugin/src/hooks/magic-context/heuristic-cleanup.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,13 @@ export function applyHeuristicCleanup(
150150
function extractToolInfo(
151151
part: Record<string, unknown>,
152152
): { toolName: string; args: unknown } | null {
153-
// OpenCode format: { type: "tool", state: { tool: "name", input: {...} } }
154-
if (part.type === "tool" && typeof part.state === "object" && part.state !== null) {
155-
const state = part.state as Record<string, unknown>;
156-
if (typeof state.tool === "string" && DEDUP_SAFE_TOOLS.has(state.tool)) {
157-
return { toolName: state.tool, args: state.input ?? {} };
158-
}
153+
// OpenCode format: { type: "tool", tool: "name", callID: "...", state: { input: {...}, output: "..." } }
154+
if (part.type === "tool" && typeof part.tool === "string" && DEDUP_SAFE_TOOLS.has(part.tool)) {
155+
const state =
156+
typeof part.state === "object" && part.state !== null
157+
? (part.state as Record<string, unknown>)
158+
: {};
159+
return { toolName: part.tool, args: state.input ?? {} };
159160
}
160161
// Tool-invocation format: { type: "tool-invocation", toolName: "name", args: {...} }
161162
if (

packages/plugin/src/plugin/tui-action-consumer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function startTuiActionConsumer(args: {
2727
const timer = setInterval(() => {
2828
try {
2929
const db = openDatabase();
30-
const actions = consumeMessages(db, "tui_to_server", { type: "action" as never });
30+
const actions = consumeMessages(db, "tui_to_server", { type: "action" });
3131

3232
for (const msg of actions) {
3333
const command = msg.payload.command;

scripts/wait-release.sh

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/usr/bin/env bash
2+
# Wait for the GitHub Actions release workflow to complete for a given tag.
3+
# Usage: ./scripts/wait-release.sh v0.6.1
4+
# Polls every 5 seconds, exits 0 on success, 1 on failure.
5+
6+
set -euo pipefail
7+
8+
TAG="${1:?Usage: wait-release.sh <tag>}"
9+
REPO="cortexkit/opencode-magic-context"
10+
INTERVAL=5
11+
12+
echo "⏳ Waiting for release workflow on ${TAG}..."
13+
14+
while true; do
15+
# Get the latest run for the release workflow triggered by this tag
16+
RUN_JSON=$(gh run list --repo "$REPO" --workflow release.yml --branch "$TAG" --limit 1 --json status,conclusion,databaseId 2>/dev/null || echo "[]")
17+
18+
STATUS=$(echo "$RUN_JSON" | jq -r '.[0].status // "not_found"')
19+
CONCLUSION=$(echo "$RUN_JSON" | jq -r '.[0].conclusion // ""')
20+
RUN_ID=$(echo "$RUN_JSON" | jq -r '.[0].databaseId // ""')
21+
22+
if [ "$STATUS" = "not_found" ]; then
23+
printf "\r Workflow not started yet..."
24+
sleep "$INTERVAL"
25+
continue
26+
fi
27+
28+
if [ "$STATUS" = "completed" ]; then
29+
if [ "$CONCLUSION" = "success" ]; then
30+
echo ""
31+
echo "✅ Release workflow succeeded (run $RUN_ID)"
32+
echo " https://github.com/$REPO/actions/runs/$RUN_ID"
33+
exit 0
34+
else
35+
echo ""
36+
echo "❌ Release workflow failed: conclusion=$CONCLUSION (run $RUN_ID)"
37+
echo " https://github.com/$REPO/actions/runs/$RUN_ID"
38+
exit 1
39+
fi
40+
fi
41+
42+
printf "\r Status: %-12s (run $RUN_ID)" "$STATUS"
43+
sleep "$INTERVAL"
44+
done

0 commit comments

Comments
 (0)