@possumtech/rummy 0.2.8 → 0.3.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.
Files changed (114) hide show
  1. package/.env.example +13 -2
  2. package/EXCEPTIONS.md +46 -0
  3. package/PLUGINS.md +422 -188
  4. package/SPEC.md +440 -106
  5. package/migrations/001_initial_schema.sql +5 -3
  6. package/package.json +17 -5
  7. package/service.js +5 -3
  8. package/src/agent/AgentLoop.js +252 -55
  9. package/src/agent/ContextAssembler.js +20 -4
  10. package/src/agent/KnownStore.js +82 -25
  11. package/src/agent/ProjectAgent.js +4 -1
  12. package/src/agent/ResponseHealer.js +86 -32
  13. package/src/agent/TurnExecutor.js +542 -207
  14. package/src/agent/XmlParser.js +77 -41
  15. package/src/agent/known_store.sql +68 -4
  16. package/src/agent/schemes.sql +3 -0
  17. package/src/agent/tokens.js +7 -21
  18. package/src/agent/turns.sql +15 -1
  19. package/src/hooks/HookRegistry.js +7 -0
  20. package/src/hooks/Hooks.js +15 -0
  21. package/src/hooks/PluginContext.js +14 -1
  22. package/src/hooks/RummyContext.js +16 -4
  23. package/src/hooks/ToolRegistry.js +77 -19
  24. package/src/llm/LlmProvider.js +27 -8
  25. package/src/llm/OpenAiClient.js +20 -0
  26. package/src/llm/OpenRouterClient.js +24 -2
  27. package/src/llm/XaiClient.js +47 -2
  28. package/src/plugins/ask_user/README.md +4 -4
  29. package/src/plugins/ask_user/ask_user.js +5 -5
  30. package/src/plugins/ask_user/ask_userDoc.js +29 -0
  31. package/src/plugins/budget/README.md +31 -0
  32. package/src/plugins/budget/budget.js +55 -0
  33. package/src/plugins/cp/README.md +5 -4
  34. package/src/plugins/cp/cp.js +10 -6
  35. package/src/plugins/cp/cpDoc.js +29 -0
  36. package/src/plugins/engine/engine.sql +1 -8
  37. package/src/plugins/engine/turn_context.sql +4 -9
  38. package/src/plugins/env/README.md +3 -4
  39. package/src/plugins/env/env.js +5 -5
  40. package/src/plugins/env/envDoc.js +29 -0
  41. package/src/plugins/file/README.md +9 -12
  42. package/src/plugins/file/file.js +34 -35
  43. package/src/plugins/get/README.md +2 -2
  44. package/src/plugins/get/get.js +77 -6
  45. package/src/plugins/get/getDoc.js +51 -0
  46. package/src/plugins/hedberg/hedberg.js +2 -1
  47. package/src/plugins/hedberg/matcher.js +10 -29
  48. package/src/plugins/hedberg/normalize.js +28 -0
  49. package/src/plugins/hedberg/patterns.js +25 -27
  50. package/src/plugins/hedberg/sed.js +17 -10
  51. package/src/plugins/index.js +66 -14
  52. package/src/plugins/instructions/README.md +6 -2
  53. package/src/plugins/instructions/instructions.js +20 -4
  54. package/src/plugins/instructions/preamble.md +19 -5
  55. package/src/plugins/known/README.md +10 -7
  56. package/src/plugins/known/known.js +23 -17
  57. package/src/plugins/known/knownDoc.js +34 -0
  58. package/src/plugins/mv/README.md +5 -4
  59. package/src/plugins/mv/mv.js +27 -6
  60. package/src/plugins/mv/mvDoc.js +45 -0
  61. package/src/plugins/performed/README.md +15 -0
  62. package/src/plugins/performed/performed.js +45 -0
  63. package/src/plugins/persona/persona.js +78 -0
  64. package/src/plugins/previous/README.md +3 -2
  65. package/src/plugins/previous/previous.js +33 -24
  66. package/src/plugins/progress/README.md +1 -2
  67. package/src/plugins/progress/progress.js +33 -21
  68. package/src/plugins/prompt/README.md +5 -5
  69. package/src/plugins/prompt/prompt.js +15 -17
  70. package/src/plugins/rm/README.md +4 -4
  71. package/src/plugins/rm/rm.js +32 -20
  72. package/src/plugins/rm/rmDoc.js +30 -0
  73. package/src/plugins/rpc/README.md +15 -28
  74. package/src/plugins/rpc/rpc.js +42 -77
  75. package/src/plugins/set/README.md +13 -12
  76. package/src/plugins/set/set.js +107 -16
  77. package/src/plugins/set/setDoc.js +49 -0
  78. package/src/plugins/sh/README.md +4 -4
  79. package/src/plugins/sh/sh.js +5 -5
  80. package/src/plugins/sh/shDoc.js +29 -0
  81. package/src/plugins/{skills/skills.js → skill/skill.js} +10 -51
  82. package/src/plugins/summarize/README.md +6 -5
  83. package/src/plugins/summarize/summarize.js +7 -6
  84. package/src/plugins/summarize/summarizeDoc.js +33 -0
  85. package/src/plugins/telemetry/telemetry.js +16 -9
  86. package/src/plugins/think/README.md +20 -0
  87. package/src/plugins/think/think.js +5 -0
  88. package/src/plugins/unknown/README.md +6 -5
  89. package/src/plugins/unknown/unknown.js +12 -9
  90. package/src/plugins/unknown/unknownDoc.js +31 -0
  91. package/src/plugins/update/README.md +3 -8
  92. package/src/plugins/update/update.js +7 -6
  93. package/src/plugins/update/updateDoc.js +33 -0
  94. package/src/server/ClientConnection.js +59 -45
  95. package/src/server/RpcRegistry.js +52 -4
  96. package/src/sql/v_model_context.sql +10 -25
  97. package/src/plugins/ask_user/docs.md +0 -2
  98. package/src/plugins/cp/docs.md +0 -2
  99. package/src/plugins/current/README.md +0 -14
  100. package/src/plugins/current/current.js +0 -47
  101. package/src/plugins/env/docs.md +0 -4
  102. package/src/plugins/get/docs.md +0 -10
  103. package/src/plugins/known/docs.md +0 -3
  104. package/src/plugins/mv/docs.md +0 -2
  105. package/src/plugins/rm/docs.md +0 -6
  106. package/src/plugins/set/docs.md +0 -6
  107. package/src/plugins/sh/docs.md +0 -2
  108. package/src/plugins/skills/README.md +0 -25
  109. package/src/plugins/store/README.md +0 -20
  110. package/src/plugins/store/docs.md +0 -6
  111. package/src/plugins/store/store.js +0 -63
  112. package/src/plugins/summarize/docs.md +0 -4
  113. package/src/plugins/unknown/docs.md +0 -5
  114. package/src/plugins/update/docs.md +0 -4
@@ -2,9 +2,25 @@ import slugify from "../sql/functions/slugify.js";
2
2
 
3
3
  export default class KnownStore {
4
4
  #db;
5
+ #onChanged;
6
+ #schemes = new Map();
7
+ #seq = 0;
5
8
 
6
- constructor(db) {
9
+ constructor(db, { onChanged } = {}) {
7
10
  this.#db = db;
11
+ this.#onChanged = onChanged || null;
12
+ }
13
+
14
+ async loadSchemes(db) {
15
+ const rows = await (db || this.#db).get_all_schemes.all();
16
+ this.#schemes.clear();
17
+ for (const row of rows) {
18
+ this.#schemes.set(row.name, row);
19
+ }
20
+ }
21
+
22
+ #emitChanged(runId, path, changeType) {
23
+ if (this.#onChanged) this.#onChanged({ runId, path, changeType });
8
24
  }
9
25
 
10
26
  static scheme(path) {
@@ -14,15 +30,16 @@ export default class KnownStore {
14
30
 
15
31
  static normalizePath(path) {
16
32
  if (!path?.includes("://")) return path;
17
- return path.replace(/:\/\/(.*)$/, (_, rest) => {
18
- try {
19
- // Decode first (idempotent), then encode — but preserve slashes
20
- const decoded = decodeURIComponent(rest);
21
- return `://${decoded.split("/").map(encodeURIComponent).join("/")}`;
22
- } catch {
23
- return `://${rest.split("/").map(encodeURIComponent).join("/")}`;
24
- }
25
- });
33
+ const sep = path.indexOf("://");
34
+ const scheme = path.slice(0, sep).toLowerCase();
35
+ const rest = path.slice(sep + 3);
36
+ try {
37
+ // Decode first (idempotent), then encode — but preserve slashes
38
+ const decoded = decodeURIComponent(rest);
39
+ return `${scheme}://${decoded.split("/").map(encodeURIComponent).join("/")}`;
40
+ } catch {
41
+ return `${scheme}://${rest.split("/").map(encodeURIComponent).join("/")}`;
42
+ }
26
43
  }
27
44
 
28
45
  async nextTurn(runId) {
@@ -30,21 +47,24 @@ export default class KnownStore {
30
47
  return row.turn;
31
48
  }
32
49
 
33
- async dedup(runId, scheme, target) {
34
- const candidate = `${scheme}://${target}`;
50
+ async dedup(runId, scheme, target, turn) {
51
+ const encodedTarget = encodeURIComponent(target);
52
+ const turnPrefix = turn ? `turn_${turn}/` : "";
53
+ const candidate = `${scheme}://${turnPrefix}${encodedTarget}`;
35
54
  const existing = await this.#db.get_entry_body.get({
36
55
  run_id: runId,
37
- path: KnownStore.normalizePath(candidate),
56
+ path: candidate,
38
57
  });
39
58
  if (!existing) return candidate;
40
- return `${candidate}_${Date.now()}`;
59
+ return `${candidate}_${++this.#seq}`;
41
60
  }
42
61
 
43
- async slugPath(runId, scheme, content) {
44
- const base = slugify(content || "");
62
+ async slugPath(runId, scheme, content, summary) {
63
+ const source = summary || content || "";
64
+ const base = slugify(source);
45
65
  const prefix = `${scheme}://`;
46
66
 
47
- if (!base) return `${prefix}${Date.now()}`;
67
+ if (!base) return `${prefix}${++this.#seq}`;
48
68
 
49
69
  const candidate = `${prefix}${base}`;
50
70
  const existing = await this.#db.get_entry_body.get({
@@ -53,7 +73,7 @@ export default class KnownStore {
53
73
  });
54
74
  if (!existing) return candidate;
55
75
 
56
- return `${prefix}${base}_${Date.now()}`;
76
+ return `${prefix}${base}_${++this.#seq}`;
57
77
  }
58
78
 
59
79
  async upsert(
@@ -70,11 +90,12 @@ export default class KnownStore {
70
90
  loopId = null,
71
91
  } = {},
72
92
  ) {
93
+ const normalized = KnownStore.normalizePath(path);
73
94
  await this.#db.upsert_known_entry.run({
74
95
  run_id: runId,
75
96
  loop_id: loopId,
76
97
  turn,
77
- path: KnownStore.normalizePath(path),
98
+ path: normalized,
78
99
  body,
79
100
  status,
80
101
  fidelity,
@@ -82,14 +103,17 @@ export default class KnownStore {
82
103
  attributes: attributes ? JSON.stringify(attributes) : null,
83
104
  updated_at: updatedAt,
84
105
  });
106
+ this.#emitChanged(runId, normalized, "upsert");
85
107
  }
86
108
 
87
109
  async promote(runId, path, turn) {
110
+ const normalized = KnownStore.normalizePath(path);
88
111
  await this.#db.promote_path.run({
89
112
  run_id: runId,
90
- path: KnownStore.normalizePath(path),
113
+ path: normalized,
91
114
  turn,
92
115
  });
116
+ this.#emitChanged(runId, normalized, "promote");
93
117
  }
94
118
 
95
119
  async setFileFidelity(runId, pattern, fidelity) {
@@ -101,28 +125,35 @@ export default class KnownStore {
101
125
  if (result.changes === 0) {
102
126
  await this.upsert(runId, 0, pattern, "", 200, { fidelity });
103
127
  }
128
+ this.#emitChanged(runId, pattern, "fidelity");
104
129
  }
105
130
 
106
131
  async setFidelity(runId, path, fidelity) {
132
+ const normalized = KnownStore.normalizePath(path);
107
133
  await this.#db.set_fidelity.run({
108
134
  run_id: runId,
109
- path: KnownStore.normalizePath(path),
135
+ path: normalized,
110
136
  fidelity,
111
137
  });
138
+ this.#emitChanged(runId, normalized, "fidelity");
112
139
  }
113
140
 
114
141
  async demote(runId, path) {
142
+ const normalized = KnownStore.normalizePath(path);
115
143
  await this.#db.demote_path.run({
116
144
  run_id: runId,
117
- path: KnownStore.normalizePath(path),
145
+ path: normalized,
118
146
  });
147
+ this.#emitChanged(runId, normalized, "demote");
119
148
  }
120
149
 
121
150
  async remove(runId, path) {
151
+ const normalized = KnownStore.normalizePath(path);
122
152
  await this.#db.delete_known_entry.run({
123
153
  run_id: runId,
124
- path: KnownStore.normalizePath(path),
154
+ path: normalized,
125
155
  });
156
+ this.#emitChanged(runId, normalized, "remove");
126
157
  }
127
158
 
128
159
  async removeFilesByPattern(runId, pattern) {
@@ -130,6 +161,7 @@ export default class KnownStore {
130
161
  run_id: runId,
131
162
  pattern,
132
163
  });
164
+ this.#emitChanged(runId, pattern, "remove");
133
165
  }
134
166
 
135
167
  static #bodyPattern(body) {
@@ -143,6 +175,7 @@ export default class KnownStore {
143
175
  body: KnownStore.#bodyPattern(body),
144
176
  turn,
145
177
  });
178
+ this.#emitChanged(runId, path, "promote");
146
179
  }
147
180
 
148
181
  async demoteByPattern(runId, path, body) {
@@ -151,6 +184,7 @@ export default class KnownStore {
151
184
  path,
152
185
  body: KnownStore.#bodyPattern(body),
153
186
  });
187
+ this.#emitChanged(runId, path, "demote");
154
188
  }
155
189
 
156
190
  async getEntriesByPattern(runId, path, body, { limit, offset } = {}) {
@@ -169,6 +203,7 @@ export default class KnownStore {
169
203
  path,
170
204
  body: KnownStore.#bodyPattern(body),
171
205
  });
206
+ this.#emitChanged(runId, path, "remove");
172
207
  }
173
208
 
174
209
  async updateBodyByPattern(runId, path, body, newBody) {
@@ -178,21 +213,41 @@ export default class KnownStore {
178
213
  body: KnownStore.#bodyPattern(body),
179
214
  new_body: newBody,
180
215
  });
216
+ this.#emitChanged(runId, path, "body");
181
217
  }
182
218
 
183
219
  async resolve(runId, path, status, body) {
220
+ const normalized = KnownStore.normalizePath(path);
184
221
  await this.#db.resolve_known_entry.run({
185
222
  run_id: runId,
186
- path: KnownStore.normalizePath(path),
223
+ path: normalized,
187
224
  status,
188
225
  body,
189
226
  });
227
+ this.#emitChanged(runId, normalized, "resolve");
228
+ }
229
+
230
+ async restoreSummarizedPrompts(runId) {
231
+ await this.#db.restore_summarized_prompts.run({ run_id: runId });
232
+ this.#emitChanged(runId, "prompt://batch", "fidelity");
233
+ }
234
+
235
+ async demotePreviousLoopLogging(runId, loopId) {
236
+ await this.#db.demote_previous_loop_logging.run({
237
+ run_id: runId,
238
+ loop_id: loopId,
239
+ });
240
+ this.#emitChanged(runId, "logging://batch", "fidelity");
190
241
  }
191
242
 
192
243
  async getLog(runId) {
193
244
  return this.#db.get_results.all({ run_id: runId });
194
245
  }
195
246
 
247
+ async getEntries(runId) {
248
+ return this.#db.get_known_entries.all({ run_id: runId });
249
+ }
250
+
196
251
  async getFileEntries(runId) {
197
252
  return this.#db.get_file_entries.all({ run_id: runId });
198
253
  }
@@ -237,11 +292,13 @@ export default class KnownStore {
237
292
  }
238
293
 
239
294
  async setAttributes(runId, path, attrs) {
295
+ const normalized = KnownStore.normalizePath(path);
240
296
  await this.#db.update_entry_attributes.run({
241
297
  run_id: runId,
242
- path: KnownStore.normalizePath(path),
298
+ path: normalized,
243
299
  attributes: JSON.stringify(attrs),
244
300
  });
301
+ this.#emitChanged(runId, normalized, "attributes");
245
302
  }
246
303
 
247
304
  async getState(runId, path) {
@@ -14,7 +14,10 @@ export default class ProjectAgent {
14
14
  this.#db = db;
15
15
  this.#hooks = hooks;
16
16
  this.#llm = new LlmProvider(db);
17
- this.#knownStore = new KnownStore(db);
17
+ this.#knownStore = new KnownStore(db, {
18
+ onChanged: (event) => hooks.entry.changed.emit(event).catch(() => {}),
19
+ });
20
+ this.#knownStore.loadSchemes(db);
18
21
 
19
22
  const turnExecutor = new TurnExecutor(
20
23
  db,
@@ -1,10 +1,57 @@
1
1
  const MAX_STALLS = Number(process.env.RUMMY_MAX_STALLS) || 3;
2
- const MAX_REPETITIONS = Number(process.env.RUMMY_MAX_REPETITIONS) || 3;
2
+ const MIN_CYCLES = Number(process.env.RUMMY_MIN_CYCLES) || 3;
3
+ const MAX_CYCLE_PERIOD = Number(process.env.RUMMY_MAX_CYCLE_PERIOD) || 4;
4
+ const MAX_UPDATE_REPEATS = Number(process.env.RUMMY_MAX_UPDATE_REPEATS) || 3;
5
+
6
+ /**
7
+ * Build a stable fingerprint for a single recorded entry.
8
+ * Uses scheme + original command target + all op-defining attributes.
9
+ * Excludes body (content too granular; same operation ≠ same content).
10
+ */
11
+ function cmdFingerprint(entry) {
12
+ const attrs = { ...(entry.attributes ?? {}) };
13
+ delete attrs.body;
14
+ const target =
15
+ attrs.path ?? attrs.command ?? attrs.query ?? attrs.question ?? "";
16
+ delete attrs.path;
17
+ const extra = Object.keys(attrs)
18
+ .toSorted()
19
+ .filter((k) => attrs[k] != null)
20
+ .map((k) => `${k}=${attrs[k]}`)
21
+ .join(",");
22
+ return `${entry.scheme}:${target}${extra ? `[${extra}]` : ""}`;
23
+ }
24
+
25
+ /**
26
+ * Detect a repeating cycle in the fingerprint history.
27
+ * Checks periods 1..MAX_CYCLE_PERIOD for MIN_CYCLES consecutive repetitions.
28
+ * Catches AAAA (period 1), ABABAB (period 2), ABCABCABC (period 3), etc.
29
+ */
30
+ function detectCycle(history) {
31
+ for (let k = 1; k <= MAX_CYCLE_PERIOD; k++) {
32
+ const needed = k * MIN_CYCLES;
33
+ if (history.length < needed) continue;
34
+ const tail = history.slice(-needed);
35
+ const cycle = tail.slice(0, k);
36
+ let match = true;
37
+ outer: for (let rep = 0; rep < MIN_CYCLES; rep++) {
38
+ for (let j = 0; j < k; j++) {
39
+ if (tail[rep * k + j] !== cycle[j]) {
40
+ match = false;
41
+ break outer;
42
+ }
43
+ }
44
+ }
45
+ if (match) return { detected: true, period: k, cycles: MIN_CYCLES };
46
+ }
47
+ return { detected: false };
48
+ }
3
49
 
4
50
  export default class ResponseHealer {
5
51
  #stallCount = 0;
6
- #lastFingerprint = null;
7
- #repetitionCount = 0;
52
+ #turnHistory = [];
53
+ #lastUpdateText = null;
54
+ #updateRepeatCount = 0;
8
55
 
9
56
  /**
10
57
  * Heal a missing status tag. Called when the model emits
@@ -49,38 +96,28 @@ export default class ResponseHealer {
49
96
  }
50
97
 
51
98
  /**
52
- * Check for repeated tool commands across turns.
99
+ * Detect cyclic tool patterns across turns.
53
100
  * Returns { continue: boolean, reason?: string }
54
101
  *
55
- * Fingerprints the commands (name + path/query). If the same fingerprint
56
- * repeats for MAX_REPETITIONS consecutive turns, force-complete.
102
+ * Appends this turn's fingerprint to history, then checks whether the
103
+ * history ends in a repeating cycle of period 1..MAX_CYCLE_PERIOD with
104
+ * at least MIN_CYCLES consecutive repetitions.
105
+ *
106
+ * Catches AAAA (period 1), ABABAB (period 2), ABCABC (period 3), etc.
107
+ * Turns with no tool calls are skipped — they don't contribute to a cycle.
57
108
  */
58
109
  assessRepetition({ actionCalls, writeCalls }) {
59
110
  const commands = [...(actionCalls || []), ...(writeCalls || [])];
60
- if (commands.length === 0) {
61
- this.#lastFingerprint = null;
62
- this.#repetitionCount = 0;
63
- return { continue: true };
64
- }
111
+ if (commands.length === 0) return { continue: true };
65
112
 
66
- const fingerprint = commands
67
- .map((c) => `${c.name}:${c.path || c.command || c.question || ""}`)
68
- .toSorted()
69
- .join("|");
70
-
71
- if (fingerprint === this.#lastFingerprint) {
72
- this.#repetitionCount++;
73
- if (this.#repetitionCount >= MAX_REPETITIONS) {
74
- const reason = `Same commands repeated ${this.#repetitionCount} turns`;
75
- console.warn(`[RUMMY] Loop detected: ${reason}. Force-completing.`);
76
- return { continue: false, reason };
77
- }
78
- console.warn(
79
- `[RUMMY] Repeated commands (${this.#repetitionCount}/${MAX_REPETITIONS}): ${fingerprint.slice(0, 80)}`,
80
- );
81
- } else {
82
- this.#repetitionCount = 1;
83
- this.#lastFingerprint = fingerprint;
113
+ const fp = commands.map(cmdFingerprint).toSorted().join("|");
114
+ this.#turnHistory.push(fp);
115
+
116
+ const cycle = detectCycle(this.#turnHistory);
117
+ if (cycle.detected) {
118
+ const reason = `Cyclic tool pattern (period ${cycle.period}, ${cycle.cycles} repetitions)`;
119
+ console.warn(`[RUMMY] Loop detected: ${reason}. Force-completing.`);
120
+ return { continue: false, reason };
84
121
  }
85
122
 
86
123
  return { continue: true };
@@ -93,11 +130,12 @@ export default class ResponseHealer {
93
130
  *
94
131
  * Rules:
95
132
  * <summarize/> present → done (terminate)
133
+ * <summarize/> + failed actions → overridden to <update> (continue)
96
134
  * <update/> present → continue (model says it's working)
97
135
  * neither present → warn, increment stall counter, continue
98
136
  * stall counter hits MAX_STALLS → force-complete
99
137
  */
100
- assessProgress({ summaryText, updateText, statusHealed }) {
138
+ assessProgress({ summaryText, updateText, statusHealed, flags }) {
101
139
  if (summaryText) {
102
140
  this.#stallCount = 0;
103
141
  return { continue: false };
@@ -105,6 +143,21 @@ export default class ResponseHealer {
105
143
 
106
144
  if (updateText && !statusHealed) {
107
145
  this.#stallCount = 0;
146
+ // Track repeated update text — model stuck declaring readiness
147
+ // But if the model created new entries this turn, it's making
148
+ // progress even if the update text is the same.
149
+ const madeProgress = flags?.hasWrites || flags?.hasReads;
150
+ if (updateText === this.#lastUpdateText && !madeProgress) {
151
+ this.#updateRepeatCount++;
152
+ if (this.#updateRepeatCount >= MAX_UPDATE_REPEATS) {
153
+ const reason = `Same <update/> repeated ${this.#updateRepeatCount} turns: "${updateText.slice(0, 60)}"`;
154
+ console.warn(`[RUMMY] Stalled: ${reason}. Force-completing.`);
155
+ return { continue: false, reason };
156
+ }
157
+ } else {
158
+ this.#lastUpdateText = updateText;
159
+ this.#updateRepeatCount = 1;
160
+ }
108
161
  return { continue: true };
109
162
  }
110
163
 
@@ -128,7 +181,8 @@ export default class ResponseHealer {
128
181
  */
129
182
  reset() {
130
183
  this.#stallCount = 0;
131
- this.#lastFingerprint = null;
132
- this.#repetitionCount = 0;
184
+ this.#turnHistory = [];
185
+ this.#lastUpdateText = null;
186
+ this.#updateRepeatCount = 0;
133
187
  }
134
188
  }