@j0hanz/thinkseq-mcp 2.0.0 → 2.1.0

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.
@@ -1,35 +1,23 @@
1
- function missingRevisionError() {
2
- return {
3
- ok: false,
4
- result: buildRevisionError('E_REVISION_MISSING', 'revisesThought is required for revision'),
5
- };
6
- }
7
- function targetNotFoundError(targetNumber) {
8
- return {
9
- ok: false,
10
- result: buildRevisionError('E_REVISION_TARGET_NOT_FOUND', `Thought ${targetNumber} not found`),
11
- };
12
- }
13
- function targetSupersededError(targetNumber) {
14
- return {
15
- ok: false,
16
- result: buildRevisionError('E_REVISION_TARGET_SUPERSEDED', `Thought ${targetNumber} was already superseded`),
17
- };
18
- }
19
1
  export function resolveRevisionTarget(input, getThoughtByNumber) {
20
2
  const targetNumber = input.revisesThought;
21
- if (targetNumber === undefined)
22
- return missingRevisionError();
3
+ if (targetNumber === undefined) {
4
+ return error('E_REVISION_MISSING', 'revisesThought is required for revision');
5
+ }
23
6
  const target = getThoughtByNumber(targetNumber);
24
- if (!target)
25
- return targetNotFoundError(targetNumber);
26
- if (!target.isActive)
27
- return targetSupersededError(targetNumber);
7
+ if (!target) {
8
+ return error('E_REVISION_TARGET_NOT_FOUND', `Thought ${targetNumber} not found`);
9
+ }
10
+ if (!target.isActive) {
11
+ return error('E_REVISION_TARGET_SUPERSEDED', `Thought ${targetNumber} was already superseded`);
12
+ }
28
13
  return { ok: true, targetNumber, target };
29
14
  }
30
- function buildRevisionError(code, message) {
15
+ function error(code, message) {
31
16
  return {
32
17
  ok: false,
33
- error: { code, message },
18
+ result: {
19
+ ok: false,
20
+ error: { code, message },
21
+ },
34
22
  };
35
23
  }
@@ -5,6 +5,7 @@ function selectRecentThoughts(activeThoughts) {
5
5
  const len = activeThoughts.length;
6
6
  const recent = [];
7
7
  const stepIndexes = [];
8
+ // If we have lots of thoughts, keep the first as an anchor.
8
9
  if (len > MAX_RECENT_THOUGHTS) {
9
10
  const anchor = activeThoughts[0];
10
11
  if (!anchor)
@@ -12,21 +13,30 @@ function selectRecentThoughts(activeThoughts) {
12
13
  recent.push(anchor);
13
14
  stepIndexes.push(1);
14
15
  }
15
- const start = len <= MAX_RECENT_THOUGHTS ? 0 : Math.max(0, len - RECENT_TAIL_COUNT);
16
- for (let i = start; i < len; i += 1) {
16
+ const startIndex = len <= MAX_RECENT_THOUGHTS ? 0 : Math.max(0, len - RECENT_TAIL_COUNT);
17
+ for (let i = startIndex; i < len; i += 1) {
17
18
  const thought = activeThoughts[i];
18
- if (thought) {
19
- recent.push(thought);
20
- stepIndexes.push(i + 1);
21
- }
19
+ if (!thought)
20
+ continue;
21
+ recent.push(thought);
22
+ stepIndexes.push(i + 1);
22
23
  }
23
24
  return { recent, stepIndexes };
24
25
  }
25
26
  function truncatePreview(input, maxChars) {
26
- const codepoints = Array.from(input);
27
- if (codepoints.length <= maxChars)
27
+ // Preserve codepoint boundaries (emoji-safe) without depending on Intl.Segmenter.
28
+ const iterator = input[Symbol.iterator]();
29
+ const preview = [];
30
+ for (let i = 0; i < maxChars; i += 1) {
31
+ const next = iterator.next();
32
+ if (next.done)
33
+ return input;
34
+ preview.push(next.value);
35
+ }
36
+ const extra = iterator.next();
37
+ if (extra.done)
28
38
  return input;
29
- return `${codepoints.slice(0, maxChars).join('')}...`;
39
+ return `${preview.join('')}...`;
30
40
  }
31
41
  export function buildContextSummary(activeThoughts, revisionInfo) {
32
42
  const { recent, stepIndexes } = selectRecentThoughts(activeThoughts);
@@ -35,8 +45,5 @@ export function buildContextSummary(activeThoughts, revisionInfo) {
35
45
  number: thought.thoughtNumber,
36
46
  preview: truncatePreview(thought.thought, MAX_PREVIEW_CHARS),
37
47
  }));
38
- if (revisionInfo !== undefined) {
39
- return { recentThoughts, revisionInfo };
40
- }
41
- return { recentThoughts };
48
+ return revisionInfo ? { recentThoughts, revisionInfo } : { recentThoughts };
42
49
  }
@@ -1,13 +1,13 @@
1
1
  import { COMPACT_RATIO, COMPACT_THRESHOLD } from '../engineConfig.js';
2
2
  export class ThoughtStore {
3
- #thoughts = [];
4
- #thoughtIndex = new Map();
3
+ #allThoughts = [];
4
+ #byNumber = new Map();
5
5
  #activeThoughts = [];
6
- #activeThoughtNumbers = [];
6
+ #activeNumbers = [];
7
7
  #activeMaxTotalThoughts = 0;
8
- #headIndex = 0;
8
+ #startIndex = 0;
9
9
  #nextThoughtNumber = 1;
10
- #estimatedBytes = 0;
10
+ #approxBytes = 0;
11
11
  #lastPruneStats = null;
12
12
  #maxThoughts;
13
13
  #maxMemoryBytes;
@@ -27,57 +27,15 @@ export class ThoughtStore {
27
27
  return this.#lastPruneStats;
28
28
  }
29
29
  storeThought(stored) {
30
- this.#thoughts.push(stored);
31
- this.#thoughtIndex.set(stored.thoughtNumber, stored);
30
+ this.#allThoughts.push(stored);
31
+ this.#byNumber.set(stored.thoughtNumber, stored);
32
32
  if (stored.isActive) {
33
- this.#activeThoughts.push(stored);
34
- this.#activeThoughtNumbers.push(stored.thoughtNumber);
35
- this.#activeMaxTotalThoughts = Math.max(this.#activeMaxTotalThoughts, stored.totalThoughts);
33
+ this.#appendActive(stored);
36
34
  }
37
- this.#estimatedBytes += this.#estimateThoughtBytes(stored);
38
- }
39
- #recomputeActiveMaxTotalThoughts() {
40
- let maxTotal = 0;
41
- for (const thought of this.#activeThoughts) {
42
- maxTotal = Math.max(maxTotal, thought.totalThoughts);
43
- }
44
- this.#activeMaxTotalThoughts = maxTotal;
45
- }
46
- #findActiveLowerBound(thoughtNumber) {
47
- const activeThoughtNumbers = this.#activeThoughtNumbers;
48
- let low = 0;
49
- let high = activeThoughtNumbers.length;
50
- while (low < high) {
51
- const mid = (low + high) >>> 1;
52
- const midValue = activeThoughtNumbers[mid];
53
- if (midValue === undefined)
54
- return -1;
55
- if (midValue < thoughtNumber)
56
- low = mid + 1;
57
- else
58
- high = mid;
59
- }
60
- return low;
61
- }
62
- #findActiveThoughtIndex(thoughtNumber) {
63
- const index = this.#findActiveLowerBound(thoughtNumber);
64
- if (index < 0)
65
- return -1;
66
- if (index < this.#activeThoughtNumbers.length &&
67
- this.#activeThoughtNumbers[index] === thoughtNumber)
68
- return index;
69
- return -1;
70
- }
71
- #findFirstActiveIndexAfter(thoughtNumber) {
72
- const index = this.#findActiveLowerBound(thoughtNumber);
73
- if (index < 0)
74
- return 0;
75
- if (this.#activeThoughtNumbers[index] === thoughtNumber)
76
- return index + 1;
77
- return index;
35
+ this.#approxBytes += this.#estimateThoughtBytes(stored);
78
36
  }
79
37
  supersedeFrom(targetNumber, supersededBy, maxSupersedes) {
80
- const startIndex = this.#findActiveThoughtIndex(targetNumber);
38
+ const startIndex = this.#findActiveIndex(targetNumber);
81
39
  if (startIndex < 0)
82
40
  return { supersedes: [], supersedesTotal: 0 };
83
41
  const supersedes = [];
@@ -86,17 +44,15 @@ export class ThoughtStore {
86
44
  const thought = this.#activeThoughts[i];
87
45
  if (!thought)
88
46
  continue;
89
- if (thought.isActive) {
90
- thought.isActive = false;
91
- thought.supersededBy = supersededBy;
92
- }
47
+ this.#markSuperseded(thought, supersededBy);
93
48
  supersedesTotal += 1;
94
49
  if (maxSupersedes === undefined || supersedes.length < maxSupersedes) {
95
50
  supersedes.push(thought.thoughtNumber);
96
51
  }
97
52
  }
53
+ // Drop superseded suffix from active arrays.
98
54
  this.#activeThoughts.length = startIndex;
99
- this.#activeThoughtNumbers.length = startIndex;
55
+ this.#activeNumbers.length = startIndex;
100
56
  this.#recomputeActiveMaxTotalThoughts();
101
57
  return { supersedes, supersedesTotal };
102
58
  }
@@ -104,21 +60,19 @@ export class ThoughtStore {
104
60
  return this.#activeThoughts;
105
61
  }
106
62
  getActiveThoughtNumbers(max) {
107
- if (max === undefined) {
108
- return this.#activeThoughtNumbers.slice();
109
- }
63
+ if (max === undefined)
64
+ return this.#activeNumbers.slice();
110
65
  if (max <= 0)
111
66
  return [];
112
- if (this.#activeThoughtNumbers.length <= max) {
113
- return this.#activeThoughtNumbers.slice();
114
- }
115
- return this.#activeThoughtNumbers.slice(-max);
67
+ if (this.#activeNumbers.length <= max)
68
+ return this.#activeNumbers.slice();
69
+ return this.#activeNumbers.slice(-max);
116
70
  }
117
71
  getThoughtByNumber(thoughtNumber) {
118
- return this.#thoughtIndex.get(thoughtNumber);
72
+ return this.#byNumber.get(thoughtNumber);
119
73
  }
120
74
  getTotalLength() {
121
- return this.#thoughts.length - this.#headIndex;
75
+ return this.#allThoughts.length - this.#startIndex;
122
76
  }
123
77
  pruneHistoryIfNeeded() {
124
78
  const beforeTotal = this.getTotalLength();
@@ -127,62 +81,114 @@ export class ThoughtStore {
127
81
  truncatedActive: false,
128
82
  droppedActiveCount: 0,
129
83
  removedThoughtsCount: 0,
130
- oldestAvailableThoughtNumber: this.#thoughts[this.#headIndex]?.thoughtNumber ?? null,
84
+ oldestAvailableThoughtNumber: this.#allThoughts[this.#startIndex]?.thoughtNumber ?? null,
131
85
  };
132
86
  this.#performPruning();
133
87
  this.#updatePruneStats(beforeTotal, beforeActive);
134
88
  }
89
+ // -------------------------
90
+ // Active-path bookkeeping
91
+ // -------------------------
92
+ #appendActive(stored) {
93
+ this.#activeThoughts.push(stored);
94
+ this.#activeNumbers.push(stored.thoughtNumber);
95
+ this.#activeMaxTotalThoughts = Math.max(this.#activeMaxTotalThoughts, stored.totalThoughts);
96
+ }
97
+ #markSuperseded(thought, supersededBy) {
98
+ if (!thought.isActive)
99
+ return;
100
+ thought.isActive = false;
101
+ thought.supersededBy = supersededBy;
102
+ }
103
+ #recomputeActiveMaxTotalThoughts() {
104
+ let maxTotal = 0;
105
+ for (const thought of this.#activeThoughts) {
106
+ maxTotal = Math.max(maxTotal, thought.totalThoughts);
107
+ }
108
+ this.#activeMaxTotalThoughts = maxTotal;
109
+ }
110
+ // Binary search lower bound for activeNumbers
111
+ #lowerBoundActive(thoughtNumber) {
112
+ let low = 0;
113
+ let high = this.#activeNumbers.length;
114
+ while (low < high) {
115
+ const mid = (low + high) >>> 1;
116
+ const midValue = this.#activeNumbers[mid];
117
+ if (midValue === undefined)
118
+ return -1;
119
+ if (midValue < thoughtNumber)
120
+ low = mid + 1;
121
+ else
122
+ high = mid;
123
+ }
124
+ return low;
125
+ }
126
+ #findActiveIndex(thoughtNumber) {
127
+ const index = this.#lowerBoundActive(thoughtNumber);
128
+ if (index < 0)
129
+ return -1;
130
+ return this.#activeNumbers[index] === thoughtNumber ? index : -1;
131
+ }
132
+ #firstActiveIndexAfter(thoughtNumber) {
133
+ const index = this.#lowerBoundActive(thoughtNumber);
134
+ if (index < 0)
135
+ return 0;
136
+ return this.#activeNumbers[index] === thoughtNumber ? index + 1 : index;
137
+ }
138
+ #dropActiveUpTo(thoughtNumber) {
139
+ if (this.#activeThoughts.length === 0)
140
+ return;
141
+ const startIndex = this.#firstActiveIndexAfter(thoughtNumber);
142
+ if (startIndex === 0)
143
+ return;
144
+ this.#activeThoughts = this.#activeThoughts.slice(startIndex);
145
+ this.#activeNumbers = this.#activeNumbers.slice(startIndex);
146
+ this.#recomputeActiveMaxTotalThoughts();
147
+ }
148
+ // -------------------------
149
+ // Pruning / compaction
150
+ // -------------------------
135
151
  #performPruning() {
136
152
  const excess = this.getTotalLength() - this.#maxThoughts;
137
153
  if (excess > 0) {
138
154
  const batch = Math.max(excess, Math.ceil(this.#maxThoughts * 0.1));
139
155
  this.#removeOldest(batch);
140
156
  }
141
- if (this.#estimatedBytes > this.#maxMemoryBytes &&
157
+ if (this.#approxBytes > this.#maxMemoryBytes &&
142
158
  this.getTotalLength() > 10) {
143
159
  const toRemove = Math.ceil(this.getTotalLength() * 0.2);
144
160
  this.#removeOldest(toRemove, { forceCompact: true });
145
161
  }
146
162
  }
147
163
  #updatePruneStats(beforeTotal, beforeActive) {
148
- if (!this.#lastPruneStats)
164
+ const stats = this.#lastPruneStats;
165
+ if (!stats)
149
166
  return;
150
167
  const afterActive = this.#activeThoughts.length;
151
- const stats = this.#lastPruneStats;
152
168
  stats.removedThoughtsCount = Math.max(0, beforeTotal - this.getTotalLength());
153
169
  stats.droppedActiveCount = Math.max(0, beforeActive - afterActive);
154
170
  stats.truncatedActive = stats.droppedActiveCount > 0;
155
171
  stats.oldestAvailableThoughtNumber =
156
- this.#thoughts[this.#headIndex]?.thoughtNumber ?? null;
172
+ this.#allThoughts[this.#startIndex]?.thoughtNumber ?? null;
157
173
  }
158
174
  #estimateThoughtBytes(thought) {
159
175
  return thought.byteLength + this.#estimatedThoughtOverheadBytes;
160
176
  }
161
- #dropActiveThoughtsUpTo(thoughtNumber) {
162
- if (this.#activeThoughts.length === 0)
163
- return;
164
- const startIndex = this.#findFirstActiveIndexAfter(thoughtNumber);
165
- if (startIndex === 0)
166
- return;
167
- this.#activeThoughts = this.#activeThoughts.slice(startIndex);
168
- this.#activeThoughtNumbers = this.#activeThoughtNumbers.slice(startIndex);
169
- this.#recomputeActiveMaxTotalThoughts();
170
- }
171
177
  #removeOldest(count, options = {}) {
172
178
  const totalLength = this.getTotalLength();
173
179
  if (count <= 0 || totalLength === 0)
174
180
  return;
175
181
  const actual = Math.min(count, totalLength);
176
- const start = this.#headIndex;
182
+ const start = this.#startIndex;
177
183
  const end = start + actual;
178
184
  const removedMaxThoughtNumber = this.#evictRange(start, end);
179
- this.#headIndex = end;
185
+ this.#startIndex = end;
180
186
  if (this.getTotalLength() === 0) {
181
- this.#resetThoughts();
187
+ this.#reset();
182
188
  return;
183
189
  }
184
190
  if (removedMaxThoughtNumber >= 0) {
185
- this.#dropActiveThoughtsUpTo(removedMaxThoughtNumber);
191
+ this.#dropActiveUpTo(removedMaxThoughtNumber);
186
192
  }
187
193
  this.#compactIfNeeded(options.forceCompact);
188
194
  }
@@ -190,42 +196,41 @@ export class ThoughtStore {
190
196
  let removedBytes = 0;
191
197
  let maxThoughtNumber = -1;
192
198
  for (let index = start; index < end; index += 1) {
193
- const thought = this.#thoughts[index];
199
+ const thought = this.#allThoughts[index];
194
200
  if (!thought)
195
201
  continue;
196
202
  removedBytes += this.#estimateThoughtBytes(thought);
197
- this.#thoughtIndex.delete(thought.thoughtNumber);
203
+ this.#byNumber.delete(thought.thoughtNumber);
198
204
  maxThoughtNumber = thought.thoughtNumber;
199
205
  }
200
- this.#estimatedBytes -= removedBytes;
206
+ this.#approxBytes -= removedBytes;
201
207
  return maxThoughtNumber;
202
208
  }
203
209
  #compactIfNeeded(force = false) {
204
- if (this.#headIndex === 0)
210
+ if (this.#startIndex === 0)
205
211
  return;
206
212
  if (this.getTotalLength() === 0) {
207
- this.#resetThoughts();
213
+ this.#reset();
208
214
  return;
209
215
  }
210
- if (!force && !this.#shouldCompact()) {
216
+ if (!force && !this.#shouldCompact())
211
217
  return;
212
- }
213
- this.#thoughts = this.#thoughts.slice(this.#headIndex);
214
- this.#headIndex = 0;
218
+ this.#allThoughts = this.#allThoughts.slice(this.#startIndex);
219
+ this.#startIndex = 0;
215
220
  }
216
221
  #shouldCompact() {
217
- return (this.#headIndex >= COMPACT_THRESHOLD ||
218
- this.#headIndex >= this.#thoughts.length * COMPACT_RATIO);
222
+ return (this.#startIndex >= COMPACT_THRESHOLD ||
223
+ this.#startIndex >= this.#allThoughts.length * COMPACT_RATIO);
219
224
  }
220
- #resetThoughts() {
221
- this.#thoughts = [];
222
- this.#thoughtIndex.clear();
225
+ #reset() {
226
+ this.#allThoughts = [];
227
+ this.#byNumber.clear();
223
228
  this.#activeThoughts = [];
224
- this.#activeThoughtNumbers = [];
229
+ this.#activeNumbers = [];
225
230
  this.#activeMaxTotalThoughts = 0;
226
- this.#headIndex = 0;
231
+ this.#startIndex = 0;
227
232
  this.#nextThoughtNumber = 1;
228
- this.#estimatedBytes = 0;
233
+ this.#approxBytes = 0;
229
234
  this.#lastPruneStats = null;
230
235
  }
231
236
  }
package/dist/engine.js CHANGED
@@ -1,44 +1,90 @@
1
1
  var _a;
2
+ import { Buffer } from 'node:buffer';
2
3
  import { resolveRevisionTarget } from './engine/revision.js';
3
4
  import { buildContextSummary } from './engine/thoughtQueries.js';
4
5
  import { ThoughtStore } from './engine/thoughtStore.js';
5
6
  import { DEFAULT_MAX_THOUGHTS, ESTIMATED_THOUGHT_OVERHEAD_BYTES, MAX_MEMORY_BYTES, MAX_REVISABLE_THOUGHTS, MAX_SUPERSEDES, MAX_THOUGHTS_CAP, normalizeInt, } from './engineConfig.js';
7
+ class PinnedLruSessions {
8
+ #map = new Map();
9
+ #pinnedKey;
10
+ #max;
11
+ constructor(pinnedKey, max) {
12
+ this.#pinnedKey = pinnedKey;
13
+ this.#max = max;
14
+ }
15
+ get size() {
16
+ return this.#map.size;
17
+ }
18
+ get(key) {
19
+ const existing = this.#map.get(key);
20
+ if (!existing)
21
+ return undefined;
22
+ // bump to most-recent
23
+ this.#map.delete(key);
24
+ this.#map.set(key, existing);
25
+ return existing;
26
+ }
27
+ set(key, value) {
28
+ if (this.#map.has(key))
29
+ this.#map.delete(key);
30
+ this.#map.set(key, value);
31
+ this.#evictIfNeeded();
32
+ }
33
+ #evictIfNeeded() {
34
+ while (this.#map.size > this.#max) {
35
+ const oldestKey = this.#map.keys().next().value;
36
+ if (!oldestKey)
37
+ return;
38
+ if (oldestKey === this.#pinnedKey) {
39
+ const pinned = this.#map.get(this.#pinnedKey);
40
+ if (!pinned) {
41
+ // Invariant broken; safest is to stop evicting.
42
+ return;
43
+ }
44
+ // Move pinned to most-recent and try again.
45
+ this.#map.delete(this.#pinnedKey);
46
+ this.#map.set(this.#pinnedKey, pinned);
47
+ continue;
48
+ }
49
+ this.#map.delete(oldestKey);
50
+ }
51
+ }
52
+ }
53
+ function normalizeSessionId(sessionId) {
54
+ const key = sessionId.trim();
55
+ return key.length > 0 ? key : 'default';
56
+ }
6
57
  export class ThinkingEngine {
7
58
  static DEFAULT_TOTAL_THOUGHTS = 3;
8
59
  #maxThoughts;
9
60
  #maxMemoryBytes;
10
61
  #estimatedThoughtOverheadBytes;
11
- #maxSessions;
12
- #sessions = new Map();
62
+ #sessions;
13
63
  constructor(options = {}) {
14
64
  this.#maxThoughts = normalizeInt(options.maxThoughts, DEFAULT_MAX_THOUGHTS, { min: 1, max: MAX_THOUGHTS_CAP });
15
65
  this.#maxMemoryBytes = normalizeInt(options.maxMemoryBytes, MAX_MEMORY_BYTES, { min: 1, max: Number.MAX_SAFE_INTEGER });
16
66
  this.#estimatedThoughtOverheadBytes = normalizeInt(options.estimatedThoughtOverheadBytes, ESTIMATED_THOUGHT_OVERHEAD_BYTES, { min: 1, max: Number.MAX_SAFE_INTEGER });
17
- this.#maxSessions = normalizeInt(options.maxSessions, 50, {
67
+ const maxSessions = normalizeInt(options.maxSessions, 50, {
18
68
  min: 1,
19
69
  max: 10_000,
20
70
  });
21
- this.#getSession('default');
71
+ this.#sessions = new PinnedLruSessions('default', maxSessions);
72
+ this.#getOrCreateSession('default');
22
73
  }
23
74
  processThought(input) {
24
75
  return this.processThoughtWithSession('default', input);
25
76
  }
26
77
  processThoughtWithSession(sessionId, input) {
27
- const session = this.#getSession(sessionId);
28
- if (input.revisesThought !== undefined) {
29
- return this.#processRevision(session, input);
30
- }
31
- return this.#processNewThought(session, input);
78
+ const session = this.#getOrCreateSession(sessionId);
79
+ return input.revisesThought !== undefined
80
+ ? this.#processRevision(session, input)
81
+ : this.#processNewThought(session, input);
32
82
  }
33
- #getSession(sessionId) {
34
- const key = sessionId.trim() || 'default';
83
+ #getOrCreateSession(sessionId) {
84
+ const key = normalizeSessionId(sessionId);
35
85
  const existing = this.#sessions.get(key);
36
- if (existing) {
37
- // bump to most-recent for LRU eviction
38
- this.#sessions.delete(key);
39
- this.#sessions.set(key, existing);
86
+ if (existing)
40
87
  return existing;
41
- }
42
88
  const state = {
43
89
  store: new ThoughtStore({
44
90
  maxThoughts: this.#maxThoughts,
@@ -48,28 +94,11 @@ export class ThinkingEngine {
48
94
  hasRevisions: false,
49
95
  };
50
96
  this.#sessions.set(key, state);
51
- this.#evictSessionsIfNeeded();
52
97
  return state;
53
98
  }
54
- #evictSessionsIfNeeded() {
55
- while (this.#sessions.size > this.#maxSessions) {
56
- const oldestKey = this.#sessions.keys().next().value;
57
- if (!oldestKey)
58
- return;
59
- if (oldestKey === 'default') {
60
- const def = this.#sessions.get('default');
61
- if (!def)
62
- return;
63
- this.#sessions.delete('default');
64
- this.#sessions.set('default', def);
65
- continue;
66
- }
67
- this.#sessions.delete(oldestKey);
68
- }
69
- }
70
99
  #processNewThought(session, input) {
71
- const { stored } = this.#createStoredThought(session.store, input);
72
- this.#commitThought(session.store, stored);
100
+ const stored = this.#createStoredThought(session.store, input);
101
+ this.#commit(session.store, stored);
73
102
  return this.#buildProcessResult(session, stored);
74
103
  }
75
104
  #processRevision(session, input) {
@@ -78,19 +107,31 @@ export class ThinkingEngine {
78
107
  if (!resolved.ok)
79
108
  return resolved.result;
80
109
  const { targetNumber } = resolved;
81
- const { numbers, stored } = this.#createStoredThought(store, input, {
110
+ const stored = this.#createStoredThought(store, input, {
82
111
  revisionOf: targetNumber,
83
112
  fallbackTotalThoughts: resolved.target.totalThoughts,
84
113
  });
85
- const { supersedes, supersedesTotal } = store.supersedeFrom(targetNumber, numbers.thoughtNumber, MAX_SUPERSEDES);
114
+ const { supersedes, supersedesTotal } = store.supersedeFrom(targetNumber, stored.thoughtNumber, MAX_SUPERSEDES);
86
115
  session.hasRevisions = true;
87
- this.#commitThought(store, stored);
116
+ this.#commit(store, stored);
88
117
  return this.#buildProcessResult(session, stored, {
89
118
  revises: targetNumber,
90
119
  supersedes,
91
120
  supersedesTotal,
92
121
  });
93
122
  }
123
+ #commit(store, stored) {
124
+ store.storeThought(stored);
125
+ store.pruneHistoryIfNeeded();
126
+ }
127
+ #createStoredThought(store, input, extras = {}) {
128
+ const totalThoughts = this.#resolveEffectiveTotalThoughts(store, input, extras.fallbackTotalThoughts);
129
+ const numbers = store.nextThoughtNumbers(totalThoughts);
130
+ return this.#buildStoredThought(input, {
131
+ ...numbers,
132
+ ...(extras.revisionOf !== undefined && { revisionOf: extras.revisionOf }),
133
+ });
134
+ }
94
135
  #buildStoredThought(input, details) {
95
136
  return {
96
137
  thought: input.thought,
@@ -107,19 +148,6 @@ export class ThinkingEngine {
107
148
  byteLength: Buffer.byteLength(input.thought),
108
149
  };
109
150
  }
110
- #createStoredThought(store, input, extras = {}) {
111
- const effectiveTotalThoughts = this.#resolveEffectiveTotalThoughts(store, input, extras.fallbackTotalThoughts);
112
- const numbers = store.nextThoughtNumbers(effectiveTotalThoughts);
113
- const stored = this.#buildStoredThought(input, {
114
- ...numbers,
115
- ...(extras.revisionOf !== undefined && { revisionOf: extras.revisionOf }),
116
- });
117
- return { stored, numbers };
118
- }
119
- #commitThought(store, stored) {
120
- store.storeThought(stored);
121
- store.pruneHistoryIfNeeded();
122
- }
123
151
  #resolveEffectiveTotalThoughts(store, input, fallback) {
124
152
  if (input.totalThoughts !== undefined) {
125
153
  return normalizeInt(input.totalThoughts, _a.DEFAULT_TOTAL_THOUGHTS, { min: 1, max: MAX_THOUGHTS_CAP });
@@ -128,15 +156,12 @@ export class ThinkingEngine {
128
156
  return fallback;
129
157
  const activeThoughts = store.getActiveThoughts();
130
158
  const lastActive = activeThoughts[activeThoughts.length - 1];
131
- if (lastActive !== undefined) {
132
- return lastActive.totalThoughts;
133
- }
134
- return _a.DEFAULT_TOTAL_THOUGHTS;
159
+ return lastActive?.totalThoughts ?? _a.DEFAULT_TOTAL_THOUGHTS;
135
160
  }
136
161
  #buildProcessResult(session, stored, revisionInfo) {
137
162
  const activeThoughts = session.store.getActiveThoughts();
138
163
  const activePathLength = activeThoughts.length;
139
- const { progress, isComplete } = this.#calculateMetrics(activePathLength, stored.totalThoughts);
164
+ const { progress, isComplete } = this.#calculateProgress(activePathLength, stored.totalThoughts);
140
165
  return {
141
166
  ok: true,
142
167
  result: {
@@ -153,11 +178,10 @@ export class ThinkingEngine {
153
178
  },
154
179
  };
155
180
  }
156
- #calculateMetrics(active, total) {
157
- return {
158
- progress: total > 0 ? Math.min(1, active / total) : 1,
159
- isComplete: active >= total,
160
- };
181
+ #calculateProgress(active, total) {
182
+ const safeTotal = total > 0 ? total : 0;
183
+ const progress = safeTotal > 0 ? Math.min(1, active / safeTotal) : 1;
184
+ return { progress, isComplete: active >= safeTotal };
161
185
  }
162
186
  }
163
187
  _a = ThinkingEngine;
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import process from 'node:process';
2
3
  import { installProcessErrorHandlers, run } from './app.js';
3
4
  import { ThinkingEngine } from './engine.js';
4
5
  import { getCliHelpText, parseCliConfig } from './lib/cli.js';