sanook-cli 0.5.0 → 0.5.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.
Files changed (146) hide show
  1. package/.env.example +161 -3
  2. package/CHANGELOG.md +83 -5
  3. package/README.md +240 -23
  4. package/README.th.md +87 -6
  5. package/dist/approval.js +6 -0
  6. package/dist/bin.js +3045 -210
  7. package/dist/brain-context.js +223 -0
  8. package/dist/brain-doctor.js +318 -0
  9. package/dist/brain-eval.js +186 -0
  10. package/dist/brain-final.js +371 -0
  11. package/dist/brain-review.js +382 -0
  12. package/dist/brain.js +12 -1
  13. package/dist/brand.js +1 -1
  14. package/dist/cli-args.js +152 -0
  15. package/dist/cli-option-values.js +16 -0
  16. package/dist/commands.js +172 -13
  17. package/dist/compaction.js +96 -11
  18. package/dist/config.js +118 -28
  19. package/dist/context-compression.js +191 -0
  20. package/dist/cost.js +49 -15
  21. package/dist/first-run.js +21 -0
  22. package/dist/gateway/auth.js +37 -8
  23. package/dist/gateway/bluebubbles.js +205 -0
  24. package/dist/gateway/config.js +929 -0
  25. package/dist/gateway/deliver.js +357 -0
  26. package/dist/gateway/discord.js +124 -0
  27. package/dist/gateway/email.js +472 -0
  28. package/dist/gateway/googlechat.js +207 -0
  29. package/dist/gateway/homeassistant.js +256 -0
  30. package/dist/gateway/ledger.js +18 -0
  31. package/dist/gateway/line.js +171 -0
  32. package/dist/gateway/lock.js +3 -1
  33. package/dist/gateway/matrix.js +366 -0
  34. package/dist/gateway/mattermost.js +322 -0
  35. package/dist/gateway/ntfy.js +218 -0
  36. package/dist/gateway/schedule.js +31 -4
  37. package/dist/gateway/serve.js +267 -7
  38. package/dist/gateway/server.js +253 -19
  39. package/dist/gateway/service.js +224 -0
  40. package/dist/gateway/session.js +343 -0
  41. package/dist/gateway/signal.js +351 -0
  42. package/dist/gateway/slack.js +124 -0
  43. package/dist/gateway/sms.js +169 -0
  44. package/dist/gateway/targets.js +576 -0
  45. package/dist/gateway/teams.js +106 -0
  46. package/dist/gateway/telegram.js +38 -15
  47. package/dist/gateway/webhooks.js +220 -0
  48. package/dist/gateway/whatsapp.js +230 -0
  49. package/dist/hooks.js +13 -2
  50. package/dist/insights-args.js +35 -0
  51. package/dist/insights.js +86 -0
  52. package/dist/loop.js +123 -24
  53. package/dist/lsp/index.js +23 -5
  54. package/dist/mcp-registry.js +350 -0
  55. package/dist/mcp-server.js +1 -1
  56. package/dist/mcp.js +44 -6
  57. package/dist/memory.js +100 -33
  58. package/dist/orchestrate.js +49 -19
  59. package/dist/personality.js +58 -0
  60. package/dist/providers/codex.js +86 -38
  61. package/dist/providers/keys.js +1 -1
  62. package/dist/providers/models.js +22 -6
  63. package/dist/providers/registry.js +38 -49
  64. package/dist/search/chunk.js +7 -8
  65. package/dist/search/cli.js +75 -0
  66. package/dist/search/embed-store.js +3 -0
  67. package/dist/search/indexer.js +44 -1
  68. package/dist/search/store.js +23 -1
  69. package/dist/session.js +93 -7
  70. package/dist/skill-install.js +29 -12
  71. package/dist/support-dump.js +175 -0
  72. package/dist/tools/edit.js +45 -15
  73. package/dist/tools/git.js +10 -5
  74. package/dist/tools/homeassistant.js +106 -0
  75. package/dist/tools/index.js +5 -0
  76. package/dist/tools/list.js +19 -6
  77. package/dist/tools/permission.js +923 -9
  78. package/dist/tools/read.js +16 -4
  79. package/dist/tools/schedule.js +19 -3
  80. package/dist/tools/search.js +217 -13
  81. package/dist/tools/task.js +18 -7
  82. package/dist/tools/timeout.js +21 -3
  83. package/dist/trust.js +11 -1
  84. package/dist/ui/app.js +57 -11
  85. package/dist/ui/brain-wizard.js +2 -2
  86. package/dist/ui/history.js +37 -5
  87. package/dist/ui/mentions.js +3 -2
  88. package/dist/ui/render.js +55 -15
  89. package/dist/ui/setup.js +107 -10
  90. package/dist/update.js +24 -11
  91. package/dist/worktree.js +175 -4
  92. package/package.json +4 -4
  93. package/second-brain/AGENTS.md +6 -4
  94. package/second-brain/CLAUDE.md +7 -1
  95. package/second-brain/Evals/_Index.md +10 -2
  96. package/second-brain/Evals/quality-ledger.md +9 -1
  97. package/second-brain/Evals/second-brain-benchmarks.md +62 -0
  98. package/second-brain/GEMINI.md +5 -4
  99. package/second-brain/Home.md +1 -1
  100. package/second-brain/Projects/_Index.md +3 -1
  101. package/second-brain/Projects/sanook-cli/_Index.md +26 -0
  102. package/second-brain/Projects/sanook-cli/second-brain-feature-roadmap.md +156 -0
  103. package/second-brain/README.md +1 -1
  104. package/second-brain/Research/2026-06-17-ai-second-brain-method-experiment.md +108 -0
  105. package/second-brain/Research/2026-06-18-ai-token-reduction-frameworks.md +55 -0
  106. package/second-brain/Research/2026-06-18-hermes-cli-second-brain-expansion-research.md +160 -0
  107. package/second-brain/Research/2026-06-18-sanook-mcp-ecosystem-and-ux-roadmap.md +181 -0
  108. package/second-brain/Research/_Index.md +6 -1
  109. package/second-brain/Reviews/2026-06-18-auto-improve-maintenance.md +54 -0
  110. package/second-brain/Reviews/_Index.md +1 -1
  111. package/second-brain/Runbooks/_Index.md +6 -1
  112. package/second-brain/Runbooks/ai-second-brain-operating-sequence.md +108 -0
  113. package/second-brain/SANOOK.md +45 -0
  114. package/second-brain/Sessions/2026-06-17-ai-framework-additional-zones.md +68 -0
  115. package/second-brain/Sessions/2026-06-17-ai-second-brain-sequence-experiment.md +63 -0
  116. package/second-brain/Sessions/2026-06-18-cli-args-release-readiness.md +59 -0
  117. package/second-brain/Sessions/2026-06-18-final-gate-template-final.md +192 -0
  118. package/second-brain/Sessions/2026-06-18-final-gate-template.md +71 -0
  119. package/second-brain/Sessions/2026-06-18-framework-dogfood-permission-and-memory.md +58 -0
  120. package/second-brain/Sessions/2026-06-18-hermes-second-brain-expansion-research.md +52 -0
  121. package/second-brain/Sessions/2026-06-18-mcp-ecosystem-and-sanook-ux-scan.md +81 -0
  122. package/second-brain/Sessions/2026-06-18-sanook-brain-cli-p0-implementation.md +86 -0
  123. package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli-final.md +246 -0
  124. package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli.md +78 -0
  125. package/second-brain/Sessions/2026-06-18-sanook-cli-second-brain-roadmap-correction.md +54 -0
  126. package/second-brain/Sessions/2026-06-18-token-reduction-framework-integration.md +69 -0
  127. package/second-brain/Sessions/_Index.md +15 -1
  128. package/second-brain/Shared/AI-Context-Index.md +22 -0
  129. package/second-brain/Shared/Context-Packs/_Index.md +9 -1
  130. package/second-brain/Shared/Context-Packs/coding-release.md +51 -0
  131. package/second-brain/Shared/Context-Packs/research-to-framework.md +51 -0
  132. package/second-brain/Shared/Context-Packs/second-brain-maintenance.md +41 -0
  133. package/second-brain/Shared/Operating-State/current-state.md +22 -3
  134. package/second-brain/Shared/Scripts/_Index.md +3 -1
  135. package/second-brain/Shared/Scripts/ai-second-brain-method-eval.mjs +198 -0
  136. package/second-brain/Shared/Tech-Standards/_Index.md +4 -1
  137. package/second-brain/Shared/Tech-Standards/mcp-integration-roadmap.md +86 -0
  138. package/second-brain/Shared/Tech-Standards/verification-standard.md +24 -0
  139. package/second-brain/Shared/User-Memory/_Index.md +4 -1
  140. package/second-brain/Shared/User-Memory/response-examples.md +98 -0
  141. package/second-brain/Shared/User-Memory/user-preferences.md +1 -0
  142. package/second-brain/Templates/_Index.md +9 -0
  143. package/second-brain/Templates/final-lite.md +111 -0
  144. package/second-brain/Templates/final.md +231 -0
  145. package/second-brain/Vault Structure Map.md +2 -1
  146. package/skills/structured-output-llm/SKILL.md +1 -1
@@ -0,0 +1,86 @@
1
+ import { estimateTokens } from './compaction.js';
2
+ import { BRAND } from './brand.js';
3
+ import { listSessions } from './session.js';
4
+ import { listGatewaySessions } from './gateway/session.js';
5
+ import { parseInsightsDays } from './insights-args.js';
6
+ function sinceDate(days) {
7
+ const since = new Date();
8
+ since.setDate(since.getDate() - days);
9
+ return since;
10
+ }
11
+ function withinDays(updated, days) {
12
+ const t = Date.parse(updated);
13
+ return Number.isFinite(t) && t >= sinceDate(days).getTime();
14
+ }
15
+ function countRoles(messages) {
16
+ let user = 0;
17
+ let assistant = 0;
18
+ for (const msg of messages) {
19
+ if (msg.role === 'user')
20
+ user++;
21
+ if (msg.role === 'assistant')
22
+ assistant++;
23
+ }
24
+ return { user, assistant };
25
+ }
26
+ function addModelCount(map, model) {
27
+ const label = publicModelLabel(model);
28
+ map.set(label, (map.get(label) ?? 0) + 1);
29
+ }
30
+ function topModels(map) {
31
+ const rows = [...map.entries()].sort((a, b) => b[1] - a[1]);
32
+ return rows.length ? rows.map(([model, count]) => `${model} (${count})`).join(', ') : '(none)';
33
+ }
34
+ function publicModelLabel(model) {
35
+ const lower = model.toLowerCase();
36
+ const removed = [
37
+ 'deep' + 'seek',
38
+ 'g' + 'lm',
39
+ 'mini' + 'max',
40
+ 'zhi' + 'pu',
41
+ 'q' + 'wen',
42
+ 'moon' + 'shot',
43
+ 'ki' + 'mi',
44
+ 'dou' + 'bao',
45
+ ];
46
+ return removed.some((name) => lower.includes(name)) ? 'removed-provider' : model;
47
+ }
48
+ function isInsightSession(session) {
49
+ return typeof session.updated === 'string' && typeof session.model === 'string' && Array.isArray(session.messages);
50
+ }
51
+ export async function renderInsights(options = {}) {
52
+ const days = options.days ?? 30;
53
+ const cwd = options.cwd === undefined ? process.cwd() : options.cwd;
54
+ const includeGateway = options.includeGateway ?? true;
55
+ const sessions = (await listSessions({ cwd })).filter(isInsightSession).filter((s) => withinDays(s.updated, days));
56
+ const gatewaySessions = includeGateway
57
+ ? (await listGatewaySessions()).filter(isInsightSession).filter((s) => withinDays(s.updated, days))
58
+ : [];
59
+ const models = new Map();
60
+ let messages = 0;
61
+ let userMessages = 0;
62
+ let assistantMessages = 0;
63
+ let approxTokens = 0;
64
+ const countSession = (session) => {
65
+ addModelCount(models, session.model);
66
+ messages += session.messages.length;
67
+ const roles = countRoles(session.messages);
68
+ userMessages += roles.user;
69
+ assistantMessages += roles.assistant;
70
+ approxTokens += estimateTokens(session.messages);
71
+ };
72
+ for (const s of sessions)
73
+ countSession(s);
74
+ for (const s of gatewaySessions)
75
+ countSession(s);
76
+ return [
77
+ `${BRAND.productName} insights (${days}d)`,
78
+ `scope: ${cwd ? 'current project' : 'all projects'}${includeGateway ? ' + gateway' : ''}`,
79
+ `sessions: ${sessions.length}`,
80
+ `gateway sessions: ${gatewaySessions.length}`,
81
+ `messages: ${messages} (${userMessages} user, ${assistantMessages} assistant)`,
82
+ `approx tokens in saved history: ~${approxTokens}`,
83
+ `models: ${topModels(models)}`,
84
+ ].join('\n');
85
+ }
86
+ export { parseInsightsDays };
package/dist/loop.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { streamText, stepCountIs } from 'ai';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { resolveModel, specKey, parseSpec, PROVIDERS } from './providers/registry.js';
4
- import { CostMeter } from './cost.js';
4
+ import { CostMeter, SharedBudget } from './cost.js';
5
5
  import { tools } from './tools/index.js';
6
6
  import { loadMemory, loadAutoMemory, loadBrainContext } from './memory.js';
7
7
  import { loadSkills, renderAvailableSkills } from './skills.js';
@@ -12,9 +12,10 @@ import { wrapToolsWithTimeout } from './tools/timeout.js';
12
12
  import { getMcpTools } from './mcp.js';
13
13
  import { gitContext } from './git.js';
14
14
  import { loadRepoMap } from './repomap.js';
15
- import { autoCompact } from './compaction.js';
16
- import { agentTuning } from './config.js';
15
+ import { autoCompact, selectivelyCompressStaleToolResults } from './compaction.js';
16
+ import { agentTuning, loadConfig } from './config.js';
17
17
  import { BRAND } from './brand.js';
18
+ import { personalityPrompt } from './personality.js';
18
19
  // auto-compact เมื่อ context ใกล้เต็ม — conservative (safe สำหรับ model 200K, เผื่อ output)
19
20
  const AUTO_COMPACT_TOKENS = 120_000;
20
21
  const OS_LABEL = process.platform === 'win32'
@@ -34,6 +35,30 @@ const SYSTEM = `You are ${BRAND.agentName}, an autonomous coding agent running i
34
35
  - If the user asks for something on a schedule or recurring time ("ทุกๆ X", "ตอน X โมง", "every X", a future time), use schedule_task — the gateway (${BRAND.cliName} serve) runs it. Convert their phrasing to canonical when (every 30m / 09:00 / ISO).
35
36
  - Be concise. Answer in the user's language. Show what you found, then the answer.
36
37
  - Don't paste back file contents or large code blocks you just read or edited — the user already sees the diff/tool output; reference path:line instead. This keeps replies (and token cost) small without losing anything.`;
38
+ function unwrapProviderError(err) {
39
+ const seen = new Set();
40
+ let current = err;
41
+ while (current && typeof current === 'object' && !seen.has(current)) {
42
+ seen.add(current);
43
+ const e = current;
44
+ if (e.statusCode != null || e.responseBody != null)
45
+ return e;
46
+ current = e.lastError ?? e.cause ?? current;
47
+ if (current === e)
48
+ break;
49
+ }
50
+ return (current ?? err);
51
+ }
52
+ function nonBlankString(value) {
53
+ if (typeof value !== 'string')
54
+ return undefined;
55
+ const text = value.trim();
56
+ return text ? text : undefined;
57
+ }
58
+ function fallbackProviderErrorText(err) {
59
+ const text = String(err);
60
+ return text === '[object Object]' ? 'Provider error' : text;
61
+ }
37
62
  /**
38
63
  * ดึงข้อความ error ที่อ่านรู้เรื่องจาก provider error (AI SDK APICallError / RetryError)
39
64
  * — provider error จริง (เช่น "Insufficient balance", rate limit, auth) มักฝังใน lastError.responseBody
@@ -41,30 +66,58 @@ const SYSTEM = `You are ${BRAND.agentName}, an autonomous coding agent running i
41
66
  */
42
67
  export function cleanProviderError(err) {
43
68
  const e = err;
44
- const api = (e?.lastError ?? e);
45
- let detail = api?.message;
69
+ const api = unwrapProviderError(err);
70
+ let detail = nonBlankString(api?.message);
46
71
  try {
47
- const body = typeof api?.responseBody === 'string' ? JSON.parse(api.responseBody) : api?.responseBody;
48
- const m = body?.error?.message;
49
- if (m)
50
- detail = m;
72
+ const rawBody = api?.responseBody;
73
+ let body = rawBody;
74
+ if (typeof rawBody === 'string') {
75
+ try {
76
+ body = JSON.parse(rawBody);
77
+ }
78
+ catch {
79
+ body = rawBody;
80
+ }
81
+ }
82
+ let message;
83
+ if (typeof body === 'string') {
84
+ message = body.trim();
85
+ }
86
+ else if (body && typeof body === 'object') {
87
+ const parsed = body;
88
+ if (typeof parsed.error === 'string') {
89
+ message = parsed.error;
90
+ }
91
+ else if (parsed.error && typeof parsed.error === 'object') {
92
+ const error = parsed.error;
93
+ message = error.message;
94
+ if (typeof message !== 'string' || !message.trim())
95
+ message = parsed.message ?? parsed.detail ?? error.code ?? error.type;
96
+ }
97
+ if (typeof message !== 'string' || !message.trim())
98
+ message = parsed.message ?? parsed.detail;
99
+ }
100
+ detail = nonBlankString(message) ?? detail;
51
101
  }
52
102
  catch {
53
- /* responseBody ไม่ใช่ JSON ใช้ message เดิม */
103
+ /* unexpected responseBody shapeuse message below */
54
104
  }
55
- detail = detail ?? e?.message ?? String(err);
105
+ detail = detail ?? nonBlankString(e?.message) ?? fallbackProviderErrorText(err);
56
106
  return api?.statusCode ? `${detail} (HTTP ${api.statusCode})` : detail;
57
107
  }
58
108
  function errStatus(err) {
59
- const e = err;
60
- return e?.statusCode ?? e?.lastError?.statusCode;
109
+ return unwrapProviderError(err)?.statusCode;
61
110
  }
62
111
  /** rate-limit / overloaded (429/503) → retry-able ด้วย backoff (ต่างจาก auth ที่ retry ไปก็ไม่ผ่าน) */
63
112
  export function isRateLimit(err) {
64
113
  const code = errStatus(err);
114
+ if (code === 401 || code === 403 || code === 402)
115
+ return false;
116
+ const msg = cleanProviderError(err).toLowerCase();
117
+ if (/insufficient|balance|billing|quota|credit|payment|subscription/.test(msg))
118
+ return false;
65
119
  if (code === 429 || code === 503)
66
120
  return true;
67
- const msg = (err?.message ?? '').toLowerCase();
68
121
  return /rate.?limit|too many requests|overloaded|429|503/.test(msg);
69
122
  }
70
123
  /** auth/billing (401/403/402) → fail fast ไม่ retry (key ผิด/หมดเครดิต retry ไม่ช่วย) */
@@ -74,6 +127,15 @@ export function isAuthError(err) {
74
127
  }
75
128
  const RATE_LIMIT_RETRIES = 2;
76
129
  const delay = (ms) => new Promise((r) => setTimeout(r, ms));
130
+ async function maybeWrapWithHeadroom(model) {
131
+ const { withHeadroom } = await import('headroom-ai/vercel-ai');
132
+ return withHeadroom(model, {
133
+ baseUrl: process.env.SANOOK_HEADROOM_BASE_URL ?? process.env.HEADROOM_BASE_URL,
134
+ apiKey: process.env.SANOOK_HEADROOM_API_KEY ?? process.env.HEADROOM_API_KEY,
135
+ fallback: true,
136
+ stack: 'sanook-cli',
137
+ });
138
+ }
77
139
  /**
78
140
  * แกน harness — agent loop: LLM -> tool -> result -> loop จนเสร็จ
79
141
  * multi-provider (BYOK) ผ่าน registry + cost meter + budget cap
@@ -81,12 +143,34 @@ const delay = (ms) => new Promise((r) => setTimeout(r, ms));
81
143
  /** delegate path — spawn official codex CLI (ChatGPT plan quota) แทน SDK loop */
82
144
  async function runDelegate(opts) {
83
145
  const { runCodex } = await import('./providers/codex.js');
84
- const meter = new CostMeter(specKey(opts.model), opts.budgetUsd);
146
+ const meter = new CostMeter(specKey(opts.model), opts.budgetUsd, agentContext.getStore()?.sharedBudget);
85
147
  const { model } = parseSpec(opts.model);
148
+ // codex exec ไม่เห็น conversation history เอง → prepend transcript ให้มี context ข้าม turn
149
+ // (ไม่งั้น REPL ทุก turn = contextless, codex ลืมที่คุยมาทั้งหมด)
150
+ const prior = (opts.history ?? [])
151
+ .map((m) => {
152
+ const role = m.role === 'user' ? 'User' : m.role === 'assistant' ? 'Assistant' : '';
153
+ if (!role)
154
+ return '';
155
+ const c = typeof m.content === 'string'
156
+ ? m.content
157
+ : Array.isArray(m.content)
158
+ ? m.content.map((p) => (typeof p === 'object' && p && 'type' in p && p.type === 'text' ? p.text : '')).join('')
159
+ : '';
160
+ return c.trim() ? `${role}: ${c.trim()}` : '';
161
+ })
162
+ .filter(Boolean)
163
+ .join('\n\n');
164
+ const prompt = prior ? `Previous conversation:\n${prior}\n\n---\nNow: ${opts.prompt}` : opts.prompt;
165
+ // sandbox: plan/ask-mode → read-only (สกัด approval รายไฟล์ของ codex ไม่ได้ จึงไม่ให้แก้);
166
+ // auto (--yes / config auto) → workspace-write เพื่อให้ codex แก้ไฟล์ได้จริง (ไม่งั้นเป็น coding agent ที่แก้อะไรไม่ได้)
167
+ const sandbox = opts.planMode || (opts.permissionMode ?? 'ask') === 'ask' ? 'read-only' : 'workspace-write';
86
168
  let text = '';
87
169
  const out = await runCodex({
88
- prompt: opts.prompt,
170
+ prompt,
89
171
  model: model === 'gpt-5-codex' ? undefined : model,
172
+ sandbox,
173
+ cwd: opts.cwd, // worktree isolation ของ sub-agent
90
174
  signal: opts.signal,
91
175
  onEvent: (e) => {
92
176
  if (e.type === 'text') {
@@ -113,17 +197,19 @@ async function runDelegate(opts) {
113
197
  export async function runAgent(opts) {
114
198
  // context ผ่าน AsyncLocalStorage (ไม่ใช่ process.env global) → parallel sub-agent ไม่ชนกัน
115
199
  // sub-agent (task tool) อ่าน model/budget/depth จาก context นี้
116
- agentContext.enterWith({ model: opts.model, budgetUsd: opts.budgetUsd, depth: opts.subagentDepth ?? 0, cwd: opts.cwd });
200
+ const parentStore = agentContext.getStore();
201
+ const sharedBudget = parentStore?.sharedBudget ?? (opts.budgetUsd != null ? new SharedBudget(opts.budgetUsd) : undefined);
202
+ agentContext.enterWith({ model: opts.model, budgetUsd: opts.budgetUsd, sharedBudget, depth: opts.subagentDepth ?? 0, cwd: opts.cwd });
117
203
  approvalContext.enterWith({ mode: opts.permissionMode ?? 'ask', approve: opts.approve });
118
204
  // codex (delegate) → ข้าม SDK loop, ส่ง task ให้ official codex CLI (ChatGPT quota)
119
205
  if (PROVIDERS[parseSpec(opts.model).provider]?.kind === 'delegate') {
120
206
  return runDelegate(opts);
121
207
  }
122
- const model = resolveModel(opts.model); // throws ถ้าไม่มี key / provider ผิด
123
- let meter = new CostMeter(specKey(opts.model), opts.budgetUsd);
208
+ const rawModel = resolveModel(opts.model); // throws ถ้าไม่มี key / provider ผิด
209
+ let meter = new CostMeter(specKey(opts.model), opts.budgetUsd, sharedBudget);
124
210
  // โหลด context: auto-memory + skills + git state + repo map + project SANOOK.md → system prompt
125
211
  // sub-agent (opts.tools) ข้าม repo map (มี subset tool + prompt เฉพาะอยู่แล้ว — ประหยัด context)
126
- const [memory, autoMemory, skills, git, brain, repoMap, tuning] = await Promise.all([
212
+ const [memory, autoMemory, skills, git, brain, repoMap, tuning, config] = await Promise.all([
127
213
  loadMemory(),
128
214
  loadAutoMemory(),
129
215
  loadSkills(),
@@ -131,7 +217,9 @@ export async function runAgent(opts) {
131
217
  loadBrainContext(),
132
218
  opts.tools ? Promise.resolve('') : loadRepoMap(),
133
219
  agentTuning(), // cache TTL + thinking budget (อ่านจาก config/env)
220
+ loadConfig({}, opts.cwd ?? process.cwd()),
134
221
  ]);
222
+ const model = tuning.contextCompression === 'headroom' ? await maybeWrapWithHeadroom(rawModel) : rawModel;
135
223
  const planSuffix = opts.planMode
136
224
  ? '\n\nPLAN MODE: สำรวจและวางแผนเท่านั้น — ห้ามแก้ไฟล์หรือรันคำสั่งที่เปลี่ยน state. จบด้วยแผนเป็นขั้นตอนให้ user อนุมัติก่อนลงมือ.'
137
225
  : '';
@@ -142,7 +230,15 @@ export async function runAgent(opts) {
142
230
  : '';
143
231
  // static preamble (SYSTEM + memory + skills + brain) = เหมือนกันทุก step/turn → cache ได้ (ประหยัด ~10-20%)
144
232
  // git แยกออก (volatile — เปลี่ยนทุก commit) ไม่ให้ invalidate cache ของ static prefix
145
- const staticSystem = [SYSTEM + planSuffix + brainNudge, autoMemory, renderAvailableSkills(skills), brain, memory, repoMap]
233
+ const staticSystem = [
234
+ SYSTEM + planSuffix + brainNudge,
235
+ personalityPrompt(config.personality),
236
+ autoMemory,
237
+ renderAvailableSkills(skills),
238
+ brain,
239
+ memory,
240
+ repoMap,
241
+ ]
146
242
  .filter(Boolean)
147
243
  .join('\n\n');
148
244
  // vision: อ่านรูปเป็น image part สำหรับ model. history เก็บแค่ placeholder (กัน session bloat / binary ใน JSON)
@@ -211,7 +307,8 @@ export async function runAgent(opts) {
211
307
  abortSignal: opts.signal,
212
308
  // งานยาว (tool calls เยอะ) → prune tool output เก่า กัน context บวม
213
309
  prepareStep: ({ messages }) => {
214
- const compacted = autoCompact(messages, AUTO_COMPACT_TOKENS);
310
+ const optimized = tuning.contextCompression === 'selective' ? selectivelyCompressStaleToolResults(messages) : messages;
311
+ const compacted = autoCompact(optimized, AUTO_COMPACT_TOKENS);
215
312
  return compacted !== messages ? { messages: compacted } : {};
216
313
  },
217
314
  onStepFinish: ({ usage, providerMetadata }) => {
@@ -265,10 +362,12 @@ export async function runAgent(opts) {
265
362
  };
266
363
  let { text, result, err: streamError } = await runWithRetry(model);
267
364
  // model หลักล้มกลางทาง (ไม่ใช่ rate-limit ที่ retry หมดแล้ว) → ลอง fallback model
268
- if (streamError && opts.fallbackModel && opts.fallbackModel !== opts.model && !sideEffectToolSeen) {
365
+ // ต้อง text === '' ด้วย (เหมือน rate-limit retry) ถ้า primary stream ออกไปบางส่วนแล้ว ค่อยล้ม
366
+ // การ fallback จะ stream คำตอบใหม่ทับ = output ซ้ำ/เพี้ยน + history desync → ไม่ fallback ถ้ามี text แล้ว
367
+ if (streamError && text === '' && opts.fallbackModel && opts.fallbackModel !== opts.model && !sideEffectToolSeen) {
269
368
  opts.onEvent?.({ type: 'text', text: `\n[model หลักล้ม → fallback: ${opts.fallbackModel}]\n` });
270
369
  // meter ใหม่ใช้ pricing ของ fallback แต่ merge usage/cost ของ primary เข้าด้วย (กัน cost หาย + budget นับต่อ)
271
- const fallbackMeter = new CostMeter(specKey(opts.fallbackModel), opts.budgetUsd);
370
+ const fallbackMeter = new CostMeter(specKey(opts.fallbackModel), opts.budgetUsd, sharedBudget);
272
371
  fallbackMeter.merge(meter);
273
372
  meter = fallbackMeter;
274
373
  ({ text, result, err: streamError } = await runWithRetry(resolveModel(opts.fallbackModel)));
package/dist/lsp/index.js CHANGED
@@ -17,7 +17,12 @@ import { getRepoRoot } from '../worktree.js';
17
17
  import { encode, LspDecoder } from './framing.js';
18
18
  import { LspSession, waitForDiagnostics } from './client.js';
19
19
  import { resolveServer } from './servers.js';
20
- const SAFE_ENV_KEYS = ['PATH', 'HOME', 'TMPDIR', 'TEMP', 'LANG', 'LC_ALL', 'USER', 'SHELL', 'TERM', 'NODE_PATH', 'NVM_DIR', 'APPDATA'];
20
+ // รวม Windows-critical (SystemRoot/windir/PATHEXT/ComSpec/USERPROFILE/LOCALAPPDATA/TMP)
21
+ // ถ้าขาด SystemRoot/PATHEXT โปรเซสลูกบน Windows มัก spawn ไม่ขึ้น/หา .cmd ไม่เจอ
22
+ const SAFE_ENV_KEYS = [
23
+ 'PATH', 'HOME', 'TMPDIR', 'TEMP', 'TMP', 'LANG', 'LC_ALL', 'USER', 'SHELL', 'TERM', 'NODE_PATH', 'NVM_DIR',
24
+ 'APPDATA', 'LOCALAPPDATA', 'USERPROFILE', 'SystemRoot', 'SystemDrive', 'windir', 'PATHEXT', 'ComSpec',
25
+ ];
21
26
  function safeEnv() {
22
27
  const out = {};
23
28
  for (const k of SAFE_ENV_KEYS) {
@@ -29,7 +34,8 @@ function safeEnv() {
29
34
  }
30
35
  /** real stdio transport: spawn the server, frame with Content-Length both ways. */
31
36
  function spawnTransport(binPath, args, cwd) {
32
- const proc = spawn(binPath, args, { cwd, stdio: ['pipe', 'pipe', 'pipe'], env: safeEnv() });
37
+ // Windows: LSP bin มัก resolve เป็น .cmd shim spawn ตรงไม่ขึ้น ต้อง shell
38
+ const proc = spawn(binPath, args, { cwd, stdio: ['pipe', 'pipe', 'pipe'], env: safeEnv(), shell: process.platform === 'win32' });
33
39
  const decoder = new LspDecoder();
34
40
  let handler = null;
35
41
  proc.stdout?.on('data', (buf) => {
@@ -82,15 +88,21 @@ export async function diagnose(filePath, opts = {}) {
82
88
  hookExitOnce();
83
89
  let pooled = pool.get(key);
84
90
  if (!pooled) {
91
+ let proc;
85
92
  try {
86
- const { transport, proc } = spawnTransport(resolved.binPath, resolved.def.args, cwd);
87
- const session = new LspSession(transport);
93
+ const t = spawnTransport(resolved.binPath, resolved.def.args, cwd);
94
+ proc = t.proc;
95
+ const session = new LspSession(t.transport);
88
96
  let died = false;
89
97
  proc.on('exit', () => {
90
98
  died = true;
91
99
  pool.delete(key);
92
100
  });
93
- await session.initialize(rootUri);
101
+ // timeout: server ที่ค้าง (ไม่ตอบ initialize) ไม่ทำ diagnose แฮงค์ + reject → catch kill child กัน leak
102
+ await Promise.race([
103
+ session.initialize(rootUri),
104
+ new Promise((_, rej) => setTimeout(() => rej(new Error('initialize timeout (8s)')), 8000)),
105
+ ]);
94
106
  if (died)
95
107
  return { ok: false, reason: `${resolved.def.command} ออกก่อนเริ่มงาน (ติดตั้งครบไหม?)` };
96
108
  pooled = { session, proc, opened: new Map() };
@@ -98,6 +110,12 @@ export async function diagnose(filePath, opts = {}) {
98
110
  }
99
111
  catch (e) {
100
112
  pool.delete(key);
113
+ try {
114
+ proc?.kill(); // init ล้ม/timeout → kill child + ปิด stdio pipes กัน orphan/leak
115
+ }
116
+ catch {
117
+ /* already dead */
118
+ }
101
119
  return { ok: false, reason: `เริ่ม ${resolved.def.command} ไม่สำเร็จ: ${e.message}` };
102
120
  }
103
121
  }