@kkelly-offical/kkcode 0.2.3-preview.1 → 0.2.3-preview.2

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # kkcode
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/@kkelly-offical/kkcode?label=v0.2.3-preview.1)](https://www.npmjs.com/package/@kkelly-offical/kkcode)
3
+ [![npm version](https://img.shields.io/npm/v/@kkelly-offical/kkcode?label=v0.2.3-preview.2)](https://www.npmjs.com/package/@kkelly-offical/kkcode)
4
4
  [![GitHub Release](https://img.shields.io/github/v/release/kkelly-offical/kkcode)](https://github.com/kkelly-offical/kkcode/releases)
5
5
  ![Node](https://img.shields.io/badge/Node.js-%3E%3D22-green)
6
6
  ![License](https://img.shields.io/badge/License-GPL--3.0-blue)
@@ -376,15 +376,17 @@ update:
376
376
  <a id="release-status"></a>
377
377
  ## Release Status / 发布状态
378
378
 
379
- **Current preview / 当前预览版本**: `v0.2.3-preview.1`
379
+ **Current preview / 当前预览版本**: `v0.2.3-preview.2`
380
380
  **Latest releases / 最新发布**: [GitHub Releases](https://github.com/kkelly-offical/kkcode/releases)
381
381
  **Package / 包地址**: [npm](https://www.npmjs.com/package/@kkelly-offical/kkcode)
382
382
 
383
383
  **English**
384
+ - `0.2.3-preview.2` is the Preview V2 context release: compaction now merges prior context state, keeps evidence before pruning, and preserves turn metadata for reliable recent-turn retention.
384
385
  - `0.2.3-preview.1` is the Preview V1 updater release: kkcode can check npm dist-tags at startup and exposes `kkcode update` for manual upgrades.
385
386
  - `0.2.1` rebuilt kkcode around Assistant as the default general-purpose lane, with dedicated Agent and LongAgent modes for coding work.
386
387
 
387
388
  **中文**
389
+ - `0.2.3-preview.2` 是 Preview V2 上下文版本:压缩会合并旧上下文状态,在裁剪前保留关键证据,并保留 turn 元数据以稳定保留最近对话。
388
390
  - `0.2.3-preview.1` 是 Preview V1 更新器版本:kkcode 可在启动时检查 npm dist-tag,并提供 `kkcode update` 手动升级入口。
389
391
  - `0.2.1` 将 kkcode 重构为以 Assistant 为默认入口的通用个人助手,同时保留专门面向代码工作的 Agent 和 LongAgent 模式。
390
392
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kkelly-offical/kkcode",
3
- "version": "0.2.3-preview.1",
3
+ "version": "0.2.3-preview.2",
4
4
  "description": "CLI-first personal assistant with dedicated coding and LongAgent modes for governed terminal workflows, MCP integrations, and extensible automation.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.5.2",
@@ -8,7 +8,7 @@
8
8
  "author": "kkelly-offical",
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "https://github.com/kkelly-offical/kkcode.git"
11
+ "url": "git+https://github.com/kkelly-offical/kkcode.git"
12
12
  },
13
13
  "homepage": "https://github.com/kkelly-offical/kkcode",
14
14
  "bugs": {
@@ -25,7 +25,7 @@
25
25
  "mcp"
26
26
  ],
27
27
  "bin": {
28
- "kkcode": "./src/index.mjs"
28
+ "kkcode": "src/index.mjs"
29
29
  },
30
30
  "files": [
31
31
  "src/",
@@ -5,36 +5,36 @@ import { saveCheckpoint } from "./checkpoint.mjs"
5
5
  import { recordTurn } from "../usage/usage-meter.mjs"
6
6
  import { loadPricing, calculateCost } from "../usage/pricing.mjs"
7
7
 
8
- const COMPACTION_SYSTEM = `You are a conversation summarizer. Create a structured summary preserving all critical information for continued work.
8
+ const COMPACTION_SYSTEM = `You are a conversation summarizer. Create a structured, merge-safe summary preserving all critical information for continued work.
9
9
 
10
10
  ## Output Format
11
11
 
12
+ Return exactly:
13
+
14
+ <context-state>
15
+ {
16
+ "goal": "The user's current overall goal",
17
+ "completed": ["Completed work with specific file paths, function names, and line numbers"],
18
+ "in_progress": ["Current work being done"],
19
+ "files_modified": [{"path":"path/to/file","changes":["specific change"]}],
20
+ "key_decisions": ["Decision, constraint, or user preference"],
21
+ "errors_resolved": ["Error -> fix applied"],
22
+ "evidence": ["Important command output, test result, provider error, or exact failure detail"],
23
+ "next_steps": ["Specific next action item"]
24
+ }
25
+ </context-state>
26
+
12
27
  <summary>
13
- <goal>The user's overall goal or current task</goal>
14
- <completed>
15
- - Completed task with specific details (file paths, function names, line numbers)
16
- </completed>
17
- <in_progress>Current work being done, if any</in_progress>
18
- <files_modified>
19
- - path/to/file: specific change description
20
- </files_modified>
21
- <key_decisions>
22
- - Decision and reasoning
23
- - User preferences or constraints
24
- </key_decisions>
25
- <errors_resolved>
26
- - Error description → fix applied
27
- </errors_resolved>
28
- <next_steps>
29
- - Specific next action items
30
- </next_steps>
28
+ Concise human-readable continuation summary.
31
29
  </summary>
32
30
 
33
31
  Rules:
34
32
  - Use the SAME LANGUAGE as the conversation
33
+ - Merge the prior context state with the new conversation delta; do not summarize the prior state as another chat message
35
34
  - Preserve ALL file paths, function names, variable names, and technical identifiers exactly
36
35
  - Include specific code changes, not just "modified file X"
37
36
  - Omit tool call metadata and message formatting details
37
+ - Preserve exact errors, failing test names, package versions, release labels, and user constraints in evidence
38
38
  - Be concise but never drop actionable information`
39
39
 
40
40
  const DEFAULT_THRESHOLD_MESSAGES = 50
@@ -42,6 +42,118 @@ const DEFAULT_THRESHOLD_RATIO = 0.7
42
42
  const DEFAULT_KEEP_RECENT = 6
43
43
  const DEFAULT_KEEP_RECENT_TURNS = 3
44
44
  const TOOL_RESULT_PREVIEW_LIMIT = 200
45
+ const EVIDENCE_PREVIEW_LIMIT = 900
46
+ const PATH_RE = /(?:^|\s)([A-Za-z0-9_.@~/-]+\.(?:mjs|js|ts|tsx|jsx|json|yaml|yml|md|txt|rs|go|py|sh|toml|lock))(?:[:\s]|$)/g
47
+ const IMPORTANT_LINE_RE = /(error|failed|failure|exception|traceback|assert|reject|denied|unauthorized|context|compact|version|publish|npm|test|lint|typecheck|diff|modified)/i
48
+
49
+ export function isCompactionSummaryMessage(msg) {
50
+ const content = msg?.content
51
+ if (typeof content === "string") return content.includes("<compaction-summary")
52
+ if (Array.isArray(content)) {
53
+ return content.some((block) => {
54
+ if (typeof block === "string") return block.includes("<compaction-summary")
55
+ return block?.type === "text" && typeof block.text === "string" && block.text.includes("<compaction-summary")
56
+ })
57
+ }
58
+ return false
59
+ }
60
+
61
+ export function extractCompactionSummary(content) {
62
+ const text = Array.isArray(content)
63
+ ? content.map((block) => typeof block === "string" ? block : (block?.text || block?.content || "")).join("\n")
64
+ : String(content || "")
65
+ const match = text.match(/<compaction-summary(?:\s[^>]*)?>\s*([\s\S]*?)\s*<\/compaction-summary>/i)
66
+ return (match ? match[1] : "").trim()
67
+ }
68
+
69
+ function clip(text, limit = EVIDENCE_PREVIEW_LIMIT) {
70
+ const raw = String(text || "")
71
+ if (raw.length <= limit) return raw
72
+ return raw.slice(0, limit) + "... [truncated " + raw.length + " chars]"
73
+ }
74
+
75
+ function extractPaths(text) {
76
+ const paths = new Set()
77
+ let match
78
+ PATH_RE.lastIndex = 0
79
+ while ((match = PATH_RE.exec(String(text || ""))) !== null) {
80
+ paths.add(match[1])
81
+ if (paths.size >= 12) break
82
+ }
83
+ return [...paths]
84
+ }
85
+
86
+ function importantLines(text, limit = 12) {
87
+ return String(text || "")
88
+ .split(/\r?\n/)
89
+ .map((line) => line.trim())
90
+ .filter((line) => line && IMPORTANT_LINE_RE.test(line))
91
+ .slice(0, limit)
92
+ }
93
+
94
+ export function collectEvidenceLedger(messages, previewLimit = EVIDENCE_PREVIEW_LIMIT) {
95
+ const evidence = []
96
+ for (const msg of messages) {
97
+ const content = msg.content
98
+ const blocks = Array.isArray(content) ? content : [{ type: "text", text: content }]
99
+ for (const block of blocks) {
100
+ const raw = String(block?.content || block?.text || "")
101
+ if (!raw) continue
102
+ if (block.type === "tool_result") {
103
+ const lines = importantLines(raw)
104
+ const paths = extractPaths(raw)
105
+ if (block.is_error || lines.length || paths.length) {
106
+ evidence.push([
107
+ "- role=" + msg.role + " tool_result" + (block.is_error ? " ERROR" : ""),
108
+ paths.length ? " paths: " + paths.join(", ") : "",
109
+ lines.length ? " key_lines:\n" + lines.map((line) => " " + clip(line, 220)).join("\n") : " preview: " + clip(raw, previewLimit)
110
+ ].filter(Boolean).join("\n"))
111
+ }
112
+ } else if (typeof content === "string" && raw.length > 1000) {
113
+ const lines = importantLines(raw)
114
+ const paths = extractPaths(raw)
115
+ if (lines.length || paths.length) {
116
+ evidence.push([
117
+ "- role=" + msg.role + " long_text",
118
+ paths.length ? " paths: " + paths.join(", ") : "",
119
+ lines.length ? " key_lines:\n" + lines.map((line) => " " + clip(line, 220)).join("\n") : ""
120
+ ].filter(Boolean).join("\n"))
121
+ }
122
+ }
123
+ if (evidence.length >= 20) return evidence
124
+ }
125
+ }
126
+ return evidence
127
+ }
128
+
129
+ export function buildCompactionPrompt({ previousSummary = "", messages, evidence = [] }) {
130
+ const transcript = messages.map((m) => {
131
+ const content = m.content
132
+ if (Array.isArray(content)) {
133
+ return "[" + m.role + "]: " + content.map((b) => {
134
+ if (b.type === "text") return b.text || ""
135
+ if (b.type === "tool_use") return "[tool_use:" + b.name + "(" + JSON.stringify(b.input || {}).slice(0, 120) + ")]"
136
+ if (b.type === "tool_result") return "[tool_result:" + (b.is_error ? "ERROR " : "") + (b.content || "") + "]"
137
+ return ""
138
+ }).filter(Boolean).join("\n")
139
+ }
140
+ return "[" + m.role + "]: " + content
141
+ }).join("\n\n")
142
+
143
+ return [
144
+ "<prior-context-state>",
145
+ previousSummary || "No prior compacted context.",
146
+ "</prior-context-state>",
147
+ "",
148
+ "<evidence-ledger>",
149
+ evidence.length ? evidence.join("\n") : "No extracted evidence ledger.",
150
+ "</evidence-ledger>",
151
+ "",
152
+ "<conversation-delta>",
153
+ transcript,
154
+ "</conversation-delta>"
155
+ ].join("\n")
156
+ }
45
157
 
46
158
  // Estimate tokens from a string, accounting for CJK characters (~1.5 chars/token vs ~4 for Latin)
47
159
  export function estimateStringTokens(str) {
@@ -193,8 +305,12 @@ export async function compactSession({
193
305
  baseUrl = null,
194
306
  apiKeyEnv = null
195
307
  }) {
196
- const history = await getConversationHistory(sessionId, 9999)
308
+ const history = await getConversationHistory(sessionId, 9999, { includeMetadata: true })
197
309
  if (history.length <= keepRecent + 2) return { compacted: false, reason: "too few messages" }
310
+ const previousSummary = isCompactionSummaryMessage(history[0])
311
+ ? extractCompactionSummary(history[0].content)
312
+ : ""
313
+ const workingHistory = previousSummary ? history.slice(1) : history
198
314
 
199
315
  // Turn-based split: keep last keepRecentTurns complete turns
200
316
  // A "turn" = one user interaction cycle (user msg + model response + all tool calls)
@@ -202,7 +318,7 @@ export async function compactSession({
202
318
  let splitIdx
203
319
  const turnIds = []
204
320
  const seenTurns = new Set()
205
- for (const msg of history) {
321
+ for (const msg of workingHistory) {
206
322
  if (msg.turnId && !seenTurns.has(msg.turnId)) {
207
323
  seenTurns.add(msg.turnId)
208
324
  turnIds.push(msg.turnId)
@@ -210,29 +326,19 @@ export async function compactSession({
210
326
  }
211
327
  if (turnIds.length > keepRecentTurns) {
212
328
  const keepFromTurnId = turnIds[turnIds.length - keepRecentTurns]
213
- splitIdx = history.findIndex(msg => msg.turnId === keepFromTurnId)
214
- if (splitIdx < 0) splitIdx = history.length - keepRecent
329
+ splitIdx = workingHistory.findIndex(msg => msg.turnId === keepFromTurnId)
330
+ if (splitIdx < 0) splitIdx = workingHistory.length - keepRecent
215
331
  } else {
216
332
  // Fallback: not enough turns, use message count
217
- splitIdx = history.length - keepRecent
333
+ splitIdx = workingHistory.length - keepRecent
218
334
  }
219
- const toSummarize = history.slice(0, splitIdx)
220
- const kept = history.slice(splitIdx)
335
+ const toSummarize = workingHistory.slice(0, splitIdx)
336
+ const kept = workingHistory.slice(splitIdx)
221
337
 
222
- // Layer 1: prune large tool outputs before sending to LLM
338
+ // Layer 1: extract exact evidence, then prune large tool outputs before sending to LLM
339
+ const evidence = collectEvidenceLedger(toSummarize)
223
340
  const pruned = pruneForSummary(toSummarize)
224
- const summaryPrompt = pruned.map((m) => {
225
- const content = m.content
226
- if (Array.isArray(content)) {
227
- return `[${m.role}]: ${content.map((b) => {
228
- if (b.type === "text") return b.text || ""
229
- if (b.type === "tool_use") return `[tool_use:${b.name}(${JSON.stringify(b.input || {}).slice(0, 120)})]`
230
- if (b.type === "tool_result") return `[tool_result:${b.is_error ? "ERROR " : ""}${b.content || ""}]`
231
- return ""
232
- }).filter(Boolean).join("\n")}`
233
- }
234
- return `[${m.role}]: ${content}`
235
- }).join("\n\n")
341
+ const summaryPrompt = buildCompactionPrompt({ previousSummary, messages: pruned, evidence })
236
342
 
237
343
  const hookPayload = await HookBus.sessionCompacting({
238
344
  sessionId,
@@ -266,7 +372,7 @@ export async function compactSession({
266
372
  // Replace all messages with: [summary] + [kept recent messages]
267
373
  const summaryMessage = {
268
374
  role: "user",
269
- content: `<compaction-summary>\n${summaryText}\n</compaction-summary>`
375
+ content: `<compaction-summary version="2">\n${summaryText}\n</compaction-summary>`
270
376
  }
271
377
  await replaceMessages(sessionId, [summaryMessage, ...kept])
272
378
 
@@ -285,8 +391,10 @@ export async function compactSession({
285
391
  compactedAt: Date.now(),
286
392
  summarizeCount: toSummarize.length,
287
393
  keepCount: kept.length,
288
- summaryVersion: 1,
289
- summaryLength: summaryText.length
394
+ summaryVersion: 2,
395
+ summaryLength: summaryText.length,
396
+ previousSummaryLength: previousSummary.length,
397
+ evidenceCount: evidence.length
290
398
  })
291
399
 
292
400
  return {
@@ -323,7 +323,7 @@ export async function listSessions({ cwd = null, limit = 100, includeChildren =
323
323
  })
324
324
  }
325
325
 
326
- export async function getConversationHistory(sessionId, limit = 30) {
326
+ export async function getConversationHistory(sessionId, limit = 30, options = {}) {
327
327
  return withLock(async () => {
328
328
  await ensureLoadedUnsafe()
329
329
  const data = await loadSessionDataUnsafe(sessionId)
@@ -339,10 +339,19 @@ export async function getConversationHistory(sessionId, limit = 30) {
339
339
  const sliced = firstIsCompaction
340
340
  ? [msgs[0], ...msgs.slice(1).slice(-limit)]
341
341
  : msgs.slice(-limit)
342
- return sliced.map((msg) => ({
343
- role: msg.role,
344
- content: msg.content
345
- }))
342
+ return sliced.map((msg) => {
343
+ const base = {
344
+ role: msg.role,
345
+ content: msg.content
346
+ }
347
+ if (!options.includeMetadata) return base
348
+ return {
349
+ ...base,
350
+ turnId: msg.turnId,
351
+ step: msg.step,
352
+ synthetic: msg.synthetic
353
+ }
354
+ })
346
355
  })
347
356
  }
348
357
 
package/src/version.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  export const PACKAGE_NAME = "@kkelly-offical/kkcode"
2
- export const PACKAGE_VERSION = "0.2.3-preview.1"
3
- export const RELEASE_LABEL = "0.2.3 Preview V1"
2
+ export const PACKAGE_VERSION = "0.2.3-preview.2"
3
+ export const RELEASE_LABEL = "0.2.3 Preview V2"