@dsh-agentic-roleplay/state 0.1.0 → 0.2.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/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { DatabaseSync } from "node:sqlite";
3
- import { join, resolve } from "node:path";
3
+ import { dirname, join, resolve } from "node:path";
4
4
  import { Service } from "@deepseek-ai/cordis";
5
5
  import z from "@deepseek-ai/schemastery";
6
6
  import { randomUUID } from "node:crypto";
@@ -1922,9 +1922,15 @@ function describeValue(def, value) {
1922
1922
  const band = def.bands.find((candidate) => value <= candidate.upTo) ?? def.bands.at(-1);
1923
1923
  return band === void 0 ? shortNumber(value) : `${band.label} (${shortNumber(value)})`;
1924
1924
  }
1925
+ /** A variable written by its name or in other letter case ("Valence" for `valence`), when exactly one variable has that name or key. */
1926
+ function namedVariable(variables, written) {
1927
+ const wanted = written.trim().normalize("NFKC").toLowerCase();
1928
+ const found = wanted === "" ? [] : variables.defs().filter((def) => def.key.normalize("NFKC").toLowerCase() === wanted || def.name.normalize("NFKC").toLowerCase() === wanted);
1929
+ return found.length === 1 ? found[0] : void 0;
1930
+ }
1925
1931
  /** Whether a change can be made, and what it would come to. Nothing is changed. */
1926
1932
  function planVariableChange(context, spec) {
1927
- const def = context.variables.def(spec.variable);
1933
+ const def = context.variables.def(spec.variable) ?? namedVariable(context.variables, spec.variable);
1928
1934
  if (def === void 0) return {
1929
1935
  ok: false,
1930
1936
  problems: [`there is no variable "${spec.variable}" (list_variables shows them)`]
@@ -2033,6 +2039,7 @@ const MAX_DEPTH = 4;
2033
2039
  const MAX_NODES = 24;
2034
2040
  const MAX_EFFECTS = 6;
2035
2041
  const MAX_TEXT$1 = 1e3;
2042
+ const MAX_JUDGED = 300;
2036
2043
  const text = (entry, key) => typeof entry[key] === "string" && entry[key].trim() !== "" ? entry[key].trim() : void 0;
2037
2044
  function parseCondition(value, path, problems, count, depth = 0) {
2038
2045
  count.nodes += 1;
@@ -2132,6 +2139,17 @@ function parseCondition(value, path, problems, count, depth = 0) {
2132
2139
  trigger
2133
2140
  };
2134
2141
  }
2142
+ case "judged": {
2143
+ const statement = need("text");
2144
+ if (statement !== void 0 && statement.length > MAX_JUDGED) {
2145
+ problems.push(`${path}.text: at most ${MAX_JUDGED} characters`);
2146
+ return;
2147
+ }
2148
+ return statement === void 0 ? void 0 : {
2149
+ type: "judged",
2150
+ text: statement
2151
+ };
2152
+ }
2135
2153
  case "all":
2136
2154
  case "any": {
2137
2155
  if (!Array.isArray(value["conditions"]) || value["conditions"].length === 0) {
@@ -2152,7 +2170,7 @@ function parseCondition(value, path, problems, count, depth = 0) {
2152
2170
  };
2153
2171
  }
2154
2172
  default:
2155
- problems.push(`${path}.type: one of time, place, variable, item, event, fired, all, any, not`);
2173
+ problems.push(`${path}.type: one of time, place, variable, item, event, fired, judged, all, any, not`);
2156
2174
  return;
2157
2175
  }
2158
2176
  }
@@ -2302,6 +2320,7 @@ function conditionProblems(target, condition, path, known) {
2302
2320
  ];
2303
2321
  case "event": return [];
2304
2322
  case "fired": return known.has(condition.trigger) ? [] : [`${path}: there is no trigger "${condition.trigger}"`];
2323
+ case "judged": return [];
2305
2324
  case "all":
2306
2325
  case "any": return condition.conditions.flatMap((part, index) => conditionProblems(target, part, `${path}.conditions[${index}]`, known));
2307
2326
  case "not": return conditionProblems(target, condition.condition, `${path}.condition`, known);
@@ -2338,6 +2357,16 @@ function compare(op, have, want) {
2338
2357
  if (typeof have === "number" && typeof want === "number") return COMPARE[op](have, want);
2339
2358
  return op === "ne" ? have !== want : op === "eq" ? have === want : false;
2340
2359
  }
2360
+ /** The statements of the `judged` parts of a condition, in order, each once. */
2361
+ function judgedStatements(condition) {
2362
+ switch (condition.type) {
2363
+ case "judged": return [condition.text];
2364
+ case "all":
2365
+ case "any": return [...new Set(condition.conditions.flatMap(judgedStatements))];
2366
+ case "not": return judgedStatements(condition.condition);
2367
+ default: return [];
2368
+ }
2369
+ }
2341
2370
  function holds(target, condition, clock) {
2342
2371
  switch (condition.type) {
2343
2372
  case "time": {
@@ -2375,6 +2404,7 @@ function holds(target, condition, clock) {
2375
2404
  }
2376
2405
  case "event": return target.events.list().some((event) => event.tags.includes(condition.tag));
2377
2406
  case "fired": return (target.triggers.getByKey(condition.trigger)?.fireCount ?? 0) > 0;
2407
+ case "judged": return clock.judged?.(condition.text) ?? false;
2378
2408
  case "all": return condition.conditions.every((part) => holds(target, part, clock));
2379
2409
  case "any": return condition.conditions.some((part) => holds(target, part, clock));
2380
2410
  case "not": return !holds(target, condition.condition, clock);
@@ -5047,11 +5077,12 @@ const isObject = (value) => typeof value === "object" && value !== null && !Arra
5047
5077
  const DEFAULT_MAX_FIRES = 3;
5048
5078
  /** An edit effect, as it is applied at this turn and round: events default to now and to what those in their place saw. */
5049
5079
  function editOf(target, raw, options) {
5080
+ const round = Math.max(1, options.round);
5050
5081
  const events = Array.isArray(raw["events"]) ? raw["events"].filter(isObject).map((event) => {
5051
5082
  const where = typeof event["location"] === "string" ? target.locations.list().find((entry) => locationKey(entry) === event["location"])?.id : void 0;
5052
5083
  if (event["perceived_by"] === "everyone") return {
5053
5084
  turn: options.turn,
5054
- round: options.round,
5085
+ round,
5055
5086
  importance: 3,
5056
5087
  tags: [],
5057
5088
  involves: [],
@@ -5067,7 +5098,7 @@ function editOf(target, raw, options) {
5067
5098
  }));
5068
5099
  return {
5069
5100
  turn: options.turn,
5070
- round: options.round,
5101
+ round,
5071
5102
  importance: 3,
5072
5103
  tags: [],
5073
5104
  involves: [],
@@ -5185,7 +5216,9 @@ const DAY$1 = 864e5;
5185
5216
  const DEFAULT_MEMORY_POLICY = {
5186
5217
  flaky: true,
5187
5218
  recall: 12,
5188
- overRetrieve: 4
5219
+ overRetrieve: 4,
5220
+ associate: 2,
5221
+ diversity: .3
5189
5222
  };
5190
5223
  /** Story days for the memory of an unimportant event to halve; each step of importance doubles it. */
5191
5224
  const HALF_LIFE_DAYS = 6;
@@ -5316,7 +5349,7 @@ const DENSE_RANGE = .5;
5316
5349
  const denseScore = (similarity) => Math.min(1, Math.max(0, (similarity - DENSE_FLOOR) / DENSE_RANGE));
5317
5350
  /** Being where something happened calls it to mind, about as much as sharing two words with it. */
5318
5351
  const PLACE_CUE = 2;
5319
- function gather(source, { characterId, characters, situation, locationId }) {
5352
+ function gather(source, { characterId, characters, situation, locationId, about }) {
5320
5353
  const names = new Map(characters.map((character) => [character.id, character.name]));
5321
5354
  const peers = mentioned(situation, characters);
5322
5355
  peers.delete(characterId);
@@ -5334,24 +5367,46 @@ function gather(source, { characterId, characters, situation, locationId }) {
5334
5367
  }
5335
5368
  const atTurn = (eventTurn) => turns.get(eventTurn) ?? start;
5336
5369
  const ageDays = (at) => now === void 0 || at === void 0 ? 0 : Math.max(0, now - at) / DAY$1;
5337
- return [...source.events.perceivedBy(characterId).map((seen) => {
5370
+ const concerns = (ids) => about === void 0 || about.length === 0 || ids.some((id) => about.includes(id));
5371
+ return [...source.events.perceivedBy(characterId).filter((seen) => concerns(seen.event.involves)).map((seen) => {
5338
5372
  const at = rounds.get(`${seen.event.turn}:${seen.event.round}`) ?? atTurn(seen.event.turn);
5373
+ const cue = {
5374
+ people: peopleCue(seen.event.involves),
5375
+ words: overlap(cues, `${seen.perception.content} ${seen.event.text}`, seen.event.tags),
5376
+ place: locationId !== void 0 && seen.event.locationId === locationId ? PLACE_CUE : 0
5377
+ };
5339
5378
  return {
5340
5379
  key: `p:${seen.perception.id}`,
5380
+ links: {
5381
+ people: seen.event.involves.filter((id) => id !== characterId),
5382
+ tags: seen.event.tags.map((tag) => tag.toLowerCase()),
5383
+ ...seen.event.locationId === void 0 ? {} : { place: seen.event.locationId }
5384
+ },
5341
5385
  recalled: describeEvent(seen, names, at),
5342
- relevance: peopleCue(seen.event.involves) + overlap(cues, `${seen.perception.content} ${seen.event.text}`, seen.event.tags) + (locationId !== void 0 && seen.event.locationId === locationId ? PLACE_CUE : 0),
5386
+ cues: cue,
5387
+ relevance: cue.people + cue.words + cue.place,
5343
5388
  importance: seen.event.importance,
5344
5389
  ageDays: ageDays(at),
5345
5390
  recalls: seen.perception.recalls,
5346
5391
  seed: seen.perception.id,
5347
5392
  durability: 1
5348
5393
  };
5349
- }), ...source.memories.list(characterId).map((belief) => {
5394
+ }), ...source.memories.list(characterId).filter((belief) => concerns(belief.about)).map((belief) => {
5350
5395
  const at = atTurn(belief.turn);
5396
+ const cue = {
5397
+ people: peopleCue(belief.about),
5398
+ words: overlap(cues, belief.text, []),
5399
+ place: 0
5400
+ };
5351
5401
  return {
5352
5402
  key: `m:${belief.id}`,
5403
+ links: {
5404
+ people: belief.about.filter((id) => id !== characterId),
5405
+ tags: []
5406
+ },
5353
5407
  recalled: describeBelief(belief, at),
5354
- relevance: peopleCue(belief.about) + overlap(cues, belief.text, []),
5408
+ cues: cue,
5409
+ relevance: cue.people + cue.words,
5355
5410
  importance: belief.importance,
5356
5411
  ageDays: ageDays(at),
5357
5412
  recalls: belief.recalls,
@@ -5380,20 +5435,25 @@ function fuseScores(searches) {
5380
5435
  /** How well a candidate stands once its relevance is known: what the moment calls to mind, how much it matters and how recent it is. */
5381
5436
  function judged(candidate, relevance, policy) {
5382
5437
  let clarity = "clear";
5383
- if (policy.flaky) clarity = clarityAt(retention({
5384
- ageDays: candidate.ageDays,
5385
- importance: candidate.importance,
5386
- recalls: candidate.recalls,
5387
- seed: candidate.seed,
5388
- durability: candidate.durability,
5389
- cue: relevance >= CUE_RELEVANCE
5390
- }), candidate.importance);
5438
+ let kept;
5439
+ if (policy.flaky) {
5440
+ kept = retention({
5441
+ ageDays: candidate.ageDays,
5442
+ importance: candidate.importance,
5443
+ recalls: candidate.recalls,
5444
+ seed: candidate.seed,
5445
+ durability: candidate.durability,
5446
+ cue: relevance >= CUE_RELEVANCE
5447
+ });
5448
+ clarity = clarityAt(kept, candidate.importance);
5449
+ }
5391
5450
  if (clarity === void 0) return void 0;
5392
5451
  const score = RELEVANCE_SCALE * relevance + .6 * candidate.importance + 2 / (1 + Math.max(0, candidate.ageDays)) + (clarity === "vague" ? -1 : 0);
5393
5452
  return {
5394
5453
  clarity,
5395
5454
  score,
5396
- text: candidate.recalled[clarity === "clear" ? "clear" : clarity === "faded" ? "faded" : "vague"]
5455
+ text: candidate.recalled[clarity === "clear" ? "clear" : clarity === "faded" ? "faded" : "vague"],
5456
+ ...kept === void 0 ? {} : { retention: kept }
5397
5457
  };
5398
5458
  }
5399
5459
  /**
@@ -5404,7 +5464,7 @@ function judged(candidate, relevance, policy) {
5404
5464
  * is, and not what has faded from memory altogether). A reranker may then judge that pool.
5405
5465
  */
5406
5466
  function recallPool(source, query, evidence = {}) {
5407
- const { policy } = query;
5467
+ const { policy, explain } = query;
5408
5468
  const candidates = gather(source, query);
5409
5469
  const fused = fuseScores([
5410
5470
  {
@@ -5421,18 +5481,28 @@ function recallPool(source, query, evidence = {}) {
5421
5481
  }
5422
5482
  ]);
5423
5483
  const width = Math.min(MAX_POOL, Math.max(policy.recall, Math.ceil(policy.recall * Math.max(1, policy.overRetrieve ?? 1))));
5484
+ const entries = candidates.map((candidate) => ({
5485
+ candidate,
5486
+ fused: fused.get(candidate.key) ?? 0,
5487
+ found: {
5488
+ ...evidence.lexical?.get(candidate.key) === void 0 ? {} : { lexical: evidence.lexical.get(candidate.key) },
5489
+ ...evidence.dense?.get(candidate.key) === void 0 ? {} : { dense: evidence.dense.get(candidate.key) }
5490
+ }
5491
+ })).flatMap((entry) => {
5492
+ const standing = judged(entry.candidate, entry.fused, policy);
5493
+ return standing === void 0 ? [] : [{
5494
+ entry,
5495
+ score: standing.score
5496
+ }];
5497
+ }).sort((a, b) => b.score - a.score || b.entry.candidate.recalled.turn - a.entry.candidate.recalled.turn).map(({ entry }) => entry).slice(0, width);
5498
+ const kept = new Set(entries.map((entry) => entry.candidate.key));
5424
5499
  return {
5425
- entries: candidates.map((candidate) => ({
5426
- candidate,
5427
- fused: fused.get(candidate.key) ?? 0
5428
- })).flatMap((entry) => {
5429
- const standing = judged(entry.candidate, entry.fused, policy);
5430
- return standing === void 0 ? [] : [{
5431
- entry,
5432
- score: standing.score
5433
- }];
5434
- }).sort((a, b) => b.score - a.score || b.entry.candidate.recalled.turn - a.entry.candidate.recalled.turn).slice(0, width).map(({ entry }) => entry),
5435
- query: { policy }
5500
+ entries,
5501
+ others: candidates.filter((candidate) => !kept.has(candidate.key)),
5502
+ query: {
5503
+ policy,
5504
+ ...explain === true ? { explain } : {}
5505
+ }
5436
5506
  };
5437
5507
  }
5438
5508
  /** What a reranker is asked to judge: every candidate of the pool, with the text it would be recalled by. */
@@ -5444,26 +5514,129 @@ function poolCandidates(pool) {
5444
5514
  importance: candidate.importance
5445
5515
  }));
5446
5516
  }
5517
+ /** Two things are tied by the same people, the same tags and the same place; this is how strongly (people count most, tags and place add a little). */
5518
+ function tie(a, b) {
5519
+ const people = a.people.filter((id) => b.people.includes(id)).length;
5520
+ const tags = a.tags.filter((tag) => b.tags.includes(tag)).length;
5521
+ const place = a.place !== void 0 && a.place === b.place;
5522
+ return {
5523
+ strength: 2 * Math.min(people, 2) + Math.min(tags, 2) + (place ? 1 : 0),
5524
+ people,
5525
+ tags,
5526
+ place
5527
+ };
5528
+ }
5529
+ /** The least a tie has to be for one thing to bring the other to mind. */
5530
+ const STRONG_TIE = 3;
5531
+ /** What does not matter much is not brought back by association. */
5532
+ const ASSOCIABLE_IMPORTANCE = 3;
5533
+ /** How much two texts say the same, from 0 to 1: the share of their words (or pairs of characters) they have in common. */
5534
+ function resemblance(a, b) {
5535
+ if (a.size === 0 || b.size === 0) return 0;
5536
+ let shared = 0;
5537
+ for (const word of a) if (b.has(word)) shared++;
5538
+ return shared / (a.size + b.size - shared);
5539
+ }
5447
5540
  /**
5448
- * The last stage: choose the `policy.recall` best of the pool. With a reranker's judgement each
5541
+ * Choose `count` of the ranked entries by maximal marginal relevance: each pick is the one with the best score
5542
+ * discounted by how much it resembles what is already chosen, so that three memories of the same moment do not
5543
+ * crowd out one of another. `diversity` 0 keeps the ranking as it is.
5544
+ */
5545
+ function diversified(ranked, count, diversity) {
5546
+ const weight = Math.min(1, Math.max(0, diversity));
5547
+ if (weight === 0 || ranked.length <= 1) return ranked.slice(0, count);
5548
+ const top = Math.max(...ranked.map((entry) => entry.score), Number.EPSILON);
5549
+ const words = new Map(ranked.map((entry) => [entry, tokens(entry.candidate.recalled.clear)]));
5550
+ const left = [...ranked];
5551
+ const chosen = [];
5552
+ while (chosen.length < count && left.length > 0) {
5553
+ let best = 0;
5554
+ let bestValue = -Infinity;
5555
+ left.forEach((entry, index) => {
5556
+ const repeat = Math.max(0, ...chosen.map((picked) => resemblance(words.get(entry), words.get(picked))));
5557
+ const value = (1 - weight) * Math.max(0, entry.score) / top - weight * repeat;
5558
+ if (value > bestValue) {
5559
+ best = index;
5560
+ bestValue = value;
5561
+ }
5562
+ });
5563
+ chosen.push(left.splice(best, 1)[0]);
5564
+ }
5565
+ return chosen;
5566
+ }
5567
+ /**
5568
+ * The last stage: choose the `policy.recall` best of the pool, spread by `policy.diversity`. With a reranker's judgement each
5449
5569
  * candidate's relevance is mostly that (a candidate it left out counts as 0), and the rest is what
5450
5570
  * retrieval found; without, it is what retrieval found.
5451
5571
  */
5452
5572
  function finishRecall(pool, rerank) {
5453
5573
  const { policy } = pool.query;
5454
- return pool.entries.flatMap(({ candidate, fused }) => {
5455
- const standing = judged(candidate, rerank === void 0 ? fused : RERANK_BLEND * Math.min(1, Math.max(0, rerank.get(candidate.key) ?? 0)) + .30000000000000004 * fused, policy);
5456
- return standing === void 0 ? [] : [{
5574
+ const standing = pool.entries.flatMap(({ candidate, fused, found }) => {
5575
+ const relevance = rerank === void 0 ? fused : RERANK_BLEND * Math.min(1, Math.max(0, rerank.get(candidate.key) ?? 0)) + .30000000000000004 * fused;
5576
+ const judgement = judged(candidate, relevance, policy);
5577
+ if (judgement === void 0) return [];
5578
+ const why = pool.query.explain === true ? {
5579
+ cues: candidate.cues,
5580
+ ...found,
5581
+ ...rerank === void 0 ? {} : { rerank: rerank.get(candidate.key) ?? 0 },
5582
+ relevance,
5583
+ importance: candidate.importance,
5584
+ ageDays: candidate.ageDays,
5585
+ ...judgement.retention === void 0 ? {} : { retention: judgement.retention },
5586
+ score: judgement.score
5587
+ } : void 0;
5588
+ return [{
5457
5589
  candidate,
5458
- ...standing
5590
+ relevance,
5591
+ ...judgement,
5592
+ why
5459
5593
  }];
5460
- }).sort((a, b) => b.score - a.score || b.candidate.recalled.turn - a.candidate.recalled.turn).slice(0, policy.recall).sort((a, b) => a.candidate.recalled.turn - b.candidate.recalled.turn).map(({ candidate, clarity, text }) => ({
5594
+ }).sort((a, b) => b.score - a.score || b.candidate.recalled.turn - a.candidate.recalled.turn);
5595
+ const chosen = diversified(standing, policy.recall, policy.diversity ?? 0);
5596
+ const result = chosen.map(({ candidate, clarity, text, why }) => ({
5461
5597
  clarity,
5462
5598
  text,
5463
5599
  turn: candidate.recalled.turn,
5464
5600
  ...candidate.recalled.at === void 0 ? {} : { at: candidate.recalled.at },
5465
- source: candidate.recalled.source
5601
+ source: candidate.recalled.source,
5602
+ ...why === void 0 ? {} : { why }
5466
5603
  }));
5604
+ const anchors = chosen.filter((entry) => entry.relevance >= CUE_RELEVANCE);
5605
+ const wanted = anchors.length === 0 ? 0 : Math.max(0, policy.associate ?? 0);
5606
+ if (wanted > 0) {
5607
+ const taken = new Set(chosen.map((entry) => entry.candidate.key));
5608
+ const tied = [...standing.filter((entry) => !chosen.includes(entry)).map((entry) => entry.candidate), ...pool.others].filter((candidate) => !taken.has(candidate.key) && candidate.importance >= ASSOCIABLE_IMPORTANCE).flatMap((candidate) => {
5609
+ const best = anchors.map((anchor) => tie(anchor.candidate.links, candidate.links)).sort((a, b) => b.strength - a.strength)[0];
5610
+ const judgement = best === void 0 || best.strength < STRONG_TIE ? void 0 : judged(candidate, 0, policy);
5611
+ return best === void 0 || judgement === void 0 || judgement.clarity === "vague" ? [] : [{
5612
+ candidate,
5613
+ best,
5614
+ judgement
5615
+ }];
5616
+ }).sort((a, b) => b.best.strength - a.best.strength || b.candidate.importance - a.candidate.importance || b.candidate.recalled.turn - a.candidate.recalled.turn).slice(0, wanted);
5617
+ for (const { candidate, best, judgement } of tied) result.push({
5618
+ clarity: judgement.clarity,
5619
+ text: judgement.text,
5620
+ turn: candidate.recalled.turn,
5621
+ ...candidate.recalled.at === void 0 ? {} : { at: candidate.recalled.at },
5622
+ source: candidate.recalled.source,
5623
+ linked: true,
5624
+ ...pool.query.explain === true ? { why: {
5625
+ cues: candidate.cues,
5626
+ relevance: 0,
5627
+ importance: candidate.importance,
5628
+ ageDays: candidate.ageDays,
5629
+ ...judgement.retention === void 0 ? {} : { retention: judgement.retention },
5630
+ score: judgement.score,
5631
+ linked: {
5632
+ people: best.people,
5633
+ tags: best.tags,
5634
+ place: best.place
5635
+ }
5636
+ } } : {}
5637
+ });
5638
+ }
5639
+ return result.sort((a, b) => a.turn - b.turn);
5467
5640
  }
5468
5641
  /** Remind a character of what the moment calls to mind, in one call: {@link recallPool}, then {@link finishRecall} with the `rerank` evidence if there is any. */
5469
5642
  function recall(source, query, evidence = {}) {
@@ -6716,7 +6889,7 @@ var SqliteItemRepository = class {
6716
6889
  patch.notable ?? current.notable ? 1 : 0,
6717
6890
  patch.hidden ?? current.hidden ? 1 : 0,
6718
6891
  patch.container?.state ?? current.container?.state ?? null,
6719
- patch.ownerId ?? current.ownerId ?? null,
6892
+ patch.ownerId === void 0 ? current.ownerId ?? null : patch.ownerId,
6720
6893
  id
6721
6894
  ]);
6722
6895
  return this.require(id);
@@ -7090,10 +7263,16 @@ var SqliteLocationRepository = class {
7090
7263
  const features = patch.features === void 0 ? current.features : checkedFeatures(patch.features);
7091
7264
  const removed = current.features.filter((feature) => !features.some((kept) => kept.key === feature.key));
7092
7265
  for (const feature of removed) if (this.db.get("SELECT 1 FROM placements WHERE location_id = ? AND feature_key = ?", [id, feature.key]) !== void 0) throw new ConflictError(`feature "${feature.key}" cannot be removed while someone is at it`);
7093
- this.db.run("UPDATE locations SET name = ?, description = ?, features = ? WHERE id = ?", [
7266
+ const parentId = patch.parentId === void 0 ? current.parentId ?? null : patch.parentId;
7267
+ if (parentId !== null && parentId !== (current.parentId ?? null)) {
7268
+ if (this.get(parentId) === void 0) throw new NotFoundError(`the parent place ${parentId} does not exist`);
7269
+ for (let ancestor = parentId; ancestor !== null; ancestor = this.get(ancestor)?.parentId ?? null) if (ancestor === id) throw new InvalidValueError(`${current.name} cannot be inside itself or a place within it`);
7270
+ }
7271
+ this.db.run("UPDATE locations SET name = ?, description = ?, features = ?, parent_id = ? WHERE id = ?", [
7094
7272
  name,
7095
7273
  (patch.description ?? current.description).trim(),
7096
7274
  toJson(features),
7275
+ parentId,
7097
7276
  id
7098
7277
  ]);
7099
7278
  if (name !== current.name) this.renamed(id);
@@ -7621,10 +7800,10 @@ var SqliteMetadataRepository = class {
7621
7800
  //#endregion
7622
7801
  //#region src/sqlite/schema.ts
7623
7802
  /**
7624
- * Migrations are applied in order and never edited once released: a change to
7625
- * the schema is a new entry. Until the first release the schema is still being
7626
- * shaped, so migration 1 is edited in place. Enumerations are spelled out so that
7627
- * a later change to the model cannot alter what an old migration did.
7803
+ * Migrations are applied in order and never edited once released (0.1.0 released
7804
+ * versions 1 to 6; tests/sqlite/migrate.spec.ts pins them): a change to the schema
7805
+ * is a new entry. Enumerations are spelled out so that a later change to the model
7806
+ * cannot alter what an old migration did.
7628
7807
  */
7629
7808
  const MIGRATIONS = [
7630
7809
  {
@@ -9138,6 +9317,7 @@ function openSessionStore(location, options = {}) {
9138
9317
  try {
9139
9318
  const state = inspect(db);
9140
9319
  if (state.version === 0 || state.applicationId !== 1146245971) throw new SchemaError(`${location} is not a session database`);
9320
+ if (state.version < LATEST_VERSION && location !== ":memory:") backUpBeforeMigrating(db, location, state.version);
9141
9321
  migrate(db);
9142
9322
  return new SqliteSessionStore(db, options.clock ?? systemClock, vectors);
9143
9323
  } catch (error) {
@@ -9145,6 +9325,24 @@ function openSessionStore(location, options = {}) {
9145
9325
  throw error;
9146
9326
  }
9147
9327
  }
9328
+ /** Folder, beside the database, holding the copies made before a schema upgrade. */
9329
+ const MIGRATION_BACKUP_FOLDER = "migration-backups";
9330
+ /** Where the copy of a database as it was at schema `version` is kept. */
9331
+ function migrationBackupPath(location, version) {
9332
+ return join(dirname(location), MIGRATION_BACKUP_FOLDER, `schema-${version}.sqlite`);
9333
+ }
9334
+ /**
9335
+ * A released session cannot be rebuilt, so an upgrade starts from a copy that passed the integrity check; one copy per old version is kept.
9336
+ * @throws {CorruptDataError} when the database is damaged, before anything is changed.
9337
+ */
9338
+ function backUpBeforeMigrating(db, location, version) {
9339
+ const check = db.get("PRAGMA quick_check")?.quick_check;
9340
+ if (check !== "ok") throw new CorruptDataError(`${location} failed its integrity check, so it was not upgraded: ${check ?? "no result"}`);
9341
+ const target = migrationBackupPath(location, version);
9342
+ if (existsSync(target)) return;
9343
+ mkdirSync(dirname(target), { recursive: true });
9344
+ db.run("VACUUM INTO ?", [target]);
9345
+ }
9148
9346
  //#endregion
9149
9347
  //#region src/sessions.ts
9150
9348
  const SESSION_DATABASE_FILE = "session.sqlite";
@@ -11161,11 +11359,17 @@ function createCharacterTools(link, characterId) {
11161
11359
  defineTool({
11162
11360
  name: "recall_my_memories",
11163
11361
  description: "Ask yourself what you remember about something: a person, an object, a place or an earlier moment. You get your own version of it, what you took in yourself and what you believe, and what has faded comes back as hazy or not at all. Use it only when you need a memory that the situation does not already give you.",
11164
- parameters: { about: {
11165
- type: "string",
11166
- required: true,
11167
- description: "What to remember: a name, an object, a topic or a short description of the moment."
11168
- } },
11362
+ parameters: {
11363
+ about: {
11364
+ type: "string",
11365
+ required: true,
11366
+ description: "What to remember: a name, an object, a topic or a short description of the moment."
11367
+ },
11368
+ person: {
11369
+ type: "string",
11370
+ description: "Only what concerns this person (a name or alias you know): everything you remember of them."
11371
+ }
11372
+ },
11169
11373
  output: {
11170
11374
  schema: {
11171
11375
  type: "array",
@@ -11190,20 +11394,24 @@ function createCharacterTools(link, characterId) {
11190
11394
  },
11191
11395
  render: (_args, value) => [{
11192
11396
  type: "text",
11193
- text: value.length === 0 ? "You do not remember anything about that." : value.map((entry) => `- (${entry.clarity === "clear" ? "" : entry.clarity === "faded" ? "the details are fuzzy" : "you barely remember"}) ${entry.text}`.replace("- () ", "- ")).join("\n")
11397
+ text: value.length === 0 ? "You do not remember anything about that, and there is nothing more to find: go on with what you know." : value.map((entry) => `- (${entry.clarity === "clear" ? "" : entry.clarity === "faded" ? "the details are fuzzy" : "you barely remember"}) ${entry.text}`.replace("- () ", "- ")).join("\n")
11194
11398
  }]
11195
11399
  },
11196
11400
  isConcurrencySafe: () => false,
11197
11401
  async execute(args, exec) {
11198
11402
  const agent = link();
11199
11403
  const { session, turn, policy } = agent;
11404
+ const who = args.person === void 0 || args.person.trim() === "" ? [] : [...mentioned(args.person, session.characters.list())].filter((id) => id !== characterId);
11405
+ if (args.person !== void 0 && args.person.trim() !== "" && who.length === 0) return [];
11406
+ const situation = who.length === 0 ? args.about : `${args.about} ${args.person}`;
11200
11407
  return (await session.recallRanked({
11201
11408
  characterId,
11202
- situation: args.about,
11409
+ situation,
11203
11410
  turn,
11204
11411
  policy,
11412
+ ...who.length === 0 ? {} : { about: who },
11205
11413
  ...placeOf(session, characterId)
11206
- }, exec.signal, await vectorOf(agent, args.about, exec.signal))).map((entry) => ({
11414
+ }, exec.signal, await vectorOf(agent, situation, exec.signal))).map((entry) => ({
11207
11415
  clarity: entry.clarity,
11208
11416
  turn: entry.turn,
11209
11417
  text: entry.text
@@ -11241,7 +11449,7 @@ function createCharacterTools(link, characterId) {
11241
11449
  },
11242
11450
  render: (_args, value) => [{
11243
11451
  type: "text",
11244
- text: value.length === 0 ? "You know of nothing matching that." : value.map((entry) => `- ${entry.kind}: ${entry.name} (${entry.key})`).join("\n")
11452
+ text: value.length === 0 ? "You know of nothing matching that." : value.map((entry) => `- ${entry.kind}: ${entry.name}`).join("\n")
11245
11453
  }]
11246
11454
  },
11247
11455
  isConcurrencySafe: () => true,
@@ -11352,6 +11560,6 @@ function createCharacterTools(link, characterId) {
11352
11560
  ];
11353
11561
  }
11354
11562
  //#endregion
11355
- export { ACCURACIES, CHARACTER_CONTROLS, CHARACTER_KINDS, CHARACTER_ROLES, CHARACTER_TIERS, CLOCK_MODES, COMPARISONS, CONSOLIDATION_KINDS, CONTAINER_STATES, Config, ConflictError, CorruptDataError, DAY, DAY_PARTS, DEFAULT_MEMORY_POLICY, DEFAULT_SLOTS, EMOTION_VARIABLES, ENGAGEMENT_GRACE, ENGAGEMENT_STATUSES, EXIT_STATES, HOUR, IMPORTANCE_MAX, IMPORTANCE_MIN, INTENT_HORIZONS, INTENT_STATUSES, INTENT_TYPES, ITEM_ACTIONS, InvalidEditError, InvalidMoveError, InvalidValueError, KNOWLEDGE_KINDS, LORE_KINDS, LORE_SUBJECT_KINDS, MAX_LOCATION_FEATURES, MAX_ROUTINE_WINDOWS, MAX_SHOWN, MAX_TRIGGERS, MAX_VARIABLES, MEMBERSHIP_STATUSES, MEMORY_KINDS, MINUTE, NotFoundError, PERCEPTION_MODES, PLACEABLE_KINDS, PLOT_NUDGE_MODES, PLOT_STATUSES, RECALL_ITEM_KINDS, RoleplayState, LATEST_VERSION as SCHEMA_VERSION, SECOND, SESSION_DATABASE_FILE, SLOT_ID, SUMMARY_LEVELS, SchemaError, StateError, THREAD_STATUSES, TIME_CHANGE_KINDS, TRIGGER_NOTE_KINDS, TRIGGER_STATUSES, VARIABLE_KEY, VARIABLE_KINDS, VARIABLE_SUBJECTS, VARIABLE_VISIBILITIES, VISUAL_SUBJECT_KINDS, WEEKDAY_NAMES, addLocalDays, agendaAt, announcements, apply, applyItemPlan, applyRecordsEdit, audienceIds, catchUpPlan, changeItem, changeVariable, characterKey, clarityAt, clockTime, conditionHolds, createCharacterTools, createSession, createSessionStore, createTools, dayPart, deleteSession, denseScore, describeDuration, describeHolder, describeInstant, describeValue, finishRecall, fuseScores, inAudience, inWindow, isSessionId, isTimeZone, itemKey, jitter, lapseNotes, lintWorld, listSessions, localIso, localTime, locationKey, mentioned, moveCharacter, name, nextOccurrence, nextTimeOfDay, nodeSqliteDriver, occurrenceAt, openSession, openSessionStore, overlap, parseInstant, parseRecordsEdit, parseTimeOfDay, parseTriggerDefinition, planItemChange, planKnownMove, planMove, planVariableChange, poolCandidates, rankGoals, readRecords, readTriggerDefinition, recall, recallCandidates, recallPool, recordsEditSchema, retention, runTriggers, seededRoll, sessionDatabase, sessionFolder, shownVariables, subjectOf, systemClock, systemTimeZone, tokens, triggerReferenceProblems, valueIssue, zonedInstant };
11563
+ export { ACCURACIES, CHARACTER_CONTROLS, CHARACTER_KINDS, CHARACTER_ROLES, CHARACTER_TIERS, CLOCK_MODES, COMPARISONS, CONSOLIDATION_KINDS, CONTAINER_STATES, Config, ConflictError, CorruptDataError, DAY, DAY_PARTS, DEFAULT_MEMORY_POLICY, DEFAULT_SLOTS, EMOTION_VARIABLES, ENGAGEMENT_GRACE, ENGAGEMENT_STATUSES, EXIT_STATES, HOUR, IMPORTANCE_MAX, IMPORTANCE_MIN, INTENT_HORIZONS, INTENT_STATUSES, INTENT_TYPES, ITEM_ACTIONS, InvalidEditError, InvalidMoveError, InvalidValueError, KNOWLEDGE_KINDS, LORE_KINDS, LORE_SUBJECT_KINDS, MAX_LOCATION_FEATURES, MAX_ROUTINE_WINDOWS, MAX_SHOWN, MAX_TRIGGERS, MAX_VARIABLES, MEMBERSHIP_STATUSES, MEMORY_KINDS, MIGRATION_BACKUP_FOLDER, MINUTE, NotFoundError, PERCEPTION_MODES, PLACEABLE_KINDS, PLOT_NUDGE_MODES, PLOT_STATUSES, RECALL_ITEM_KINDS, RoleplayState, LATEST_VERSION as SCHEMA_VERSION, SECOND, SESSION_DATABASE_FILE, SLOT_ID, SUMMARY_LEVELS, SchemaError, StateError, THREAD_STATUSES, TIME_CHANGE_KINDS, TRIGGER_NOTE_KINDS, TRIGGER_STATUSES, VARIABLE_KEY, VARIABLE_KINDS, VARIABLE_SUBJECTS, VARIABLE_VISIBILITIES, VISUAL_SUBJECT_KINDS, WEEKDAY_NAMES, addLocalDays, agendaAt, announcements, apply, applyItemPlan, applyRecordsEdit, audienceIds, catchUpPlan, changeItem, changeVariable, characterKey, clarityAt, clockTime, conditionHolds, createCharacterTools, createSession, createSessionStore, createTools, dayPart, deleteSession, denseScore, describeDuration, describeHolder, describeInstant, describeValue, finishRecall, fuseScores, inAudience, inWindow, isSessionId, isTimeZone, itemKey, jitter, judgedStatements, lapseNotes, lintWorld, listSessions, localIso, localTime, locationKey, mentioned, migrationBackupPath, moveCharacter, name, nextOccurrence, nextTimeOfDay, nodeSqliteDriver, occurrenceAt, openSession, openSessionStore, overlap, parseInstant, parseRecordsEdit, parseTimeOfDay, parseTriggerDefinition, planItemChange, planKnownMove, planMove, planVariableChange, poolCandidates, rankGoals, readRecords, readTriggerDefinition, recall, recallCandidates, recallPool, recordsEditSchema, retention, runTriggers, seededRoll, sessionDatabase, sessionFolder, shownVariables, subjectOf, systemClock, systemTimeZone, tokens, triggerReferenceProblems, valueIssue, zonedInstant };
11356
11564
 
11357
11565
  //# sourceMappingURL=index.js.map