thatgfsj-code 2.2.9 → 3.0.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 (190) hide show
  1. package/CHANGELOG.md +15 -67
  2. package/dist/app/index.d.ts +28 -5
  3. package/dist/app/index.d.ts.map +1 -1
  4. package/dist/app/index.js +76 -10
  5. package/dist/app/index.js.map +1 -1
  6. package/dist/cache/fingerprint.d.ts +49 -0
  7. package/dist/cache/fingerprint.d.ts.map +1 -0
  8. package/dist/cache/fingerprint.js +100 -0
  9. package/dist/cache/fingerprint.js.map +1 -0
  10. package/dist/cache/index.d.ts +15 -0
  11. package/dist/cache/index.d.ts.map +1 -0
  12. package/dist/cache/index.js +15 -0
  13. package/dist/cache/index.js.map +1 -0
  14. package/dist/cache/smartModel.d.ts +40 -0
  15. package/dist/cache/smartModel.d.ts.map +1 -0
  16. package/dist/cache/smartModel.js +43 -0
  17. package/dist/cache/smartModel.js.map +1 -0
  18. package/dist/cache/stats.d.ts +93 -0
  19. package/dist/cache/stats.d.ts.map +1 -0
  20. package/dist/cache/stats.js +155 -0
  21. package/dist/cache/stats.js.map +1 -0
  22. package/dist/cache/volatile.d.ts +32 -0
  23. package/dist/cache/volatile.d.ts.map +1 -0
  24. package/dist/cache/volatile.js +44 -0
  25. package/dist/cache/volatile.js.map +1 -0
  26. package/dist/cmd/index.js +49 -57
  27. package/dist/cmd/index.js.map +1 -1
  28. package/dist/config/index.d.ts.map +1 -1
  29. package/dist/config/index.js +8 -0
  30. package/dist/config/index.js.map +1 -1
  31. package/dist/config/types.d.ts +14 -0
  32. package/dist/config/types.d.ts.map +1 -1
  33. package/dist/llm/anthropic.d.ts +55 -5
  34. package/dist/llm/anthropic.d.ts.map +1 -1
  35. package/dist/llm/anthropic.js +172 -26
  36. package/dist/llm/anthropic.js.map +1 -1
  37. package/dist/llm/index.d.ts +26 -8
  38. package/dist/llm/index.d.ts.map +1 -1
  39. package/dist/llm/index.js +83 -15
  40. package/dist/llm/index.js.map +1 -1
  41. package/dist/llm/openai.d.ts +28 -8
  42. package/dist/llm/openai.d.ts.map +1 -1
  43. package/dist/llm/openai.js +58 -8
  44. package/dist/llm/openai.js.map +1 -1
  45. package/dist/llm/provider.d.ts +40 -8
  46. package/dist/llm/provider.d.ts.map +1 -1
  47. package/dist/llm/provider.js +8 -1
  48. package/dist/llm/provider.js.map +1 -1
  49. package/dist/prompts/index.d.ts +32 -0
  50. package/dist/prompts/index.d.ts.map +1 -1
  51. package/dist/prompts/index.js +33 -11
  52. package/dist/prompts/index.js.map +1 -1
  53. package/dist/session/index.d.ts +32 -3
  54. package/dist/session/index.d.ts.map +1 -1
  55. package/dist/session/index.js +51 -36
  56. package/dist/session/index.js.map +1 -1
  57. package/dist/skills/brainstorming.js +33 -33
  58. package/dist/skills/code-review.js +35 -35
  59. package/dist/skills/executing-plans.js +18 -18
  60. package/dist/skills/frontend-design.js +25 -25
  61. package/dist/skills/git-workflow.js +26 -26
  62. package/dist/skills/improve-architecture.js +28 -28
  63. package/dist/skills/neuroweave.js +37 -37
  64. package/dist/skills/playwright.js +62 -62
  65. package/dist/skills/prototype.js +20 -20
  66. package/dist/skills/subagent.js +19 -19
  67. package/dist/skills/supabase.js +34 -34
  68. package/dist/skills/systematic-debugging.js +34 -34
  69. package/dist/skills/tdd.js +29 -29
  70. package/dist/skills/triage.js +25 -25
  71. package/dist/skills/verification.js +22 -22
  72. package/dist/skills/writing-plans.js +32 -32
  73. package/dist/tools/nwt.js +16 -16
  74. package/dist/tui/app.d.ts.map +1 -1
  75. package/dist/tui/app.js +9 -2
  76. package/dist/tui/app.js.map +1 -1
  77. package/dist/tui/components/Header.d.ts +8 -0
  78. package/dist/tui/components/Header.d.ts.map +1 -1
  79. package/dist/tui/components/Header.js +17 -4
  80. package/dist/tui/components/Header.js.map +1 -1
  81. package/dist/tui/components/InitWizard.d.ts +12 -1
  82. package/dist/tui/components/InitWizard.d.ts.map +1 -1
  83. package/dist/tui/components/InitWizard.js +58 -3
  84. package/dist/tui/components/InitWizard.js.map +1 -1
  85. package/dist/tui/components/ModelSelector.d.ts +3 -1
  86. package/dist/tui/components/ModelSelector.d.ts.map +1 -1
  87. package/dist/tui/components/ModelSelector.js +24 -7
  88. package/dist/tui/components/ModelSelector.js.map +1 -1
  89. package/dist/tui/components/ToolCall.d.ts.map +1 -1
  90. package/dist/tui/components/ToolCall.js +1 -6
  91. package/dist/tui/components/ToolCall.js.map +1 -1
  92. package/dist/tui/components/UserInput.d.ts.map +1 -1
  93. package/dist/tui/components/UserInput.js +20 -0
  94. package/dist/tui/components/UserInput.js.map +1 -1
  95. package/dist/tui/hooks/useChat.d.ts +22 -0
  96. package/dist/tui/hooks/useChat.d.ts.map +1 -1
  97. package/dist/tui/hooks/useChat.js +82 -46
  98. package/dist/tui/hooks/useChat.js.map +1 -1
  99. package/dist/tui/hooks/useCommands.d.ts.map +1 -1
  100. package/dist/tui/hooks/useCommands.js +57 -0
  101. package/dist/tui/hooks/useCommands.js.map +1 -1
  102. package/dist/tui/welcome.d.ts.map +1 -1
  103. package/dist/tui/welcome.js +2 -3
  104. package/dist/tui/welcome.js.map +1 -1
  105. package/dist/types.d.ts +79 -6
  106. package/dist/types.d.ts.map +1 -1
  107. package/dist/types.js +7 -0
  108. package/dist/types.js.map +1 -1
  109. package/dist/utils/stableStringify.d.ts +21 -0
  110. package/dist/utils/stableStringify.d.ts.map +1 -0
  111. package/dist/utils/stableStringify.js +53 -0
  112. package/dist/utils/stableStringify.js.map +1 -0
  113. package/dist/utils/thinking.d.ts.map +1 -1
  114. package/dist/utils/thinking.js +0 -3
  115. package/dist/utils/thinking.js.map +1 -1
  116. package/package.json +7 -6
  117. package/src/app/index.ts +189 -124
  118. package/src/cache/fingerprint.ts +101 -0
  119. package/src/cache/index.ts +15 -0
  120. package/src/cache/smartModel.ts +52 -0
  121. package/src/cache/stats.ts +199 -0
  122. package/src/cache/volatile.ts +47 -0
  123. package/src/cmd/index.tsx +288 -292
  124. package/src/config/index.ts +156 -148
  125. package/src/config/providers.ts +234 -234
  126. package/src/config/types.ts +67 -53
  127. package/src/hooks/index.ts +111 -111
  128. package/src/llm/anthropic.ts +388 -243
  129. package/src/llm/gemini.ts +169 -169
  130. package/src/llm/index.ts +265 -200
  131. package/src/llm/openai.ts +243 -196
  132. package/src/llm/provider.ts +66 -34
  133. package/src/prompts/index.ts +260 -220
  134. package/src/session/compactor.ts +103 -103
  135. package/src/session/index.ts +181 -168
  136. package/src/session/message.ts +42 -42
  137. package/src/skills/brainstorming.ts +43 -43
  138. package/src/skills/code-review.ts +45 -45
  139. package/src/skills/executing-plans.ts +27 -27
  140. package/src/skills/frontend-design.ts +35 -35
  141. package/src/skills/git-workflow.ts +36 -36
  142. package/src/skills/improve-architecture.ts +38 -38
  143. package/src/skills/index.ts +136 -136
  144. package/src/skills/neuroweave.ts +47 -47
  145. package/src/skills/playwright.ts +72 -72
  146. package/src/skills/prototype.ts +30 -30
  147. package/src/skills/subagent.ts +28 -28
  148. package/src/skills/supabase.ts +44 -44
  149. package/src/skills/systematic-debugging.ts +44 -44
  150. package/src/skills/tdd.ts +39 -39
  151. package/src/skills/triage.ts +35 -35
  152. package/src/skills/verification.ts +31 -31
  153. package/src/skills/writing-plans.ts +42 -42
  154. package/src/tools/nwt.ts +598 -598
  155. package/src/tools/types.ts +122 -122
  156. package/src/tui/app.tsx +200 -186
  157. package/src/tui/components/ChatList.tsx +41 -41
  158. package/src/tui/components/ChatMessage.tsx +54 -54
  159. package/src/tui/components/Header.tsx +56 -29
  160. package/src/tui/components/InitWizard.tsx +217 -132
  161. package/src/tui/components/Markdown.tsx +35 -35
  162. package/src/tui/components/ModelSelector.tsx +107 -87
  163. package/src/tui/components/StatusBar.tsx +30 -30
  164. package/src/tui/components/Thinking.tsx +17 -17
  165. package/src/tui/components/ToolCall.tsx +102 -107
  166. package/src/tui/components/UserInput.tsx +151 -131
  167. package/src/tui/hooks/useChat.ts +287 -238
  168. package/src/tui/hooks/useCommands.ts +234 -176
  169. package/src/tui/index.ts +6 -6
  170. package/src/tui/welcome.ts +177 -178
  171. package/src/types.ts +104 -42
  172. package/src/utils/diff.ts +71 -71
  173. package/src/utils/project.ts +99 -99
  174. package/src/utils/stableStringify.ts +47 -0
  175. package/src/utils/thinking.ts +118 -121
  176. package/tests/cache/fingerprint.test.ts +75 -0
  177. package/tests/cache/providerCatalog.test.ts +31 -0
  178. package/tests/cache/stableStringify.test.ts +43 -0
  179. package/tests/cache/stats.test.ts +146 -0
  180. package/tests/cache/volatile.test.ts +75 -0
  181. package/tests/smoke-pollution.mjs +78 -0
  182. package/tests/smoke-thinking.mjs +110 -0
  183. package/tests/smoke-tool-stream.mjs +69 -0
  184. package/tests/smoke-tool.mjs +117 -0
  185. package/.nwt/meta.json +0 -5
  186. package/dist/version.d.ts +0 -16
  187. package/dist/version.d.ts.map +0 -1
  188. package/dist/version.js +0 -16
  189. package/dist/version.js.map +0 -1
  190. package/src/version.ts +0 -16
@@ -1,103 +1,103 @@
1
- /**
2
- * Context Compactor - Progressive compression for conversation history
3
- * Migrated from old src/core/context-compactor.ts
4
- *
5
- * Strategy:
6
- * 1. Within limit: no compression
7
- * 2. Beyond limit: keep system + recent N turns + summarize middle
8
- */
9
-
10
- import type { ChatMessage } from '../types.js';
11
-
12
- export interface CompactorConfig {
13
- /** Max messages before compression kicks in */
14
- maxMessages?: number;
15
- /** How many recent turns to always preserve */
16
- preserveRecent?: number;
17
- }
18
-
19
- export interface CompressionResult {
20
- originalCount: number;
21
- compactedCount: number;
22
- removedCount: number;
23
- }
24
-
25
- export class ContextCompactor {
26
- private maxMessages: number;
27
- private preserveRecent: number;
28
-
29
- constructor(config: CompactorConfig = {}) {
30
- this.maxMessages = config.maxMessages ?? 50;
31
- this.preserveRecent = config.preserveRecent ?? 10;
32
- }
33
-
34
- /**
35
- * Compact messages if needed
36
- */
37
- compact(messages: ChatMessage[]): { compacted: ChatMessage[]; result: CompressionResult } {
38
- if (messages.length <= this.maxMessages) {
39
- return {
40
- compacted: messages,
41
- result: { originalCount: messages.length, compactedCount: messages.length, removedCount: 0 },
42
- };
43
- }
44
-
45
- const systemMsg = messages.find(m => m.role === 'system');
46
- const others = messages.filter(m => m.role !== 'system');
47
-
48
- if (others.length <= this.preserveRecent) {
49
- return {
50
- compacted: messages,
51
- result: { originalCount: messages.length, compactedCount: messages.length, removedCount: 0 },
52
- };
53
- }
54
-
55
- // Keep recent N turns
56
- const recent = others.slice(-this.preserveRecent);
57
-
58
- // Summarize the middle portion
59
- const middle = others.slice(0, -this.preserveRecent);
60
- const summary: ChatMessage = {
61
- role: 'system',
62
- content: `[Earlier conversation summary: ${middle.length} messages covering ${this.extractTopics(middle)}]`,
63
- };
64
-
65
- const compacted = systemMsg
66
- ? [systemMsg, summary, ...recent]
67
- : [summary, ...recent];
68
-
69
- return {
70
- compacted,
71
- result: {
72
- originalCount: messages.length,
73
- compactedCount: compacted.length,
74
- removedCount: messages.length - compacted.length,
75
- },
76
- };
77
- }
78
-
79
- /**
80
- * Auto-compact if messages exceed limit
81
- */
82
- autoCompact(messages: ChatMessage[]): { compacted: ChatMessage[]; result: CompressionResult } {
83
- return this.compact(messages);
84
- }
85
-
86
- /**
87
- * Estimate token count (rough: ~4 chars per token)
88
- */
89
- estimateTokens(messages: ChatMessage[]): number {
90
- return messages.reduce((sum, m) => sum + Math.ceil(m.content.length / 4) + 10, 0);
91
- }
92
-
93
- /**
94
- * Extract topic summary from messages
95
- */
96
- private extractTopics(msgs: ChatMessage[]): string {
97
- const topics = msgs
98
- .filter(m => m.role === 'user')
99
- .map(m => m.content.slice(0, 30))
100
- .slice(0, 3);
101
- return topics.join(', ') || 'various topics';
102
- }
103
- }
1
+ /**
2
+ * Context Compactor - Progressive compression for conversation history
3
+ * Migrated from old src/core/context-compactor.ts
4
+ *
5
+ * Strategy:
6
+ * 1. Within limit: no compression
7
+ * 2. Beyond limit: keep system + recent N turns + summarize middle
8
+ */
9
+
10
+ import type { ChatMessage } from '../types.js';
11
+
12
+ export interface CompactorConfig {
13
+ /** Max messages before compression kicks in */
14
+ maxMessages?: number;
15
+ /** How many recent turns to always preserve */
16
+ preserveRecent?: number;
17
+ }
18
+
19
+ export interface CompressionResult {
20
+ originalCount: number;
21
+ compactedCount: number;
22
+ removedCount: number;
23
+ }
24
+
25
+ export class ContextCompactor {
26
+ private maxMessages: number;
27
+ private preserveRecent: number;
28
+
29
+ constructor(config: CompactorConfig = {}) {
30
+ this.maxMessages = config.maxMessages ?? 50;
31
+ this.preserveRecent = config.preserveRecent ?? 10;
32
+ }
33
+
34
+ /**
35
+ * Compact messages if needed
36
+ */
37
+ compact(messages: ChatMessage[]): { compacted: ChatMessage[]; result: CompressionResult } {
38
+ if (messages.length <= this.maxMessages) {
39
+ return {
40
+ compacted: messages,
41
+ result: { originalCount: messages.length, compactedCount: messages.length, removedCount: 0 },
42
+ };
43
+ }
44
+
45
+ const systemMsg = messages.find(m => m.role === 'system');
46
+ const others = messages.filter(m => m.role !== 'system');
47
+
48
+ if (others.length <= this.preserveRecent) {
49
+ return {
50
+ compacted: messages,
51
+ result: { originalCount: messages.length, compactedCount: messages.length, removedCount: 0 },
52
+ };
53
+ }
54
+
55
+ // Keep recent N turns
56
+ const recent = others.slice(-this.preserveRecent);
57
+
58
+ // Summarize the middle portion
59
+ const middle = others.slice(0, -this.preserveRecent);
60
+ const summary: ChatMessage = {
61
+ role: 'system',
62
+ content: `[Earlier conversation summary: ${middle.length} messages covering ${this.extractTopics(middle)}]`,
63
+ };
64
+
65
+ const compacted = systemMsg
66
+ ? [systemMsg, summary, ...recent]
67
+ : [summary, ...recent];
68
+
69
+ return {
70
+ compacted,
71
+ result: {
72
+ originalCount: messages.length,
73
+ compactedCount: compacted.length,
74
+ removedCount: messages.length - compacted.length,
75
+ },
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Auto-compact if messages exceed limit
81
+ */
82
+ autoCompact(messages: ChatMessage[]): { compacted: ChatMessage[]; result: CompressionResult } {
83
+ return this.compact(messages);
84
+ }
85
+
86
+ /**
87
+ * Estimate token count (rough: ~4 chars per token)
88
+ */
89
+ estimateTokens(messages: ChatMessage[]): number {
90
+ return messages.reduce((sum, m) => sum + Math.ceil(m.content.length / 4) + 10, 0);
91
+ }
92
+
93
+ /**
94
+ * Extract topic summary from messages
95
+ */
96
+ private extractTopics(msgs: ChatMessage[]): string {
97
+ const topics = msgs
98
+ .filter(m => m.role === 'user')
99
+ .map(m => m.content.slice(0, 30))
100
+ .slice(0, 3);
101
+ return topics.join(', ') || 'various topics';
102
+ }
103
+ }
@@ -1,168 +1,181 @@
1
- /**
2
- * Session Manager - Manages conversation history with auto-compaction
3
- */
4
-
5
- import type { ChatMessage } from '../types.js';
6
- import { ContextCompactor } from './compactor.js';
7
-
8
- /**
9
- * v2.2.4 (port from v2.1.0): patterns that, if found in an assistant
10
- * message, indicate the message is a truncated/aborted response that
11
- * should NOT be persisted into history. Without this filter, the next
12
- * turn's LLM sees the marker and starts echoing it back, creating a
13
- * self-reinforcing "[已中断]" hallucination loop.
14
- *
15
- * v2.2.6.1 tightening (smoke-test driven): the design here is a
16
- * two-tier check:
17
- * 1. STRONG markers — unambiguous pollution, always drop:
18
- * - `[已中断]` bracketed (the model emits this exact form)
19
- * - `[interrupted]` bracketed English equivalent
20
- * 2. WEAK markers — only drop if the message is also short (<200
21
- * chars). Long messages mentioning "response truncated" are
22
- * legitimate conversation (e.g. user complaints about past
23
- * behavior), not pollution.
24
- *
25
- * Earlier drafts matched bare `已中断` substrings and "response
26
- * truncated" anywhere in the message — both were too greedy and
27
- * dropped legitimate Chinese/English conversation.
28
- */
29
- const POLLUTION_STRONG: RegExp[] = [
30
- /\[已中断\]/,
31
- /\[interrupted\]/i,
32
- ];
33
-
34
- const POLLUTION_WEAK: RegExp[] = [
35
- /^\s*[\*#>\-`]*\s*\[已中断\]/m,
36
- /^\s*[\*#>\-`]*\s*\[interrupted\]/im,
37
- /\bresponse (was )?(truncated|cut off|interrupted)\b/i,
38
- /\boutput (was )?(truncated|cut off|interrupted)\b/i,
39
- ];
40
-
41
- function looksPolluted(content: string): boolean {
42
- if (!content) return false;
43
- // Common gates (apply to both tiers):
44
- // - Length: model got cut off mid-stream → pollution is short.
45
- // A long message with [已中断] in the middle is the model
46
- // legitimately referencing the marker, not pollution.
47
- // - Question: ends with '?' user complaining about past behavior.
48
- // - Temporal: contains "last time" / "earlier" / etc. → past tense.
49
- if (content.length >= 200) return false;
50
- if (/\?\s*$/.test(content.trim())) return false;
51
- if (/\b(last time|earlier|before|previously|yesterday)\b/i.test(content)) return false;
52
- // STRONG markers — bracketed truncation markers, unambiguous.
53
- if (POLLUTION_STRONG.some(p => p.test(content))) return true;
54
- // WEAK markers bare "response truncated" phrases.
55
- if (POLLUTION_WEAK.some(p => p.test(content))) return true;
56
- return false;
57
- }
58
-
59
- export class SessionManager {
60
- private messages: ChatMessage[] = [];
61
- private sessionId: string;
62
- private createdAt: Date;
63
- private compactor: ContextCompactor;
64
- private maxMessages: number;
65
- /** v2.2.4: counter for messages dropped by the pollution filter. */
66
- private droppedCount: number = 0;
67
-
68
- constructor(maxMessages = 50) {
69
- this.maxMessages = maxMessages;
70
- this.sessionId = `session_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
71
- this.createdAt = new Date();
72
- this.compactor = new ContextCompactor({ maxMessages });
73
- }
74
-
75
- addMessage(role: ChatMessage['role'], content: string, extras?: Partial<ChatMessage>): void {
76
- this.messages.push({ role, content, ...extras });
77
- this.autoCompact();
78
- }
79
-
80
- /**
81
- * v2.2.4 (port from v2.1.0): same as addMessage but returns false
82
- * (and skips the push) if the message content matches a known
83
- * truncation/abort pollution pattern. Use this for assistant
84
- * messages whose stream might have been aborted.
85
- */
86
- addMessageSafe(role: ChatMessage['role'], content: string, extras?: Partial<ChatMessage>): boolean {
87
- if (role === 'assistant' && looksPolluted(content)) {
88
- this.droppedCount++;
89
- return false;
90
- }
91
- this.addMessage(role, content, extras);
92
- return true;
93
- }
94
-
95
- /** v2.2.4: total messages dropped by addMessageSafe since session start. */
96
- getDroppedCount(): number {
97
- return this.droppedCount;
98
- }
99
-
100
- getMessages(): ChatMessage[] {
101
- return [...this.messages];
102
- }
103
-
104
- getMessageCount(): number {
105
- return this.messages.length;
106
- }
107
-
108
- clear(): void {
109
- this.messages = [];
110
- }
111
-
112
- /**
113
- * Auto-compact when messages exceed max.
114
- * Keeps system messages + recent half. One summary replaces the rest.
115
- */
116
- private autoCompact(): void {
117
- if (this.messages.length <= this.maxMessages) return;
118
-
119
- const systemMsgs = this.messages.filter(m => m.role === 'system');
120
- const others = this.messages.filter(m => m.role !== 'system');
121
- const keepCount = Math.floor(this.maxMessages * 0.5);
122
- const recent = others.slice(-keepCount);
123
- const removed = others.length - keepCount;
124
-
125
- if (removed <= 0) return;
126
-
127
- // Remove old summaries before adding new one
128
- const cleanSystem = systemMsgs.filter(m => !m.content.startsWith('[Earlier conversation'));
129
- const summary: ChatMessage = {
130
- role: 'system',
131
- content: `[Earlier conversation: ${removed} messages compacted to save context]`,
132
- };
133
- this.messages = [...cleanSystem, summary, ...recent];
134
- }
135
-
136
- truncate(maxMessages?: number): void {
137
- if (maxMessages) {
138
- // v2.2.7 edge fix: preserveRecent must be <= maxMessages - 1,
139
- // otherwise the compactor's "always keep recent N" branch
140
- // overrides the trim and no messages actually get dropped.
141
- // Set preserveRecent to maxMessages-1 so anything beyond the
142
- // last maxMessages-1 user/assistant msgs gets summarized.
143
- this.maxMessages = maxMessages;
144
- this.compactor = new ContextCompactor({
145
- maxMessages,
146
- preserveRecent: Math.max(1, maxMessages - 1),
147
- });
148
- }
149
- const { compacted } = this.compactor.compact(this.messages);
150
- this.messages = compacted;
151
- }
152
-
153
- getId(): string {
154
- return this.sessionId;
155
- }
156
-
157
- getInfo(): { id: string; messageCount: number; createdAt: Date } {
158
- return {
159
- id: this.sessionId,
160
- messageCount: this.messages.length,
161
- createdAt: this.createdAt,
162
- };
163
- }
164
- }
165
-
166
- export { ContextCompactor } from './compactor.js';
167
- export type { CompactorConfig, CompressionResult } from './compactor.js';
168
- export * from './message.js';
1
+ /**
2
+ * Session Manager - Manages conversation history with auto-compaction
3
+ */
4
+
5
+ import type { ChatMessage } from '../types.js';
6
+ import { ContextCompactor } from './compactor.js';
7
+
8
+ /**
9
+ * v2.2.4 (port from v2.1.0): patterns that, if found in an assistant
10
+ * message, indicate the message is a truncated/aborted response that
11
+ * should NOT be persisted into history. Without this filter, the next
12
+ * turn's LLM sees the marker and starts echoing it back, creating a
13
+ * self-reinforcing "[已中断]" hallucination loop.
14
+ *
15
+ * v2.2.6.1 tightening (smoke-test driven): the design here is a
16
+ * two-tier check:
17
+ * 1. STRONG markers — unambiguous pollution, always drop:
18
+ * - `[已中断]` bracketed (the model emits this exact form)
19
+ * - `[interrupted]` bracketed English equivalent
20
+ * 2. WEAK markers — only drop if the message is also short (<200
21
+ * chars). Long messages mentioning "response truncated" are
22
+ * legitimate conversation (e.g. user complaints about past
23
+ * behavior), not pollution.
24
+ *
25
+ * Earlier drafts matched bare `已中断` substrings and "response
26
+ * truncated" anywhere in the message — both were too greedy and
27
+ * dropped legitimate Chinese/English conversation.
28
+ */
29
+ const POLLUTION_STRONG: RegExp[] = [
30
+ /\[已中断\]/,
31
+ /\[interrupted\]/i,
32
+ ];
33
+
34
+ const POLLUTION_WEAK: RegExp[] = [
35
+ /^\s*[\*#>\-`]*\s*\[已中断\]/m,
36
+ /^\s*[\*#>\-`]*\s*\[interrupted\]/im,
37
+ /\bresponse (was )?(truncated|cut off|interrupted)\b/i,
38
+ /\boutput (was )?(truncated|cut off|interrupted)\b/i,
39
+ ];
40
+
41
+ function looksPolluted(content: string): boolean {
42
+ if (!content) return false;
43
+ if (POLLUTION_STRONG.some(p => p.test(content))) return true;
44
+ // WEAK pattern gate:
45
+ // - Message must be short (model got cut off mid-stream, so it
46
+ // can't be long)
47
+ // - Message must NOT end with '?' (questions are user complaints
48
+ // about past behavior, not pollution)
49
+ // - Message must NOT contain "last time" / "earlier" / "before"
50
+ // (temporal references indicate user discussing past behavior)
51
+ if (content.length >= 200) return false;
52
+ if (/\?\s*$/.test(content.trim())) return false;
53
+ if (/\b(last time|earlier|before|previously|yesterday)\b/i.test(content)) return false;
54
+ if (POLLUTION_WEAK.some(p => p.test(content))) return true;
55
+ return false;
56
+ }
57
+
58
+ export class SessionManager {
59
+ private messages: ChatMessage[] = [];
60
+ private sessionId: string;
61
+ private createdAt: Date;
62
+ private compactor: ContextCompactor;
63
+ private maxMessages: number;
64
+ /** v2.2.4: counter for messages dropped by the pollution filter. */
65
+ private droppedCount: number = 0;
66
+ /**
67
+ * v3.0.0: callback invoked when the message count exceeds maxMessages.
68
+ * Reasonix principle: do not mutate messages to compact; instead, signal
69
+ * the TUI to suggest the user call /new (start a fresh session) so that
70
+ * the upstream prompt cache keeps a stable prefix. NWT auto-logs every
71
+ * meaningful event, so no history is lost — it just moves out of the
72
+ * live cache window.
73
+ */
74
+ public onSuggestNewSession?: (info: { currentLength: number; max: number }) => void;
75
+
76
+ constructor(maxMessages = 50, options?: { onSuggestNewSession?: SessionManager['onSuggestNewSession'] }) {
77
+ this.maxMessages = maxMessages;
78
+ this.sessionId = `session_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
79
+ this.createdAt = new Date();
80
+ this.compactor = new ContextCompactor({ maxMessages });
81
+ this.onSuggestNewSession = options?.onSuggestNewSession;
82
+ }
83
+
84
+ addMessage(role: ChatMessage['role'], content: string, extras?: Partial<ChatMessage>): void {
85
+ this.messages.push({ role, content, ...extras });
86
+ this.autoCompact();
87
+ }
88
+
89
+ /**
90
+ * v2.2.4 (port from v2.1.0): same as addMessage but returns false
91
+ * (and skips the push) if the message content matches a known
92
+ * truncation/abort pollution pattern. Use this for assistant
93
+ * messages whose stream might have been aborted.
94
+ */
95
+ addMessageSafe(role: ChatMessage['role'], content: string, extras?: Partial<ChatMessage>): boolean {
96
+ if (role === 'assistant' && looksPolluted(content)) {
97
+ this.droppedCount++;
98
+ return false;
99
+ }
100
+ this.addMessage(role, content, extras);
101
+ return true;
102
+ }
103
+
104
+ /** v2.2.4: total messages dropped by addMessageSafe since session start. */
105
+ getDroppedCount(): number {
106
+ return this.droppedCount;
107
+ }
108
+
109
+ getMessages(): ChatMessage[] {
110
+ return [...this.messages];
111
+ }
112
+
113
+ getMessageCount(): number {
114
+ return this.messages.length;
115
+ }
116
+
117
+ clear(): void {
118
+ this.messages = [];
119
+ }
120
+
121
+ /**
122
+ * v3.0.0: "auto-compact" is now a no-op on `messages`. We instead invoke
123
+ * the registered `onSuggestNewSession` callback (if any) so the TUI can
124
+ * surface a "context is getting long, consider /new" hint. This keeps
125
+ * the messages array byte-stable across rounds, which is critical for
126
+ * prompt-cache hit-rate: any reordering or summary insertion would
127
+ * invalidate every cache breakpoint the upstream provider has set.
128
+ *
129
+ * Rationale (Reasonix-style): when context exceeds the model window or
130
+ * the configured maxMessages threshold, the right action is to start a
131
+ * NEW session, not to silently truncate or summarize. NWT auto-logs
132
+ * every meaningful event, so the user does not lose context — it just
133
+ * moves out of the live cache window. If the user wants a longer
134
+ * session, they can bump maxMessages in `~/.thatgfsj/config.json`.
135
+ *
136
+ * The original "fake compact" logic (insert `[Earlier conversation...]`
137
+ * summary) is preserved as `truncate()` for callers that explicitly
138
+ * want to compact (e.g. /new command hard-reset).
139
+ */
140
+ private autoCompact(): void {
141
+ if (this.messages.length <= this.maxMessages) return;
142
+
143
+ // Notify the TUI. The default TUI behavior is to render a toast:
144
+ // ⚠️ 上下文超长({currentLength}/{max}),建议调 /new 开新会话
145
+ // (NWT 已自动归档历史)
146
+ try {
147
+ this.onSuggestNewSession?.({
148
+ currentLength: this.messages.length,
149
+ max: this.maxMessages,
150
+ });
151
+ } catch {
152
+ // Callback failure must not break the chat loop.
153
+ }
154
+ // Intentionally do NOT mutate this.messages. Prefix stability matters more.
155
+ }
156
+
157
+ truncate(maxMessages?: number): void {
158
+ if (maxMessages) {
159
+ this.maxMessages = maxMessages;
160
+ this.compactor = new ContextCompactor({ maxMessages });
161
+ }
162
+ const { compacted } = this.compactor.compact(this.messages);
163
+ this.messages = compacted;
164
+ }
165
+
166
+ getId(): string {
167
+ return this.sessionId;
168
+ }
169
+
170
+ getInfo(): { id: string; messageCount: number; createdAt: Date } {
171
+ return {
172
+ id: this.sessionId,
173
+ messageCount: this.messages.length,
174
+ createdAt: this.createdAt,
175
+ };
176
+ }
177
+ }
178
+
179
+ export { ContextCompactor } from './compactor.js';
180
+ export type { CompactorConfig, CompressionResult } from './compactor.js';
181
+ export * from './message.js';
@@ -1,42 +1,42 @@
1
- /**
2
- * Message types and utilities
3
- */
4
-
5
- import type { ChatMessage, Role } from '../types.js';
6
-
7
- export { ChatMessage, Role };
8
-
9
- /**
10
- * Create a message
11
- */
12
- export function createMessage(role: Role, content: string, extras?: Partial<ChatMessage>): ChatMessage {
13
- return { role, content, ...extras };
14
- }
15
-
16
- /**
17
- * Create a system message
18
- */
19
- export function systemMessage(content: string): ChatMessage {
20
- return { role: 'system', content };
21
- }
22
-
23
- /**
24
- * Create a user message
25
- */
26
- export function userMessage(content: string): ChatMessage {
27
- return { role: 'user', content };
28
- }
29
-
30
- /**
31
- * Create an assistant message
32
- */
33
- export function assistantMessage(content: string): ChatMessage {
34
- return { role: 'assistant', content };
35
- }
36
-
37
- /**
38
- * Create a tool result message
39
- */
40
- export function toolMessage(content: string, toolCallId: string, name: string): ChatMessage {
41
- return { role: 'tool', content, tool_call_id: toolCallId, name };
42
- }
1
+ /**
2
+ * Message types and utilities
3
+ */
4
+
5
+ import type { ChatMessage, Role } from '../types.js';
6
+
7
+ export { ChatMessage, Role };
8
+
9
+ /**
10
+ * Create a message
11
+ */
12
+ export function createMessage(role: Role, content: string, extras?: Partial<ChatMessage>): ChatMessage {
13
+ return { role, content, ...extras };
14
+ }
15
+
16
+ /**
17
+ * Create a system message
18
+ */
19
+ export function systemMessage(content: string): ChatMessage {
20
+ return { role: 'system', content };
21
+ }
22
+
23
+ /**
24
+ * Create a user message
25
+ */
26
+ export function userMessage(content: string): ChatMessage {
27
+ return { role: 'user', content };
28
+ }
29
+
30
+ /**
31
+ * Create an assistant message
32
+ */
33
+ export function assistantMessage(content: string): ChatMessage {
34
+ return { role: 'assistant', content };
35
+ }
36
+
37
+ /**
38
+ * Create a tool result message
39
+ */
40
+ export function toolMessage(content: string, toolCallId: string, name: string): ChatMessage {
41
+ return { role: 'tool', content, tool_call_id: toolCallId, name };
42
+ }