pi-mega-compact 0.8.0 → 0.8.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.
@@ -17,10 +17,16 @@ export async function launchDashboardServer(stateDir) {
17
17
  // upgrade instead of reuse it.
18
18
  let SERVER_VERSION = "0.0.0";
19
19
  try {
20
- // dashboard-server.js lives at <pkg>/dist/extensions/, so package.json is
21
- // two levels up. Guard each candidate so a dev-checkout layout still works.
20
+ // Since v0.7.9 (8821ef3) dashboard-server.js lives at
21
+ // <pkg>/dist/extensions/dashboard-server/, so package.json is THREE levels
22
+ // up. Keep the two- and one-level-up candidates as fallbacks for flatter
23
+ // dev-checkout layouts. Guard each candidate so a missing file is skipped.
22
24
  const here = dirname(fileURLToPath(import.meta.url));
23
- const candidates = [join(here, "..", "..", "package.json"), join(here, "..", "package.json")];
25
+ const candidates = [
26
+ join(here, "..", "..", "..", "package.json"),
27
+ join(here, "..", "..", "package.json"),
28
+ join(here, "..", "package.json"),
29
+ ];
24
30
  for (const p of candidates) {
25
31
  if (!existsSync(p))
26
32
  continue;
@@ -18,7 +18,7 @@
18
18
  * - mega-runtime.ts shared live state (MegaRuntime) + widget + model capture
19
19
  * - mega-pipeline.ts runCompact (Trident+persist) + doRecall (Layer 5)
20
20
  * - mega-commands.ts data/inspection slash commands
21
- * - mega-game-cmds.ts /mega-game toggle + theme + TUI display mode
21
+ * - mega-game-cmds.ts /mega-compact-settings (+ /mega-game alias) toggle + theme + TUI display mode
22
22
  * - mega-dashboard-cmds.ts localhost dashboard server lifecycle commands
23
23
  * - mega-events.ts pi lifecycle event handlers
24
24
  *
@@ -50,15 +50,17 @@ export function registerContextHandler(pi, runtime, config) {
50
50
  // Legacy: MEGACOMPACT_LEGACY_DURABLE_TRIM=true restores the v0.4.28 ctx.compact
51
51
  // path (kept one release as rollback).
52
52
  pi.on("context", async (event, ctx) => {
53
- if (!config.auto)
54
- return;
55
53
  const usage = ctx.getContextUsage();
56
54
  const pct = usage?.percent;
57
- // Always track context for the dashboard, even if we return early below.
55
+ // Always track context for the dashboard/widget, even when auto is off.
56
+ // (v0.8 regression: !config.auto gate sat above this, leaving ctx stats
57
+ // null -> widget '?% / ?/?' when auto disabled. Track first, THEN gate.)
58
58
  runtime.lastCtxTokens = usage?.tokens ?? null;
59
59
  runtime.lastCtxPercent = pct ?? null;
60
60
  runtime.lastCtxWindow = usage?.contextWindow ?? 0;
61
61
  runtime.snapshot(ctx);
62
+ if (!config.auto)
63
+ return;
62
64
  const messages = event.messages;
63
65
  const view = runtime.engineView(messages);
64
66
  const currentTokens = usage?.tokens ??
@@ -1,5 +1,5 @@
1
1
  /**
2
- * mega-game-cmds.ts — /mega-game slash command (S30).
2
+ * mega-game-cmds.ts — /mega-compact-settings slash command (S30; renamed in v0.8.x).
3
3
  *
4
4
  * Backs the game-mode toggle + theme picker + TUI display mode. All state is
5
5
  * the global `game_state` SQLite row (src/store/sqlite/game-state.ts) — local
@@ -7,100 +7,113 @@
7
7
  * command touches no fetch/http). All SQL is parameterized (PREVENT-002) and
8
8
  * lives in the src/ submodule, not here.
9
9
  *
10
+ * The primary command is /mega-compact-settings. /mega-game is retained as a
11
+ * backward-compat alias (same handler) so existing muscle memory + docs keep
12
+ * working.
13
+ *
10
14
  * Usage:
11
- * /mega-game print current state
12
- * /mega-game on enable game mode (scoring + level-up + MEGA CACHE)
13
- * /mega-game off disable game mode
14
- * /mega-game theme list available themes
15
- * /mega-game theme <id> set theme by id
16
- * /mega-game theme next cycle to next theme
17
- * /mega-game tui full full TUI widget (bars, stats, flair)
18
- * /mega-game tui minimal one-line TUI widget (level + cache %)
19
- * /mega-game achievements list unlocked achievements
15
+ * /mega-compact-settings print current state
16
+ * /mega-compact-settings on enable game mode (scoring + level-up + MEGA CACHE)
17
+ * /mega-compact-settings off disable game mode
18
+ * /mega-compact-settings theme list available themes
19
+ * /mega-compact-settings theme <id> set theme by id
20
+ * /mega-compact-settings theme next cycle to next theme
21
+ * /mega-compact-settings tui full full TUI widget (bars, stats, flair)
22
+ * /mega-compact-settings tui minimal one-line TUI widget (level + cache %)
23
+ * /mega-compact-settings achievements list unlocked achievements
20
24
  */
21
25
  import { getGameState, setGameState, } from "../src/store/sqlite.js";
22
26
  import { listAchievements } from "../src/store/sqlite/game-achievements.js";
23
27
  import { THEMES, THEME_IDS, getTheme, isValidTheme, nextTheme, DEFAULT_THEME } from "../src/config/themes.js";
28
+ /** Notify/usage tag + command name. Primary surface is /mega-compact-settings. */
29
+ const TAG = "mega-compact-settings";
24
30
  /** Format the current state as a human-readable status line set. */
25
31
  function fmtState(s) {
26
32
  return [
27
- `[mega-game] game mode: ${s.game_mode_on ? "ON" : "off"}`,
33
+ `[${TAG}] game mode: ${s.game_mode_on ? "ON" : "off"}`,
28
34
  ` theme: ${s.theme}${s.theme === DEFAULT_THEME ? " (default)" : ""}`,
29
35
  ` tui: ${s.tui_display_mode}`,
30
36
  ];
31
37
  }
32
- /** Register the /mega-game command. */
38
+ /** Shared handler for /mega-compact-settings (primary) + /mega-game (alias). */
39
+ async function handleSettings(args, ctx, runtime) {
40
+ runtime.bindRepo(ctx.cwd);
41
+ const stateDir = runtime.currentStateDir;
42
+ const parts = args.trim().split(/\s+/).filter(Boolean);
43
+ // bare → print current state.
44
+ if (parts.length === 0) {
45
+ const s = getGameState(stateDir);
46
+ for (const line of fmtState(s))
47
+ ctx.ui.notify(line);
48
+ return;
49
+ }
50
+ const sub = parts[0];
51
+ // achievements — terse list of unlocked (hidden only once unlocked).
52
+ if (sub === "achievements") {
53
+ const rows = listAchievements(stateDir).filter((r) => r.unlocked_at != null);
54
+ ctx.ui.notify(`[${TAG}] achievements unlocked (${rows.length}/9):`);
55
+ for (const r of rows) {
56
+ ctx.ui.notify(` ${r.icon ?? ""} ${r.title}`);
57
+ }
58
+ return;
59
+ }
60
+ // on|off
61
+ if (sub === "on" || sub === "off") {
62
+ const s = setGameState({ game_mode_on: sub === "on" }, stateDir);
63
+ runtime.bumpGameState();
64
+ ctx.ui.notify(`[${TAG}] game mode ${s.game_mode_on ? "ON" : "off"}`);
65
+ return;
66
+ }
67
+ // theme [id|next]
68
+ if (sub === "theme") {
69
+ if (parts.length === 1) {
70
+ // list themes
71
+ ctx.ui.notify(`[${TAG}] themes:`);
72
+ for (const t of THEMES) {
73
+ ctx.ui.notify(` ${t.id.padEnd(14)} ${t.label}`);
74
+ }
75
+ return;
76
+ }
77
+ const arg = parts[1];
78
+ let id;
79
+ if (arg === "next") {
80
+ id = nextTheme(getGameState(stateDir).theme);
81
+ }
82
+ else if (isValidTheme(arg)) {
83
+ id = arg;
84
+ }
85
+ else {
86
+ ctx.ui.notify(`[${TAG}] unknown theme "${arg}". Valid: ${THEME_IDS.join(", ")}`);
87
+ return;
88
+ }
89
+ const s = setGameState({ theme: id }, stateDir);
90
+ runtime.bumpGameState();
91
+ ctx.ui.notify(`[${TAG}] theme → ${s.theme} (${getTheme(s.theme)?.label ?? ""})`);
92
+ return;
93
+ }
94
+ // tui full|minimal
95
+ if (sub === "tui") {
96
+ const arg = parts[1];
97
+ if (arg !== "full" && arg !== "minimal") {
98
+ ctx.ui.notify(`[${TAG}] usage: /${TAG} tui full|minimal`);
99
+ return;
100
+ }
101
+ const s = setGameState({ tui_display_mode: arg }, stateDir);
102
+ runtime.bumpGameState();
103
+ ctx.ui.notify(`[${TAG}] tui → ${s.tui_display_mode}`);
104
+ return;
105
+ }
106
+ ctx.ui.notify(`[${TAG}] usage: /${TAG} [on|off|theme [id|next]|tui [full|minimal]|achievements]`);
107
+ }
108
+ /** Register /mega-compact-settings (primary) + /mega-game (backward-compat alias). */
33
109
  export function registerGameCommands(pi, runtime) {
110
+ const description = "Game mode toggle + theme picker + TUI display mode. Usage: /mega-compact-settings [on|off|theme [id|next]|tui [full|minimal]|achievements]";
111
+ const handler = (args, ctx) => handleSettings(args, ctx, runtime);
112
+ // Primary command (renamed in v0.8.x from /mega-game).
113
+ pi.registerCommand("mega-compact-settings", { description, handler });
114
+ // Backward-compat alias: /mega-game still resolves to the same settings UI.
34
115
  pi.registerCommand("mega-game", {
35
- description: "Game mode toggle + theme picker + TUI display mode. Usage: /mega-game [on|off|theme [id|next]|tui [full|minimal]|achievements]",
36
- handler: async (args, ctx) => {
37
- runtime.bindRepo(ctx.cwd);
38
- const stateDir = runtime.currentStateDir;
39
- const parts = args.trim().split(/\s+/).filter(Boolean);
40
- // /mega-game → print current state.
41
- if (parts.length === 0) {
42
- const s = getGameState(stateDir);
43
- for (const line of fmtState(s))
44
- ctx.ui.notify(line);
45
- return;
46
- }
47
- const sub = parts[0];
48
- // /mega-game achievements — terse list of unlocked (hidden only once unlocked).
49
- if (sub === "achievements") {
50
- const rows = listAchievements(stateDir).filter((r) => r.unlocked_at != null);
51
- ctx.ui.notify(`[mega-game] achievements unlocked (${rows.length}/9):`);
52
- for (const r of rows) {
53
- ctx.ui.notify(` ${r.icon ?? ""} ${r.title}`);
54
- }
55
- return;
56
- }
57
- // /mega-game on|off
58
- if (sub === "on" || sub === "off") {
59
- const s = setGameState({ game_mode_on: sub === "on" }, stateDir);
60
- runtime.bumpGameState();
61
- ctx.ui.notify(`[mega-game] game mode ${s.game_mode_on ? "ON" : "off"}`);
62
- return;
63
- }
64
- // /mega-game theme [id|next]
65
- if (sub === "theme") {
66
- if (parts.length === 1) {
67
- // list themes
68
- ctx.ui.notify("[mega-game] themes:");
69
- for (const t of THEMES) {
70
- ctx.ui.notify(` ${t.id.padEnd(14)} ${t.label}`);
71
- }
72
- return;
73
- }
74
- const arg = parts[1];
75
- let id;
76
- if (arg === "next") {
77
- id = nextTheme(getGameState(stateDir).theme);
78
- }
79
- else if (isValidTheme(arg)) {
80
- id = arg;
81
- }
82
- else {
83
- ctx.ui.notify(`[mega-game] unknown theme "${arg}". Valid: ${THEME_IDS.join(", ")}`);
84
- return;
85
- }
86
- const s = setGameState({ theme: id }, stateDir);
87
- runtime.bumpGameState();
88
- ctx.ui.notify(`[mega-game] theme → ${s.theme} (${getTheme(s.theme)?.label ?? ""})`);
89
- return;
90
- }
91
- // /mega-game tui full|minimal
92
- if (sub === "tui") {
93
- const arg = parts[1];
94
- if (arg !== "full" && arg !== "minimal") {
95
- ctx.ui.notify(`[mega-game] usage: /mega-game tui full|minimal`);
96
- return;
97
- }
98
- const s = setGameState({ tui_display_mode: arg }, stateDir);
99
- runtime.bumpGameState();
100
- ctx.ui.notify(`[mega-game] tui → ${s.tui_display_mode}`);
101
- return;
102
- }
103
- ctx.ui.notify(`[mega-game] usage: /mega-game [on|off|theme [id|next]|tui [full|minimal]|achievements]`);
104
- },
116
+ description: "(alias for /mega-compact-settings) " + description,
117
+ handler,
105
118
  });
106
119
  }
@@ -35,7 +35,7 @@ function makeHarness(stateDir) {
35
35
  mod.registerGameCommands(fakePi, runtime);
36
36
  return { commands, notifies, ctx };
37
37
  }
38
- describe("/mega-game (S30)", () => {
38
+ describe("/mega-compact-settings (S30; /mega-game alias)", () => {
39
39
  let dir;
40
40
  before(() => {
41
41
  dir = mkdtempSync(join(tmpdir(), "mc-megagame-"));
@@ -52,6 +52,11 @@ describe("/mega-game (S30)", () => {
52
52
  await h.commands["mega-game"].handler(args, h.ctx);
53
53
  return h.notifies;
54
54
  }
55
+ it("registers /mega-compact-settings as primary + /mega-game as alias", async () => {
56
+ const h = makeHarness(dir);
57
+ assert.ok(h.commands["mega-compact-settings"], "primary registered");
58
+ assert.ok(h.commands["mega-game"], "alias registered");
59
+ });
55
60
  it("bare command prints current (default) state", async () => {
56
61
  const lines = await run("");
57
62
  assert.ok(lines.some((l) => l.includes("game mode: off")));
@@ -767,11 +767,25 @@ export class MegaRuntime {
767
767
  this.gameStateWatchDir = undefined;
768
768
  }
769
769
  try {
770
- this.gameStateWatcher = watch(join(this.currentStateDir, "sqlite.db"), () => { this.cachedGameState = undefined; });
770
+ // Watch the state DIR (not just sqlite.db) and filter by filename.
771
+ // Why: the store is WAL-mode (openStore sets PRAGMA journal_mode=WAL).
772
+ // Cross-process writes (dashboard server child) append to sqlite.db-wal
773
+ // and do NOT modify sqlite.db until a checkpoint — and a long-lived
774
+ // parent connection (VectorStore + dashboard readers) keeps the WAL
775
+ // uncheckpointed, so a watcher on sqlite.db alone never fires and
776
+ // cachedGameState stays stale (theme stuck after a dashboard edit).
777
+ // Watching the dir + matching sqlite.db* catches the main db, the -wal
778
+ // sidecar, and -shm, so the memo evicts on any cross-process write. The
779
+ // filter also excludes events.log / *.log noise in the same dir.
780
+ this.gameStateWatcher = watch(this.currentStateDir, (_eventType, filename) => {
781
+ if (typeof filename === "string" && filename.startsWith("sqlite.db")) {
782
+ this.cachedGameState = undefined;
783
+ }
784
+ });
771
785
  this.gameStateWatchDir = this.currentStateDir;
772
786
  }
773
787
  catch {
774
- /* non-fatal: missing file / platform issue — next snapshot re-queries */
788
+ /* non-fatal: missing dir / platform issue — next snapshot re-queries */
775
789
  }
776
790
  }
777
791
  /** S32: release the fs.watch game-state watcher. Called when the runtime is
@@ -22,10 +22,16 @@ export async function launchDashboardServer(stateDir: string): Promise<{ port: n
22
22
  // upgrade instead of reuse it.
23
23
  let SERVER_VERSION = "0.0.0";
24
24
  try {
25
- // dashboard-server.js lives at <pkg>/dist/extensions/, so package.json is
26
- // two levels up. Guard each candidate so a dev-checkout layout still works.
25
+ // Since v0.7.9 (8821ef3) dashboard-server.js lives at
26
+ // <pkg>/dist/extensions/dashboard-server/, so package.json is THREE levels
27
+ // up. Keep the two- and one-level-up candidates as fallbacks for flatter
28
+ // dev-checkout layouts. Guard each candidate so a missing file is skipped.
27
29
  const here = dirname(fileURLToPath(import.meta.url));
28
- const candidates = [join(here, "..", "..", "package.json"), join(here, "..", "package.json")];
30
+ const candidates = [
31
+ join(here, "..", "..", "..", "package.json"),
32
+ join(here, "..", "..", "package.json"),
33
+ join(here, "..", "package.json"),
34
+ ];
29
35
  for (const p of candidates) {
30
36
  if (!existsSync(p)) continue;
31
37
  const pkg = JSON.parse(readFileSync(p, "utf-8"));
@@ -18,7 +18,7 @@
18
18
  * - mega-runtime.ts shared live state (MegaRuntime) + widget + model capture
19
19
  * - mega-pipeline.ts runCompact (Trident+persist) + doRecall (Layer 5)
20
20
  * - mega-commands.ts data/inspection slash commands
21
- * - mega-game-cmds.ts /mega-game toggle + theme + TUI display mode
21
+ * - mega-game-cmds.ts /mega-compact-settings (+ /mega-game alias) toggle + theme + TUI display mode
22
22
  * - mega-dashboard-cmds.ts localhost dashboard server lifecycle commands
23
23
  * - mega-events.ts pi lifecycle event handlers
24
24
  *
@@ -83,14 +83,16 @@ export function registerContextHandler(
83
83
  // Legacy: MEGACOMPACT_LEGACY_DURABLE_TRIM=true restores the v0.4.28 ctx.compact
84
84
  // path (kept one release as rollback).
85
85
  pi.on("context", async (event: ContextEvent, ctx: ExtensionContext) => {
86
- if (!config.auto) return;
87
86
  const usage = ctx.getContextUsage();
88
87
  const pct = usage?.percent;
89
- // Always track context for the dashboard, even if we return early below.
88
+ // Always track context for the dashboard/widget, even when auto is off.
89
+ // (v0.8 regression: !config.auto gate sat above this, leaving ctx stats
90
+ // null -> widget '?% / ?/?' when auto disabled. Track first, THEN gate.)
90
91
  runtime.lastCtxTokens = usage?.tokens ?? null;
91
92
  runtime.lastCtxPercent = pct ?? null;
92
93
  runtime.lastCtxWindow = usage?.contextWindow ?? 0;
93
94
  runtime.snapshot(ctx);
95
+ if (!config.auto) return;
94
96
 
95
97
  const messages = event.messages;
96
98
  const view = runtime.engineView(messages);
@@ -48,7 +48,7 @@ function makeHarness(stateDir: string): Harness {
48
48
  return { commands, notifies, ctx };
49
49
  }
50
50
 
51
- describe("/mega-game (S30)", () => {
51
+ describe("/mega-compact-settings (S30; /mega-game alias)", () => {
52
52
  let dir: string;
53
53
  before(() => {
54
54
  dir = mkdtempSync(join(tmpdir(), "mc-megagame-"));
@@ -67,6 +67,12 @@ describe("/mega-game (S30)", () => {
67
67
  return h.notifies;
68
68
  }
69
69
 
70
+ it("registers /mega-compact-settings as primary + /mega-game as alias", async () => {
71
+ const h = makeHarness(dir);
72
+ assert.ok(h.commands["mega-compact-settings"], "primary registered");
73
+ assert.ok(h.commands["mega-game"], "alias registered");
74
+ });
75
+
70
76
  it("bare command prints current (default) state", async () => {
71
77
  const lines = await run("");
72
78
  assert.ok(lines.some((l) => l.includes("game mode: off")));
@@ -1,5 +1,5 @@
1
1
  /**
2
- * mega-game-cmds.ts — /mega-game slash command (S30).
2
+ * mega-game-cmds.ts — /mega-compact-settings slash command (S30; renamed in v0.8.x).
3
3
  *
4
4
  * Backs the game-mode toggle + theme picker + TUI display mode. All state is
5
5
  * the global `game_state` SQLite row (src/store/sqlite/game-state.ts) — local
@@ -7,16 +7,20 @@
7
7
  * command touches no fetch/http). All SQL is parameterized (PREVENT-002) and
8
8
  * lives in the src/ submodule, not here.
9
9
  *
10
+ * The primary command is /mega-compact-settings. /mega-game is retained as a
11
+ * backward-compat alias (same handler) so existing muscle memory + docs keep
12
+ * working.
13
+ *
10
14
  * Usage:
11
- * /mega-game print current state
12
- * /mega-game on enable game mode (scoring + level-up + MEGA CACHE)
13
- * /mega-game off disable game mode
14
- * /mega-game theme list available themes
15
- * /mega-game theme <id> set theme by id
16
- * /mega-game theme next cycle to next theme
17
- * /mega-game tui full full TUI widget (bars, stats, flair)
18
- * /mega-game tui minimal one-line TUI widget (level + cache %)
19
- * /mega-game achievements list unlocked achievements
15
+ * /mega-compact-settings print current state
16
+ * /mega-compact-settings on enable game mode (scoring + level-up + MEGA CACHE)
17
+ * /mega-compact-settings off disable game mode
18
+ * /mega-compact-settings theme list available themes
19
+ * /mega-compact-settings theme <id> set theme by id
20
+ * /mega-compact-settings theme next cycle to next theme
21
+ * /mega-compact-settings tui full full TUI widget (bars, stats, flair)
22
+ * /mega-compact-settings tui minimal one-line TUI widget (level + cache %)
23
+ * /mega-compact-settings achievements list unlocked achievements
20
24
  */
21
25
 
22
26
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
@@ -29,94 +33,110 @@ import {
29
33
  import { listAchievements } from "../src/store/sqlite/game-achievements.js";
30
34
  import { THEMES, THEME_IDS, getTheme, isValidTheme, nextTheme, DEFAULT_THEME } from "../src/config/themes.js";
31
35
 
36
+ /** Notify/usage tag + command name. Primary surface is /mega-compact-settings. */
37
+ const TAG = "mega-compact-settings";
38
+
32
39
  /** Format the current state as a human-readable status line set. */
33
40
  function fmtState(s: GameState): string[] {
34
41
  return [
35
- `[mega-game] game mode: ${s.game_mode_on ? "ON" : "off"}`,
42
+ `[${TAG}] game mode: ${s.game_mode_on ? "ON" : "off"}`,
36
43
  ` theme: ${s.theme}${s.theme === DEFAULT_THEME ? " (default)" : ""}`,
37
44
  ` tui: ${s.tui_display_mode}`,
38
45
  ];
39
46
  }
40
47
 
41
- /** Register the /mega-game command. */
42
- export function registerGameCommands(pi: ExtensionAPI, runtime: MegaRuntime): void {
43
- pi.registerCommand("mega-game", {
44
- description:
45
- "Game mode toggle + theme picker + TUI display mode. Usage: /mega-game [on|off|theme [id|next]|tui [full|minimal]|achievements]",
46
- handler: async (args: string, ctx: ExtensionContext) => {
47
- runtime.bindRepo(ctx.cwd);
48
- const stateDir = runtime.currentStateDir;
49
- const parts = args.trim().split(/\s+/).filter(Boolean);
48
+ /** Shared handler for /mega-compact-settings (primary) + /mega-game (alias). */
49
+ async function handleSettings(
50
+ args: string,
51
+ ctx: ExtensionContext,
52
+ runtime: MegaRuntime,
53
+ ): Promise<void> {
54
+ runtime.bindRepo(ctx.cwd);
55
+ const stateDir = runtime.currentStateDir;
56
+ const parts = args.trim().split(/\s+/).filter(Boolean);
50
57
 
51
- // /mega-game → print current state.
52
- if (parts.length === 0) {
53
- const s = getGameState(stateDir);
54
- for (const line of fmtState(s)) ctx.ui.notify(line);
55
- return;
56
- }
58
+ // bare → print current state.
59
+ if (parts.length === 0) {
60
+ const s = getGameState(stateDir);
61
+ for (const line of fmtState(s)) ctx.ui.notify(line);
62
+ return;
63
+ }
57
64
 
58
- const sub = parts[0]!;
65
+ const sub = parts[0]!;
59
66
 
60
- // /mega-game achievements — terse list of unlocked (hidden only once unlocked).
61
- if (sub === "achievements") {
62
- const rows = listAchievements(stateDir).filter((r) => r.unlocked_at != null);
63
- ctx.ui.notify(`[mega-game] achievements unlocked (${rows.length}/9):`);
64
- for (const r of rows) {
65
- ctx.ui.notify(` ${r.icon ?? ""} ${r.title}`);
66
- }
67
- return;
68
- }
67
+ // achievements — terse list of unlocked (hidden only once unlocked).
68
+ if (sub === "achievements") {
69
+ const rows = listAchievements(stateDir).filter((r) => r.unlocked_at != null);
70
+ ctx.ui.notify(`[${TAG}] achievements unlocked (${rows.length}/9):`);
71
+ for (const r of rows) {
72
+ ctx.ui.notify(` ${r.icon ?? ""} ${r.title}`);
73
+ }
74
+ return;
75
+ }
69
76
 
70
- // /mega-game on|off
71
- if (sub === "on" || sub === "off") {
72
- const s = setGameState({ game_mode_on: sub === "on" }, stateDir);
73
- runtime.bumpGameState();
74
- ctx.ui.notify(`[mega-game] game mode ${s.game_mode_on ? "ON" : "off"}`);
75
- return;
76
- }
77
+ // on|off
78
+ if (sub === "on" || sub === "off") {
79
+ const s = setGameState({ game_mode_on: sub === "on" }, stateDir);
80
+ runtime.bumpGameState();
81
+ ctx.ui.notify(`[${TAG}] game mode ${s.game_mode_on ? "ON" : "off"}`);
82
+ return;
83
+ }
77
84
 
78
- // /mega-game theme [id|next]
79
- if (sub === "theme") {
80
- if (parts.length === 1) {
81
- // list themes
82
- ctx.ui.notify("[mega-game] themes:");
83
- for (const t of THEMES) {
84
- ctx.ui.notify(` ${t.id.padEnd(14)} ${t.label}`);
85
- }
86
- return;
87
- }
88
- const arg = parts[1]!;
89
- let id: string;
90
- if (arg === "next") {
91
- id = nextTheme(getGameState(stateDir).theme);
92
- } else if (isValidTheme(arg)) {
93
- id = arg;
94
- } else {
95
- ctx.ui.notify(`[mega-game] unknown theme "${arg}". Valid: ${THEME_IDS.join(", ")}`);
96
- return;
97
- }
98
- const s = setGameState({ theme: id }, stateDir);
99
- runtime.bumpGameState();
100
- ctx.ui.notify(`[mega-game] theme → ${s.theme} (${getTheme(s.theme)?.label ?? ""})`);
101
- return;
85
+ // theme [id|next]
86
+ if (sub === "theme") {
87
+ if (parts.length === 1) {
88
+ // list themes
89
+ ctx.ui.notify(`[${TAG}] themes:`);
90
+ for (const t of THEMES) {
91
+ ctx.ui.notify(` ${t.id.padEnd(14)} ${t.label}`);
102
92
  }
93
+ return;
94
+ }
95
+ const arg = parts[1]!;
96
+ let id: string;
97
+ if (arg === "next") {
98
+ id = nextTheme(getGameState(stateDir).theme);
99
+ } else if (isValidTheme(arg)) {
100
+ id = arg;
101
+ } else {
102
+ ctx.ui.notify(`[${TAG}] unknown theme "${arg}". Valid: ${THEME_IDS.join(", ")}`);
103
+ return;
104
+ }
105
+ const s = setGameState({ theme: id }, stateDir);
106
+ runtime.bumpGameState();
107
+ ctx.ui.notify(`[${TAG}] theme → ${s.theme} (${getTheme(s.theme)?.label ?? ""})`);
108
+ return;
109
+ }
103
110
 
104
- // /mega-game tui full|minimal
105
- if (sub === "tui") {
106
- const arg = parts[1];
107
- if (arg !== "full" && arg !== "minimal") {
108
- ctx.ui.notify(`[mega-game] usage: /mega-game tui full|minimal`);
109
- return;
110
- }
111
- const s = setGameState({ tui_display_mode: arg }, stateDir);
112
- runtime.bumpGameState();
113
- ctx.ui.notify(`[mega-game] tui → ${s.tui_display_mode}`);
114
- return;
115
- }
111
+ // tui full|minimal
112
+ if (sub === "tui") {
113
+ const arg = parts[1];
114
+ if (arg !== "full" && arg !== "minimal") {
115
+ ctx.ui.notify(`[${TAG}] usage: /${TAG} tui full|minimal`);
116
+ return;
117
+ }
118
+ const s = setGameState({ tui_display_mode: arg }, stateDir);
119
+ runtime.bumpGameState();
120
+ ctx.ui.notify(`[${TAG}] tui → ${s.tui_display_mode}`);
121
+ return;
122
+ }
123
+
124
+ ctx.ui.notify(
125
+ `[${TAG}] usage: /${TAG} [on|off|theme [id|next]|tui [full|minimal]|achievements]`,
126
+ );
127
+ }
116
128
 
117
- ctx.ui.notify(
118
- `[mega-game] usage: /mega-game [on|off|theme [id|next]|tui [full|minimal]|achievements]`,
119
- );
120
- },
129
+ /** Register /mega-compact-settings (primary) + /mega-game (backward-compat alias). */
130
+ export function registerGameCommands(pi: ExtensionAPI, runtime: MegaRuntime): void {
131
+ const description =
132
+ "Game mode toggle + theme picker + TUI display mode. Usage: /mega-compact-settings [on|off|theme [id|next]|tui [full|minimal]|achievements]";
133
+ const handler = (args: string, ctx: ExtensionContext) => handleSettings(args, ctx, runtime);
134
+
135
+ // Primary command (renamed in v0.8.x from /mega-game).
136
+ pi.registerCommand("mega-compact-settings", { description, handler });
137
+ // Backward-compat alias: /mega-game still resolves to the same settings UI.
138
+ pi.registerCommand("mega-game", {
139
+ description: "(alias for /mega-compact-settings) " + description,
140
+ handler,
121
141
  });
122
142
  }
@@ -846,13 +846,27 @@ export class MegaRuntime {
846
846
  this.gameStateWatchDir = undefined;
847
847
  }
848
848
  try {
849
+ // Watch the state DIR (not just sqlite.db) and filter by filename.
850
+ // Why: the store is WAL-mode (openStore sets PRAGMA journal_mode=WAL).
851
+ // Cross-process writes (dashboard server child) append to sqlite.db-wal
852
+ // and do NOT modify sqlite.db until a checkpoint — and a long-lived
853
+ // parent connection (VectorStore + dashboard readers) keeps the WAL
854
+ // uncheckpointed, so a watcher on sqlite.db alone never fires and
855
+ // cachedGameState stays stale (theme stuck after a dashboard edit).
856
+ // Watching the dir + matching sqlite.db* catches the main db, the -wal
857
+ // sidecar, and -shm, so the memo evicts on any cross-process write. The
858
+ // filter also excludes events.log / *.log noise in the same dir.
849
859
  this.gameStateWatcher = watch(
850
- join(this.currentStateDir, "sqlite.db"),
851
- () => { this.cachedGameState = undefined; },
860
+ this.currentStateDir,
861
+ (_eventType, filename) => {
862
+ if (typeof filename === "string" && filename.startsWith("sqlite.db")) {
863
+ this.cachedGameState = undefined;
864
+ }
865
+ },
852
866
  );
853
867
  this.gameStateWatchDir = this.currentStateDir;
854
868
  } catch {
855
- /* non-fatal: missing file / platform issue — next snapshot re-queries */
869
+ /* non-fatal: missing dir / platform issue — next snapshot re-queries */
856
870
  }
857
871
  }
858
872
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Layered, local, vector-backed context compressor for pi — supersede/collapse/cluster compaction with deduped inline recall.",
5
5
  "type": "module",
6
6
  "license": "BSD-2-Clause",