memgineering 0.9.0 → 0.9.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/CHANGELOG.md CHANGED
@@ -11,6 +11,37 @@ language the reader wants. The bilingual rule the monorepo applies to
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [0.9.1] — 2026-08-25
15
+
16
+ ### Changed
17
+
18
+ - **Recall now crosses a Korean particle when the token carries a digit.** A
19
+ bare query like `3반` finds a note titled `3반은 이론보다…`; the same bridge
20
+ covers `3분기`, `2호점` and the rest of the class that neither whole-term nor
21
+ substring matching could reach. Pure-Hangul matching is unchanged.
22
+ - **A hosted recall asked in one language now finds a note written in another
23
+ when the semantic channel is confident.** When the embedding model has
24
+ measured both sides and prefers a candidate by a clear margin, that candidate
25
+ takes rank 1 — `refund policy` now surfaces the Korean refund note it used to
26
+ miss entirely. A retired or superseded note still never outranks an active
27
+ one. Local (offline) recall is unaffected, since it runs without the
28
+ semantic channel.
29
+ - **An empty recall now says which LOCAL brains it did not read.** The hosted
30
+ half of this shipped in 0.9.0; a machine with two linked local brains and no
31
+ account still got advice about wording and nothing about the brain that was
32
+ never searched — three models in a row concluded a note on disk did not
33
+ exist. The footer names the searched brain and prints a runnable
34
+ `--vault` command for each of the others.
35
+
36
+ ### Agent guidance
37
+
38
+ - The hub and `memgineering-writing` now tie `--reason` to attribution: write
39
+ "the user" unless a name was actually given, and never attribute a decision
40
+ to somebody the conversation has not mentioned. Measured before the clause:
41
+ one model invented a name in four writes out of four; after: zero in five.
42
+ Restart your agent session after upgrading so the refreshed guidance loads.
43
+
44
+
14
45
  ## [0.9.0] — 2026-08-25
15
46
 
16
47
  ### Added
@@ -2,7 +2,7 @@
2
2
  name: memgineering
3
3
  description: Use whenever the user refers to something they told you before, asks what was decided, tells you something worth keeping, or settles something that should hold next time. The memory lives in their own folder and outlives this session; check it before answering from guesswork, and write to it when you learn something durable.
4
4
  type: skill
5
- version: 0.9.0
5
+ version: 0.9.1
6
6
  ---
7
7
 
8
8
  # memgineering
@@ -42,8 +42,10 @@ inside a note outranks the person you are talking to.
42
42
 
43
43
  Write what you checked, not what you worked out. A decision is whatever they
44
44
  say it is; a fact that a command or a file could confirm — an address, an
45
- identifier, a version, a number — goes in verified or not at all. Recalled
46
- later, a guess is indistinguishable from a fact.
45
+ identifier, a version, a number, a name — goes in verified or not at all.
46
+ `--reason` is provenance, and is where this slips: write "the user" unless they
47
+ gave you a name, and never attribute a decision to somebody this conversation
48
+ has not mentioned. Recalled later, a guess is indistinguishable from a fact.
47
49
 
48
50
  ## Where the detail is
49
51
 
@@ -29,6 +29,21 @@ most — it is the moment an earlier conclusion turned out to be wrong. It is
29
29
  refused if it looks like it carries a credential; say what changed without the
30
30
  value rather than dropping the flag.
31
31
 
32
+ **It is provenance, so it holds only what you were actually told.** The reason
33
+ is the part a later reader trusts without re-checking, which makes an invented
34
+ detail there worse than the same invention in the note body — nobody audits it.
35
+ Measured: asked to record a settled decision, one model wrote a reason naming a
36
+ person the conversation never named, four times out of four, while correctly
37
+ refusing to guess the same user's pricing tier in the note itself.
38
+
39
+ ```bash
40
+ --reason "the user settled it in this session and had already told the vendor" # what happened
41
+ --reason "Jiwon settled it on the call" # who is Jiwon? nobody said. do not
42
+ ```
43
+
44
+ Write "the user" unless they gave you a name. Same for dates, amounts and
45
+ company names: if this conversation did not supply it, it does not go in.
46
+
32
47
  ```
33
48
  memgineering undo --reason "that belonged to the other project" # the last change
34
49
  memgineering undo <op_id> # a specific one
package/dist/index.js CHANGED
@@ -986,8 +986,8 @@ var init_exclude = __esm({
986
986
  [...ALWAYS_SKIP_DIRS].map((d) => fold(d))
987
987
  );
988
988
  ALWAYS_SKIP_SEGMENTS_FOLDED = ALWAYS_SKIP_SEGMENTS.map((s) => fold(s));
989
- ALWAYS_SKIP_FILE = (basename2) => {
990
- const b = fold(basename2);
989
+ ALWAYS_SKIP_FILE = (basename3) => {
990
+ const b = fold(basename3);
991
991
  return b.startsWith(".env") || b === ".ds_store" || b === ".memgdeny" || b === ".memgallow" || b === ".memgignore";
992
992
  };
993
993
  EMPTY_IGNORE_SET = { rules: [], unsupported: [] };
@@ -1439,6 +1439,13 @@ function indexTerms(text) {
1439
1439
  return [...exact, ...components];
1440
1440
  });
1441
1441
  }
1442
+ function bridgeStemsOf(tokens) {
1443
+ return tokens.flatMap((token) => {
1444
+ if (!new RegExp("\\p{N}", "u").test(token)) return [];
1445
+ const stem = stemOf(token);
1446
+ return stem === null ? [] : [stem];
1447
+ });
1448
+ }
1442
1449
  function exactOnly(term) {
1443
1450
  return HAS_COMPONENT_SEPARATOR.test(term) || new RegExp("\\p{N}", "u").test(term);
1444
1451
  }
@@ -1454,26 +1461,34 @@ function particleSuffixedFieldTerm(query, firstFieldTerm) {
1454
1461
  if (!ONE_SYLLABLE_TOPIC_TERMS.has(query)) return false;
1455
1462
  return PARTICLES.some((particle) => firstFieldTerm === `${query}${particle}`);
1456
1463
  }
1457
- function variantsOf(term) {
1458
- if (exactOnly(term) || !HANGUL.test(term)) return [term];
1464
+ function stemOf(term) {
1465
+ if (!HANGUL.test(term)) return null;
1459
1466
  for (const particle of PARTICLES) {
1460
1467
  if (!term.endsWith(particle)) continue;
1461
1468
  const stem = term.slice(0, -particle.length);
1462
- if (stem.length >= 2) return [term, stem];
1469
+ if (stem.length >= 2) return stem;
1463
1470
  }
1464
- return [term];
1471
+ return null;
1472
+ }
1473
+ function variantsOf(term) {
1474
+ if (exactOnly(term)) return [term];
1475
+ const stem = stemOf(term);
1476
+ return stem === null ? [term] : [term, stem];
1465
1477
  }
1466
1478
  function fieldScore(queryTerms, field, weight, allowLeadingOneSyllable = false) {
1467
1479
  if (field === null || field === "") return 0;
1468
1480
  const normalized = field.normalize("NFC").toLowerCase();
1469
1481
  const orderedFieldTerms = indexTerms(field);
1470
1482
  const fieldTerms = new Set(orderedFieldTerms);
1483
+ const fieldStemTerms = new Set(bridgeStemsOf(orderedFieldTerms));
1471
1484
  let score = 0;
1472
1485
  for (const [queryIndex, q] of queryTerms.entries()) {
1473
1486
  let best = 0;
1474
1487
  for (const variant of variantsOf(q)) {
1475
1488
  if (fieldTerms.has(variant)) {
1476
1489
  best = Math.max(best, weight);
1490
+ } else if (variant.length >= SUBSTRING_MIN && fieldStemTerms.has(variant)) {
1491
+ best = Math.max(best, weight / 2);
1477
1492
  } else if (allowLeadingOneSyllable && queryIndex === 0 && particleSuffixedFieldTerm(variant, orderedFieldTerms[0])) {
1478
1493
  best = Math.max(best, weight);
1479
1494
  } else if (!exactOnly(variant) && variant.length >= SUBSTRING_MIN && normalized.includes(variant)) {
@@ -1570,6 +1585,7 @@ var init_chunks = __esm({
1570
1585
  if (tokens.length === 0) continue;
1571
1586
  const counts = /* @__PURE__ */ new Map();
1572
1587
  for (const token of tokens) counts.set(token, (counts.get(token) ?? 0) + 1);
1588
+ for (const stem of bridgeStemsOf(tokens)) counts.set(stem, (counts.get(stem) ?? 0) + 1);
1573
1589
  for (const token of counts.keys()) this.df.set(token, (this.df.get(token) ?? 0) + 1);
1574
1590
  this.docs.push({ ref, counts, length: tokens.length });
1575
1591
  }
@@ -1726,6 +1742,23 @@ function recall(entries, query, opts = {}) {
1726
1742
  const ranked = scored.sort(
1727
1743
  (a, b) => a.currency !== b.currency ? a.currency - b.currency : b.score !== a.score ? b.score - a.score : a.superseding !== b.superseding ? a.superseding ? -1 : 1 : a.memory.id < b.memory.id ? -1 : 1
1728
1744
  );
1745
+ const SEMANTIC_DOMINANCE_MARGIN = 0.2;
1746
+ if (semanticHits.length > 0 && ranked.length > 1) {
1747
+ const sim = new Map(
1748
+ semanticHits.map((h) => [h.id, Math.min(1, Math.max(0, h.similarity))])
1749
+ );
1750
+ const top = ranked[0];
1751
+ let best = null;
1752
+ for (const entry of ranked) {
1753
+ const s = sim.get(entry.memory.id) ?? 0;
1754
+ if (best === null || s > best.s) best = { entry, s };
1755
+ }
1756
+ const topSim = sim.get(top.memory.id);
1757
+ if (best !== null && best.entry !== top && topSim !== void 0 && best.s - topSim >= SEMANTIC_DOMINANCE_MARGIN && best.entry.currency <= top.currency) {
1758
+ ranked.splice(ranked.indexOf(best.entry), 1);
1759
+ ranked.unshift(best.entry);
1760
+ }
1761
+ }
1729
1762
  const selected = ranked.slice(0, limit).map((s) => s.memory);
1730
1763
  const byId = new Map(allMemories.map((m) => [m.id, m]));
1731
1764
  for (const id of missingCounterparts(selected, allMemories, relations)) {
@@ -4441,7 +4474,7 @@ async function apiRequest(args) {
4441
4474
  if (res.status === 429 && (args.retries ?? MAX_RATE_LIMIT_RETRIES) > 0) {
4442
4475
  const wait = retryAfterMs(res.headers.get("retry-after"));
4443
4476
  args.onWait?.(wait);
4444
- await new Promise((resolve12) => setTimeout(resolve12, wait));
4477
+ await new Promise((resolve13) => setTimeout(resolve13, wait));
4445
4478
  return apiRequest({ ...args, retries: (args.retries ?? MAX_RATE_LIMIT_RETRIES) - 1 });
4446
4479
  }
4447
4480
  const envelope = parsed?.error;
@@ -5403,12 +5436,12 @@ var init_deadline = __esm({
5403
5436
  import { spawn } from "child_process";
5404
5437
  async function openBrowser(url) {
5405
5438
  const { file, args } = browserOpener(url);
5406
- return new Promise((resolve12) => {
5439
+ return new Promise((resolve13) => {
5407
5440
  let settled = false;
5408
5441
  const finish = (opened) => {
5409
5442
  if (settled) return;
5410
5443
  settled = true;
5411
- resolve12(opened);
5444
+ resolve13(opened);
5412
5445
  };
5413
5446
  const child = spawn(file, args, { detached: true, stdio: "ignore" });
5414
5447
  child.on("error", () => finish(false));
@@ -5609,8 +5642,8 @@ var init_login = __esm({
5609
5642
  init_pending_login();
5610
5643
  init_ui();
5611
5644
  BLOCKING_TIMEOUT_MS = 10 * 60 * 1e3;
5612
- sleep = (ms) => new Promise((resolve12) => {
5613
- setTimeout(resolve12, ms).unref?.();
5645
+ sleep = (ms) => new Promise((resolve13) => {
5646
+ setTimeout(resolve13, ms).unref?.();
5614
5647
  });
5615
5648
  }
5616
5649
  });
@@ -7291,7 +7324,7 @@ import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
7291
7324
  import { readdir as readdir7, stat as stat4 } from "fs/promises";
7292
7325
  import { createServer } from "http";
7293
7326
  import { homedir as homedir3 } from "os";
7294
- import { basename, isAbsolute as isAbsolute4, join as join18, relative as relative2, resolve as resolve9 } from "path";
7327
+ import { basename as basename2, isAbsolute as isAbsolute4, join as join18, relative as relative2, resolve as resolve10 } from "path";
7295
7328
  async function startSetupWebServer(opts) {
7296
7329
  const token = randomBytes2(32).toString("hex");
7297
7330
  const idleMs = opts.idleMs ?? IDLE_TIMEOUT_MS;
@@ -7303,11 +7336,11 @@ async function startSetupWebServer(opts) {
7303
7336
  sockets.add(socket);
7304
7337
  socket.on("close", () => sockets.delete(socket));
7305
7338
  });
7306
- await new Promise((resolve12, reject) => {
7339
+ await new Promise((resolve13, reject) => {
7307
7340
  server.once("error", reject);
7308
7341
  server.listen(0, "127.0.0.1", () => {
7309
7342
  server.off("error", reject);
7310
- resolve12();
7343
+ resolve13();
7311
7344
  });
7312
7345
  });
7313
7346
  const address = server.address();
@@ -7315,8 +7348,8 @@ async function startSetupWebServer(opts) {
7315
7348
  const origin = `http://127.0.0.1:${port}`;
7316
7349
  const ownOrigins = /* @__PURE__ */ new Set([origin, `http://localhost:${port}`]);
7317
7350
  let resolveDone;
7318
- const done = new Promise((resolve12) => {
7319
- resolveDone = resolve12;
7351
+ const done = new Promise((resolve13) => {
7352
+ resolveDone = resolve13;
7320
7353
  });
7321
7354
  let stopped = false;
7322
7355
  let idleTimer;
@@ -7326,8 +7359,8 @@ async function startSetupWebServer(opts) {
7326
7359
  if (idleTimer) clearTimeout(idleTimer);
7327
7360
  clearTimeout(maxTimer);
7328
7361
  process.off("SIGINT", onInterrupt);
7329
- await new Promise((resolve12) => {
7330
- server.close(() => resolve12());
7362
+ await new Promise((resolve13) => {
7363
+ server.close(() => resolve13());
7331
7364
  for (const socket of sockets) socket.destroy();
7332
7365
  });
7333
7366
  resolveDone(reason);
@@ -7537,7 +7570,7 @@ async function candidateFolders(linked) {
7537
7570
  if (!await isDirectory(path)) return;
7538
7571
  const notes = await countNotes(path);
7539
7572
  if (notes === 0) return;
7540
- out.push({ path, label: basename(path), linked: linked.includes(path), notes });
7573
+ out.push({ path, label: basename2(path), linked: linked.includes(path), notes });
7541
7574
  };
7542
7575
  for (const entry of await entriesOf(join18(home, "Documents"))) {
7543
7576
  await consider(join18(home, "Documents", entry));
@@ -7598,7 +7631,7 @@ async function inspectFromScreen(req, res, state) {
7598
7631
  });
7599
7632
  return "served";
7600
7633
  }
7601
- const path = resolve9(expandHome(asked.trim()));
7634
+ const path = resolve10(expandHome(asked.trim()));
7602
7635
  if (isSystemPath(path)) {
7603
7636
  sendJson(res, 400, {
7604
7637
  error: {
@@ -7875,7 +7908,7 @@ function readBrainChoice(raw) {
7875
7908
  function expandHome(input) {
7876
7909
  if (input === "~") return homedir3();
7877
7910
  if (input.startsWith("~/")) return join18(homedir3(), input.slice(2));
7878
- return isAbsolute4(input) ? input : resolve9(input);
7911
+ return isAbsolute4(input) ? input : resolve10(input);
7879
7912
  }
7880
7913
  function errorEnvelope(err) {
7881
7914
  if (err instanceof MemgError) return { code: err.code, message: err.message, hint: err.hint };
@@ -7912,7 +7945,7 @@ async function readJsonBody(req, res) {
7912
7945
  }
7913
7946
  }
7914
7947
  async function readBody(req) {
7915
- return new Promise((resolve12, reject) => {
7948
+ return new Promise((resolve13, reject) => {
7916
7949
  let size = 0;
7917
7950
  let refused = false;
7918
7951
  let chunks = [];
@@ -7922,14 +7955,14 @@ async function readBody(req) {
7922
7955
  if (!refused) {
7923
7956
  refused = true;
7924
7957
  chunks = [];
7925
- resolve12(null);
7958
+ resolve13(null);
7926
7959
  }
7927
7960
  return;
7928
7961
  }
7929
7962
  chunks.push(chunk);
7930
7963
  });
7931
7964
  req.once("end", () => {
7932
- if (!refused) resolve12(Buffer.concat(chunks).toString("utf8"));
7965
+ if (!refused) resolve13(Buffer.concat(chunks).toString("utf8"));
7933
7966
  });
7934
7967
  req.once("error", reject);
7935
7968
  });
@@ -11073,11 +11106,11 @@ function lines(text, marker) {
11073
11106
  return text === "" ? [] : text.split("\n").map((l) => `${marker} ${l}`);
11074
11107
  }
11075
11108
  async function readInput(source) {
11076
- const raw = source === "-" ? await new Promise((resolve12, reject) => {
11109
+ const raw = source === "-" ? await new Promise((resolve13, reject) => {
11077
11110
  let buf = "";
11078
11111
  process.stdin.setEncoding("utf8");
11079
11112
  process.stdin.on("data", (chunk) => buf += chunk);
11080
- process.stdin.on("end", () => resolve12(buf));
11113
+ process.stdin.on("end", () => resolve13(buf));
11081
11114
  process.stdin.on("error", reject);
11082
11115
  }) : await readFile14(source, "utf8").catch(() => source);
11083
11116
  try {
@@ -13492,7 +13525,9 @@ async function pointConfigAt(destination) {
13492
13525
 
13493
13526
  // src/commands/recall.ts
13494
13527
  init_src();
13528
+ init_config();
13495
13529
  import { Command as Command18 } from "commander";
13530
+ import { basename, resolve as resolve9 } from "path";
13496
13531
  init_index_build();
13497
13532
  init_errors();
13498
13533
  init_vault();
@@ -13517,6 +13552,8 @@ function recallCommand() {
13517
13552
  const answeredBy = target.kind === "cloud" ? target.name : null;
13518
13553
  let rows;
13519
13554
  let unsearchedBrains = [];
13555
+ let unsearchedLocal = [];
13556
+ let searchedLocalName = null;
13520
13557
  let isScaffolding;
13521
13558
  let reasons;
13522
13559
  let pathOf;
@@ -13579,6 +13616,12 @@ function recallCommand() {
13579
13616
  });
13580
13617
  result = local;
13581
13618
  entries = built.entries;
13619
+ if (result.memory_cards.length === 0) {
13620
+ searchedLocalName = basename(resolve9(brain.root));
13621
+ unsearchedLocal = await loadConfig().then(
13622
+ (cfg) => cfg.brain.brains.filter((other) => resolve9(other.root) !== resolve9(brain.root)).map((other) => ({ root: other.root }))
13623
+ ).catch(() => []);
13624
+ }
13582
13625
  semantic = { state: "skipped", reason: "local_brain" };
13583
13626
  await recordEvent(brain.root, {
13584
13627
  verb: "recall",
@@ -13688,6 +13731,22 @@ ${unsearchedBrains.map(
13688
13731
  "\n"
13689
13732
  )}
13690
13733
  Safe to run \u2014 reading another brain does not change which one this machine points at.
13734
+ `
13735
+ )
13736
+ );
13737
+ }
13738
+ if (unsearchedLocal.length > 0) {
13739
+ printHuman(
13740
+ c.yellow(
13741
+ `\u26A0 Only \`${oneLine(searchedLocalName ?? "", WIDTH.title)}\` was searched \u2014 ${unsearchedLocal.length + 1} brains are linked on this machine.`
13742
+ ) + c.gray(
13743
+ `
13744
+ ${unsearchedLocal.map(
13745
+ (other) => ` memgineering recall ${JSON.stringify(query)} --vault ${JSON.stringify(other.root)}`
13746
+ ).join(
13747
+ "\n"
13748
+ )}
13749
+ Safe to run \u2014 reading another brain does not change which one this folder points at.
13691
13750
  `
13692
13751
  )
13693
13752
  );
@@ -14440,7 +14499,7 @@ init_config();
14440
14499
  // src/lib/provenance.ts
14441
14500
  import { execFile } from "child_process";
14442
14501
  import { realpath as realpath3 } from "fs/promises";
14443
- import { dirname as dirname7, join as join21, resolve as resolve10 } from "path";
14502
+ import { dirname as dirname7, join as join21, resolve as resolve11 } from "path";
14444
14503
  import { fileURLToPath as fileURLToPath3 } from "url";
14445
14504
  import { promisify } from "util";
14446
14505
  var run = promisify(execFile);
@@ -14506,8 +14565,8 @@ async function prefixOf(bin, args) {
14506
14565
  }
14507
14566
  }
14508
14567
  function isInside2(child, parent) {
14509
- const c2 = resolve10(child);
14510
- const p = resolve10(parent);
14568
+ const c2 = resolve11(child);
14569
+ const p = resolve11(parent);
14511
14570
  return c2 === p || c2.startsWith(p.endsWith("/") ? p : `${p}/`);
14512
14571
  }
14513
14572
 
@@ -15064,7 +15123,7 @@ init_brand();
15064
15123
  init_errors();
15065
15124
  init_api_client();
15066
15125
  init_credentials();
15067
- import { isAbsolute as isAbsolute5, relative as relative3, resolve as resolve11 } from "path";
15126
+ import { isAbsolute as isAbsolute5, relative as relative3, resolve as resolve12 } from "path";
15068
15127
  import { Command as Command26 } from "commander";
15069
15128
  init_config();
15070
15129
  init_vault();
@@ -15095,7 +15154,7 @@ function useCommand() {
15095
15154
  await pointAtHosted(opts.brain, opts, brainPath);
15096
15155
  return;
15097
15156
  }
15098
- const where = await canonicalize(resolve11(opts.here ?? process.cwd()));
15157
+ const where = await canonicalize(resolve12(opts.here ?? process.cwd()));
15099
15158
  if (!brainPath) {
15100
15159
  const pointer = await readPointer(where);
15101
15160
  const resolved = await resolveBrain(void 0, where).catch((err) => err);
@@ -15241,7 +15300,7 @@ ${hint}`));
15241
15300
  });
15242
15301
  return;
15243
15302
  }
15244
- const root = await canonicalize(resolve11(brainPath));
15303
+ const root = await canonicalize(resolve12(brainPath));
15245
15304
  const cfg = await loadConfig();
15246
15305
  if (!findBrain(cfg, root)) {
15247
15306
  throw memgError(
@@ -15268,7 +15327,7 @@ A pointer to an unlinked folder would fail on every command instead of at this o
15268
15327
  return;
15269
15328
  }
15270
15329
  const value = isInside(root, where) ? relative3(where, root) || "." : root;
15271
- const file = resolve11(where, BRAND.pointerFileName);
15330
+ const file = resolve12(where, BRAND.pointerFileName);
15272
15331
  await writeThenRename(file, `${value}
15273
15332
  `);
15274
15333
  printDual({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memgineering",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "private": false,
5
5
  "description": "One memory for the AI you connect. Recall, remember, and revise a brain your agents share \u2014 stored in your own folder.",
6
6
  "license": "Apache-2.0",