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.
- package/CHANGELOG.md +15 -67
- package/dist/app/index.d.ts +28 -5
- package/dist/app/index.d.ts.map +1 -1
- package/dist/app/index.js +76 -10
- package/dist/app/index.js.map +1 -1
- package/dist/cache/fingerprint.d.ts +49 -0
- package/dist/cache/fingerprint.d.ts.map +1 -0
- package/dist/cache/fingerprint.js +100 -0
- package/dist/cache/fingerprint.js.map +1 -0
- package/dist/cache/index.d.ts +15 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +15 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/smartModel.d.ts +40 -0
- package/dist/cache/smartModel.d.ts.map +1 -0
- package/dist/cache/smartModel.js +43 -0
- package/dist/cache/smartModel.js.map +1 -0
- package/dist/cache/stats.d.ts +93 -0
- package/dist/cache/stats.d.ts.map +1 -0
- package/dist/cache/stats.js +155 -0
- package/dist/cache/stats.js.map +1 -0
- package/dist/cache/volatile.d.ts +32 -0
- package/dist/cache/volatile.d.ts.map +1 -0
- package/dist/cache/volatile.js +44 -0
- package/dist/cache/volatile.js.map +1 -0
- package/dist/cmd/index.js +49 -57
- package/dist/cmd/index.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +8 -0
- package/dist/config/index.js.map +1 -1
- package/dist/config/types.d.ts +14 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/llm/anthropic.d.ts +55 -5
- package/dist/llm/anthropic.d.ts.map +1 -1
- package/dist/llm/anthropic.js +172 -26
- package/dist/llm/anthropic.js.map +1 -1
- package/dist/llm/index.d.ts +26 -8
- package/dist/llm/index.d.ts.map +1 -1
- package/dist/llm/index.js +83 -15
- package/dist/llm/index.js.map +1 -1
- package/dist/llm/openai.d.ts +28 -8
- package/dist/llm/openai.d.ts.map +1 -1
- package/dist/llm/openai.js +58 -8
- package/dist/llm/openai.js.map +1 -1
- package/dist/llm/provider.d.ts +40 -8
- package/dist/llm/provider.d.ts.map +1 -1
- package/dist/llm/provider.js +8 -1
- package/dist/llm/provider.js.map +1 -1
- package/dist/prompts/index.d.ts +32 -0
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +33 -11
- package/dist/prompts/index.js.map +1 -1
- package/dist/session/index.d.ts +32 -3
- package/dist/session/index.d.ts.map +1 -1
- package/dist/session/index.js +51 -36
- package/dist/session/index.js.map +1 -1
- package/dist/skills/brainstorming.js +33 -33
- package/dist/skills/code-review.js +35 -35
- package/dist/skills/executing-plans.js +18 -18
- package/dist/skills/frontend-design.js +25 -25
- package/dist/skills/git-workflow.js +26 -26
- package/dist/skills/improve-architecture.js +28 -28
- package/dist/skills/neuroweave.js +37 -37
- package/dist/skills/playwright.js +62 -62
- package/dist/skills/prototype.js +20 -20
- package/dist/skills/subagent.js +19 -19
- package/dist/skills/supabase.js +34 -34
- package/dist/skills/systematic-debugging.js +34 -34
- package/dist/skills/tdd.js +29 -29
- package/dist/skills/triage.js +25 -25
- package/dist/skills/verification.js +22 -22
- package/dist/skills/writing-plans.js +32 -32
- package/dist/tools/nwt.js +16 -16
- package/dist/tui/app.d.ts.map +1 -1
- package/dist/tui/app.js +9 -2
- package/dist/tui/app.js.map +1 -1
- package/dist/tui/components/Header.d.ts +8 -0
- package/dist/tui/components/Header.d.ts.map +1 -1
- package/dist/tui/components/Header.js +17 -4
- package/dist/tui/components/Header.js.map +1 -1
- package/dist/tui/components/InitWizard.d.ts +12 -1
- package/dist/tui/components/InitWizard.d.ts.map +1 -1
- package/dist/tui/components/InitWizard.js +58 -3
- package/dist/tui/components/InitWizard.js.map +1 -1
- package/dist/tui/components/ModelSelector.d.ts +3 -1
- package/dist/tui/components/ModelSelector.d.ts.map +1 -1
- package/dist/tui/components/ModelSelector.js +24 -7
- package/dist/tui/components/ModelSelector.js.map +1 -1
- package/dist/tui/components/ToolCall.d.ts.map +1 -1
- package/dist/tui/components/ToolCall.js +1 -6
- package/dist/tui/components/ToolCall.js.map +1 -1
- package/dist/tui/components/UserInput.d.ts.map +1 -1
- package/dist/tui/components/UserInput.js +20 -0
- package/dist/tui/components/UserInput.js.map +1 -1
- package/dist/tui/hooks/useChat.d.ts +22 -0
- package/dist/tui/hooks/useChat.d.ts.map +1 -1
- package/dist/tui/hooks/useChat.js +82 -46
- package/dist/tui/hooks/useChat.js.map +1 -1
- package/dist/tui/hooks/useCommands.d.ts.map +1 -1
- package/dist/tui/hooks/useCommands.js +57 -0
- package/dist/tui/hooks/useCommands.js.map +1 -1
- package/dist/tui/welcome.d.ts.map +1 -1
- package/dist/tui/welcome.js +2 -3
- package/dist/tui/welcome.js.map +1 -1
- package/dist/types.d.ts +79 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -1
- package/dist/utils/stableStringify.d.ts +21 -0
- package/dist/utils/stableStringify.d.ts.map +1 -0
- package/dist/utils/stableStringify.js +53 -0
- package/dist/utils/stableStringify.js.map +1 -0
- package/dist/utils/thinking.d.ts.map +1 -1
- package/dist/utils/thinking.js +0 -3
- package/dist/utils/thinking.js.map +1 -1
- package/package.json +7 -6
- package/src/app/index.ts +189 -124
- package/src/cache/fingerprint.ts +101 -0
- package/src/cache/index.ts +15 -0
- package/src/cache/smartModel.ts +52 -0
- package/src/cache/stats.ts +199 -0
- package/src/cache/volatile.ts +47 -0
- package/src/cmd/index.tsx +288 -292
- package/src/config/index.ts +156 -148
- package/src/config/providers.ts +234 -234
- package/src/config/types.ts +67 -53
- package/src/hooks/index.ts +111 -111
- package/src/llm/anthropic.ts +388 -243
- package/src/llm/gemini.ts +169 -169
- package/src/llm/index.ts +265 -200
- package/src/llm/openai.ts +243 -196
- package/src/llm/provider.ts +66 -34
- package/src/prompts/index.ts +260 -220
- package/src/session/compactor.ts +103 -103
- package/src/session/index.ts +181 -168
- package/src/session/message.ts +42 -42
- package/src/skills/brainstorming.ts +43 -43
- package/src/skills/code-review.ts +45 -45
- package/src/skills/executing-plans.ts +27 -27
- package/src/skills/frontend-design.ts +35 -35
- package/src/skills/git-workflow.ts +36 -36
- package/src/skills/improve-architecture.ts +38 -38
- package/src/skills/index.ts +136 -136
- package/src/skills/neuroweave.ts +47 -47
- package/src/skills/playwright.ts +72 -72
- package/src/skills/prototype.ts +30 -30
- package/src/skills/subagent.ts +28 -28
- package/src/skills/supabase.ts +44 -44
- package/src/skills/systematic-debugging.ts +44 -44
- package/src/skills/tdd.ts +39 -39
- package/src/skills/triage.ts +35 -35
- package/src/skills/verification.ts +31 -31
- package/src/skills/writing-plans.ts +42 -42
- package/src/tools/nwt.ts +598 -598
- package/src/tools/types.ts +122 -122
- package/src/tui/app.tsx +200 -186
- package/src/tui/components/ChatList.tsx +41 -41
- package/src/tui/components/ChatMessage.tsx +54 -54
- package/src/tui/components/Header.tsx +56 -29
- package/src/tui/components/InitWizard.tsx +217 -132
- package/src/tui/components/Markdown.tsx +35 -35
- package/src/tui/components/ModelSelector.tsx +107 -87
- package/src/tui/components/StatusBar.tsx +30 -30
- package/src/tui/components/Thinking.tsx +17 -17
- package/src/tui/components/ToolCall.tsx +102 -107
- package/src/tui/components/UserInput.tsx +151 -131
- package/src/tui/hooks/useChat.ts +287 -238
- package/src/tui/hooks/useCommands.ts +234 -176
- package/src/tui/index.ts +6 -6
- package/src/tui/welcome.ts +177 -178
- package/src/types.ts +104 -42
- package/src/utils/diff.ts +71 -71
- package/src/utils/project.ts +99 -99
- package/src/utils/stableStringify.ts +47 -0
- package/src/utils/thinking.ts +118 -121
- package/tests/cache/fingerprint.test.ts +75 -0
- package/tests/cache/providerCatalog.test.ts +31 -0
- package/tests/cache/stableStringify.test.ts +43 -0
- package/tests/cache/stats.test.ts +146 -0
- package/tests/cache/volatile.test.ts +75 -0
- package/tests/smoke-pollution.mjs +78 -0
- package/tests/smoke-thinking.mjs +110 -0
- package/tests/smoke-tool-stream.mjs +69 -0
- package/tests/smoke-tool.mjs +117 -0
- package/.nwt/meta.json +0 -5
- package/dist/version.d.ts +0 -16
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js +0 -16
- package/dist/version.js.map +0 -1
- package/src/version.ts +0 -16
package/src/session/compactor.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/session/index.ts
CHANGED
|
@@ -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
|
-
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
// -
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
private
|
|
61
|
-
private
|
|
62
|
-
private
|
|
63
|
-
private
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
this.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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';
|
package/src/session/message.ts
CHANGED
|
@@ -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
|
+
}
|