Skip to content

Commit 6086c18

Browse files
edtadrosclaude
andcommitted
fix: v5.9.5 — self-healing machine_id + stale "gnosys dashboard" copy
Two hotfixes from the first hour of dogfooding v5.9.4. **Self-healing machine_id (Bug 9 v5.9.4 follow-on).** v5.9.4's `resolveHostname()` fix was correct — `os.hostname()` returns the right name. But `getMachineId()` caches the first computed value in `gnosys_meta.machine_id`, so users who first ran gnosys under pre-v5.9.4 code already had `unknown-<base36>` cached and the identifier kept appearing in the settings panel and dream wizard. `getMachineId()` now detects cached values matching `/^unknown-[a-z0-9]+$/` and re-derives via the v5.9.4 hostname path; if `dream_machine_id` was pointing at the same broken id, that row heals in lockstep. No-churn guard preserves the cache when the new derivation would also start with `unknown-`. 4 tests in src/test/v595-self-healing-machine-id.test.ts. **Three stale `gnosys dashboard` references.** The command was removed in v5.7.1 but several callsites still reference it: - src/lib/setup.ts dream wizard "check status anytime" hint — replaced with `gnosys status --system` and collapsed from a two-column right-aligned Status row to a single inline phrase (the prior layout produced a large empty gap between hint and command). - src/lib/ingest.ts smart-ingest provider-unavailable error — same replacement. - src/lib/dream.ts non-designated-machine comment — same. Tests: 1078/1078 pass (82 files, +1 file + 4 tests vs v5.9.4 baseline). tsc clean. build clean. CLI reports 5.9.5. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a780f3a commit 6086c18

7 files changed

Lines changed: 164 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,36 @@ All notable changes to Gnosys are documented here.
55
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [5.9.5] — 2026-05-21
9+
10+
Hotfix release. Two issues surfaced during the first hour of dogfooding
11+
v5.9.4.
12+
13+
### Fixed
14+
15+
- **Self-healing stale `machine_id` cache.** v5.9.4's Bug 9 fix correctly
16+
taught `resolveHostname()` to fall back to `os.hostname()`, but the
17+
derivation result is cached in `gnosys_meta.machine_id` on first call.
18+
Users who first ran gnosys under pre-v5.9.4 code already had
19+
`unknown-<base36>` cached — the identifier kept appearing in the
20+
settings panel, dream wizard, and "this machine" displays. v5.9.5's
21+
`getMachineId()` detects cached values matching `/^unknown-[a-z0-9]+$/`
22+
and re-derives via the v5.9.4 hostname path; if the cache was
23+
referenced by `dream_machine_id`, that row heals in lockstep. The
24+
no-churn branch (cache stale AND new derivation still "unknown")
25+
preserves the existing id so we don't churn meta on every read.
26+
Regression coverage: `src/test/v595-self-healing-machine-id.test.ts`
27+
(4 tests).
28+
29+
- **Three stale `gnosys dashboard` references.** The dream wizard's
30+
post-success hint pointed at a command that was removed in v5.7.1.
31+
Replaced with `gnosys status --system` (the canonical dashboard
32+
formatter today) in three places: `src/lib/setup.ts` dream wizard
33+
"check status anytime" hint (also dropped the right-aligned two-column
34+
layout that produced a large blank gap), `src/lib/ingest.ts`
35+
smart-ingestion provider-unavailable error message, and
36+
`src/lib/dream.ts` non-designated-machine comment.
37+
838
## [5.9.4] — 2026-05-20
939

1040
The "v5.9.3 dogfooding fixes" release. Twelve bugs Edward surfaced during

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "gnosys",
3-
"version": "5.9.4",
3+
"version": "5.9.5",
44
"description": "Gnosys — Persistent Memory for AI Agents. Sandbox-first runtime, central SQLite brain, federated search, Dream Mode, Web Knowledge Base, Obsidian export.",
55
"type": "module",
66
"main": "dist/index.js",

src/lib/dream.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -803,7 +803,7 @@ export class DreamScheduler {
803803
if (this.checkInterval) return;
804804
if (!this.isDesignatedMachine()) {
805805
// Quiet — non-designated machines simply don't dream. The user can
806-
// see designation status via `gnosys dashboard` if curious.
806+
// see designation status via `gnosys status --system` if curious.
807807
return;
808808
}
809809

src/lib/ingest.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export class GnosysIngestion {
8989
];
9090
if (isLocal) {
9191
lines.push(
92-
`Make sure ${providerName} is running locally (gnosys dashboard will probe it).`,
92+
`Make sure ${providerName} is running locally (gnosys status --system will probe it).`,
9393
`Or use gnosys_add_structured for direct memory writes (no LLM needed).`,
9494
);
9595
} else if (envVar) {

src/lib/remote.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -810,14 +810,30 @@ export class RemoteSync {
810810
* v5.9.4 Bug 9 — on macOS, neither `HOSTNAME` nor `COMPUTERNAME` are set
811811
* by default in subshells, which caused identifiers like `unknown-mp9cyh4j`.
812812
* `os.hostname()` is the reliable fallback before giving up to `unknown`.
813+
*
814+
* v5.9.5 — self-heal: when the cached value is a pre-fix `unknown-<rand>`
815+
* id and `resolveHostname()` now returns a real name, overwrite the cache
816+
* (and any `dream_machine_id` pointing at the broken id) so users who
817+
* upgrade past v5.9.4 stop seeing the stale identifier in panels.
813818
*/
814819
export function getMachineId(localDb: GnosysDB): string {
815-
let id = localDb.getMeta(META_MACHINE_ID);
816-
if (!id) {
817-
id = `${resolveHostname()}-${Date.now().toString(36)}`;
818-
localDb.setMeta(META_MACHINE_ID, id);
820+
const cached = localDb.getMeta(META_MACHINE_ID);
821+
if (cached && !isStaleUnknownId(cached)) return cached;
822+
const fresh = `${resolveHostname()}-${Date.now().toString(36)}`;
823+
// Don't churn the cache if we still can't resolve a real hostname —
824+
// keep the existing `unknown-<rand>` row so the id stays stable.
825+
if (cached && fresh.startsWith("unknown-")) return cached;
826+
localDb.setMeta(META_MACHINE_ID, fresh);
827+
// Heal a dream_machine_id that points at the broken cached id.
828+
if (cached && localDb.getDreamMachineId() === cached) {
829+
localDb.setDreamMachineId(fresh);
819830
}
820-
return id;
831+
return fresh;
832+
}
833+
834+
/** v5.9.5 — match the pre-fix `unknown-<base36>` shape. */
835+
function isStaleUnknownId(id: string): boolean {
836+
return /^unknown-[a-z0-9]+$/.test(id);
821837
}
822838

823839
/**

src/lib/setup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2775,7 +2775,7 @@ export async function runDreamSetup(opts: DreamSetupOpts = {}): Promise<void> {
27752775
);
27762776
const dreamerName = designate ? localMachine : (designatedMachine ?? "the designated machine");
27772777
printStatus("progress", `first cycle runs after ${dreamerName} is idle for ${idleMinutes} min`);
2778-
printStatus("progress", "check status anytime with", "gnosys dashboard");
2778+
printStatus("progress", "check status anytime with `gnosys status --system`");
27792779
} finally {
27802780
if (ownsRl) rl.close();
27812781
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* v5.9.5 — self-healing machine_id regression suite.
3+
*
4+
* v5.9.4 fixed the derivation (resolveHostname() falls back to os.hostname())
5+
* but didn't migrate the stale `unknown-<rand>` cache rows already persisted
6+
* in `gnosys_meta`. v5.9.5's `getMachineId()` now heals those cached values
7+
* (and any `dream_machine_id` pointing at the same broken id) the next time
8+
* it's called with a real hostname available.
9+
*
10+
* Pin process.env.HOSTNAME so the resolveHostname() path is deterministic
11+
* across dev and CI; pin to "" + use os.hostname() fallback for the
12+
* "still-unknown" branch.
13+
*/
14+
15+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
16+
import * as fs from "fs";
17+
import * as fsp from "fs/promises";
18+
import * as os from "os";
19+
import * as path from "path";
20+
import { GnosysDB } from "../lib/db.js";
21+
import { getMachineId } from "../lib/remote.js";
22+
23+
const STALE_ID = "unknown-mp9cyh4j";
24+
const ORIGINAL_HOSTNAME_ENV = process.env.HOSTNAME;
25+
const ORIGINAL_COMPUTERNAME_ENV = process.env.COMPUTERNAME;
26+
27+
interface Env {
28+
dir: string;
29+
db: GnosysDB;
30+
}
31+
32+
async function makeDb(): Promise<Env> {
33+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "gnosys-v595-"));
34+
const db = new GnosysDB(dir);
35+
return { dir, db };
36+
}
37+
38+
async function cleanup(env: Env): Promise<void> {
39+
env.db.close();
40+
await fsp.rm(env.dir, { recursive: true, force: true });
41+
}
42+
43+
describe("v5.9.5 — self-healing machine_id", () => {
44+
beforeEach(() => {
45+
process.env.HOSTNAME = "EdsMacStudio";
46+
delete process.env.COMPUTERNAME;
47+
});
48+
49+
afterEach(() => {
50+
if (ORIGINAL_HOSTNAME_ENV === undefined) delete process.env.HOSTNAME;
51+
else process.env.HOSTNAME = ORIGINAL_HOSTNAME_ENV;
52+
if (ORIGINAL_COMPUTERNAME_ENV === undefined) delete process.env.COMPUTERNAME;
53+
else process.env.COMPUTERNAME = ORIGINAL_COMPUTERNAME_ENV;
54+
});
55+
56+
it("heals a stale `unknown-<rand>` cache when a real hostname is available", async () => {
57+
const env = await makeDb();
58+
try {
59+
env.db.setMeta("machine_id", STALE_ID);
60+
const id = getMachineId(env.db);
61+
expect(id).not.toBe(STALE_ID);
62+
expect(id.startsWith("EdsMacStudio-")).toBe(true);
63+
expect(env.db.getMeta("machine_id")).toBe(id);
64+
} finally {
65+
await cleanup(env);
66+
}
67+
});
68+
69+
it("heals a stale `dream_machine_id` pointing at the same broken cached id", async () => {
70+
const env = await makeDb();
71+
try {
72+
env.db.setMeta("machine_id", STALE_ID);
73+
env.db.setDreamMachineId(STALE_ID);
74+
const id = getMachineId(env.db);
75+
expect(id.startsWith("EdsMacStudio-")).toBe(true);
76+
expect(env.db.getDreamMachineId()).toBe(id);
77+
} finally {
78+
await cleanup(env);
79+
}
80+
});
81+
82+
it("leaves a real cached id untouched (no churn)", async () => {
83+
const env = await makeDb();
84+
try {
85+
const realId = "EdsMacStudio-abc123";
86+
env.db.setMeta("machine_id", realId);
87+
const id = getMachineId(env.db);
88+
expect(id).toBe(realId);
89+
expect(env.db.getMeta("machine_id")).toBe(realId);
90+
} finally {
91+
await cleanup(env);
92+
}
93+
});
94+
95+
it("leaves an unrelated `dream_machine_id` alone when machine_id heals", async () => {
96+
const env = await makeDb();
97+
try {
98+
env.db.setMeta("machine_id", STALE_ID);
99+
env.db.setDreamMachineId("OtherMachine-xyz789");
100+
const id = getMachineId(env.db);
101+
expect(id.startsWith("EdsMacStudio-")).toBe(true);
102+
// dream_machine_id pointed at a DIFFERENT machine — don't touch it.
103+
expect(env.db.getDreamMachineId()).toBe("OtherMachine-xyz789");
104+
} finally {
105+
await cleanup(env);
106+
}
107+
});
108+
109+
});

0 commit comments

Comments
 (0)