companionbot 0.8.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,8 +20,8 @@ ${conversationText}
20
20
  ];
21
21
  try {
22
22
  // haiku로 빠르게 요약 생성
23
- const summary = await chat(summaryPrompt, undefined, "haiku");
24
- return summary;
23
+ const result = await chat(summaryPrompt, undefined, "haiku");
24
+ return result.text;
25
25
  }
26
26
  catch (error) {
27
27
  console.error("Summary generation error:", error);
@@ -49,7 +49,7 @@ function validateResetToken(chatId, token) {
49
49
  resetTokens.delete(chatId); // 사용 후 삭제
50
50
  return true;
51
51
  }
52
- import { getHistory, clearHistory, getModel, setModel, runWithChatId, } from "../../session/state.js";
52
+ import { getHistory, clearHistory, getModel, setModel, runWithChatId, getPinnedContexts, pinContext, unpinContext, clearPins, getSessionStats, } from "../../session/state.js";
53
53
  import { hasBootstrap, loadRecentMemories, getWorkspacePath, } from "../../workspace/index.js";
54
54
  import { getSecret, setSecret, deleteSecret } from "../../config/secrets.js";
55
55
  import { getReminders } from "../../reminders/index.js";
@@ -81,9 +81,9 @@ export function registerCommands(bot) {
81
81
  content: "[시스템: 사용자가 /start를 눌렀습니다. 온보딩을 시작하세요.]",
82
82
  });
83
83
  try {
84
- const response = await chat(history, systemPrompt, modelId);
85
- history.push({ role: "assistant", content: response });
86
- await ctx.reply(response);
84
+ const result = await chat(history, systemPrompt, modelId);
85
+ history.push({ role: "assistant", content: result.text });
86
+ await ctx.reply(result.text);
87
87
  }
88
88
  catch (error) {
89
89
  console.error("Bootstrap start error:", error);
@@ -627,4 +627,91 @@ export function registerCommands(bot) {
627
627
  `❌ 에러: ${status.errorCount}개\n` +
628
628
  `🔋 상태: ${status.isHealthy ? "정상 ✅" : "점검 필요 ⚠️"}`);
629
629
  });
630
+ // /pin 명령어 - 중요 맥락 핀하기
631
+ bot.command("pin", async (ctx) => {
632
+ const chatId = ctx.chat.id;
633
+ const text = ctx.message?.text?.split(" ").slice(1).join(" ");
634
+ if (!text) {
635
+ await ctx.reply("📌 핀 사용법\n\n" +
636
+ "중요한 정보를 핀해서 대화가 길어져도 기억하게 해요.\n\n" +
637
+ "예시:\n" +
638
+ "/pin 내 이름은 민수야\n" +
639
+ "/pin 나는 채식주의자야\n" +
640
+ "/pin 다음주 화요일 치과 예약\n\n" +
641
+ "또는 대화 중에 \"기억해: ...\" 라고 하면 자동으로 핀됩니다.");
642
+ return;
643
+ }
644
+ const success = pinContext(chatId, text, "user");
645
+ if (success) {
646
+ await ctx.reply(`📌 핀됨: "${text.slice(0, 50)}${text.length > 50 ? "..." : ""}"\n\n대화가 길어져도 이 정보는 항상 기억할게요!`);
647
+ }
648
+ else {
649
+ await ctx.reply("핀 한도(~5000 토큰)에 도달했어요. /pins 에서 일부를 삭제해주세요.");
650
+ }
651
+ });
652
+ // /pins 명령어 - 핀 목록 보기
653
+ bot.command("pins", async (ctx) => {
654
+ const chatId = ctx.chat.id;
655
+ const pins = getPinnedContexts(chatId);
656
+ if (pins.length === 0) {
657
+ await ctx.reply("📌 핀된 맥락이 없어요.\n\n" +
658
+ "/pin [내용] 으로 중요한 정보를 핀해보세요.");
659
+ return;
660
+ }
661
+ let message = "📌 핀된 맥락\n\n";
662
+ pins.forEach((pin, i) => {
663
+ const source = pin.source === "auto" ? "🤖" : "👤";
664
+ const time = new Date(pin.createdAt).toLocaleDateString("ko-KR");
665
+ message += `${i + 1}. ${source} ${pin.text.slice(0, 60)}${pin.text.length > 60 ? "..." : ""}\n 📅 ${time}\n\n`;
666
+ });
667
+ message += "삭제: /unpin [번호] 또는 /clear_pins (전체)";
668
+ await ctx.reply(message);
669
+ });
670
+ // /unpin 명령어 - 핀 삭제
671
+ bot.command("unpin", async (ctx) => {
672
+ const chatId = ctx.chat.id;
673
+ const arg = ctx.message?.text?.split(" ")[1];
674
+ if (!arg) {
675
+ await ctx.reply("사용법: /unpin [번호]\n\n/pins 에서 번호를 확인하세요.");
676
+ return;
677
+ }
678
+ const index = parseInt(arg) - 1; // 1-based to 0-based
679
+ const pins = getPinnedContexts(chatId);
680
+ if (isNaN(index) || index < 0 || index >= pins.length) {
681
+ await ctx.reply(`유효하지 않은 번호예요. 1-${pins.length} 사이로 입력해주세요.`);
682
+ return;
683
+ }
684
+ const removed = pins[index].text;
685
+ const success = unpinContext(chatId, index);
686
+ if (success) {
687
+ await ctx.reply(`📌 핀 삭제됨: "${removed.slice(0, 40)}..."`);
688
+ }
689
+ else {
690
+ await ctx.reply("핀 삭제에 실패했어요.");
691
+ }
692
+ });
693
+ // /clear_pins 명령어 - 모든 핀 삭제
694
+ bot.command("clear_pins", async (ctx) => {
695
+ const chatId = ctx.chat.id;
696
+ const pins = getPinnedContexts(chatId);
697
+ if (pins.length === 0) {
698
+ await ctx.reply("삭제할 핀이 없어요.");
699
+ return;
700
+ }
701
+ clearPins(chatId);
702
+ await ctx.reply(`📌 ${pins.length}개 핀이 모두 삭제되었습니다.`);
703
+ });
704
+ // /context 명령어 - 현재 맥락 상태 확인
705
+ bot.command("context", async (ctx) => {
706
+ const chatId = ctx.chat.id;
707
+ const stats = getSessionStats(chatId);
708
+ await ctx.reply(`📊 맥락 상태\n\n` +
709
+ `💬 히스토리: ${stats.historyLength}개 메시지 (~${stats.historyTokens} 토큰)\n` +
710
+ `📌 핀: ${stats.pinnedCount}개 (~${stats.pinnedTokens} 토큰)\n` +
711
+ `📜 요약: ${stats.summaryCount}개\n\n` +
712
+ `명령어:\n` +
713
+ `/pins - 핀 목록\n` +
714
+ `/compact - 히스토리 압축\n` +
715
+ `/clear - 히스토리 초기화 (핀 유지)`);
716
+ });
630
717
  }
@@ -1,8 +1,8 @@
1
1
  import { chat, chatSmart } from "../../ai/claude.js";
2
2
  import { recordActivity, recordError } from "../../health/index.js";
3
- import { getHistory, getModel, runWithChatId, trimHistoryByTokens, } from "../../session/state.js";
3
+ import { getHistory, getModel, runWithChatId, trimHistoryByTokens, smartTrimHistory, detectImportantContext, pinContext, } from "../../session/state.js";
4
4
  import { updateLastMessageTime } from "../../heartbeat/index.js";
5
- import { extractUrls, fetchWebContent, buildSystemPrompt, } from "../utils/index.js";
5
+ import { extractUrls, fetchWebContent, formatUrlContent, buildSystemPrompt, } from "../utils/index.js";
6
6
  import { estimateMessagesTokens } from "../../utils/tokens.js";
7
7
  const MAX_CONTEXT_TOKENS = 100000; // Claude 컨텍스트
8
8
  const COMPACTION_THRESHOLD = 0.35; // 35% (35,000 토큰) - MAX_HISTORY_TOKENS(50k)보다 먼저 트리거되도록
@@ -23,11 +23,11 @@ async function autoCompactIfNeeded(ctx, history) {
23
23
  oldMessages
24
24
  .map((m) => `${m.role}: ${typeof m.content === "string" ? m.content : "[media]"}`)
25
25
  .join("\n");
26
- const summary = await chat([{ role: "user", content: summaryPrompt }], "", "haiku");
26
+ const summaryResult = await chat([{ role: "user", content: summaryPrompt }], "", "haiku");
27
27
  // 히스토리 교체
28
28
  const recentMessages = history.slice(-4);
29
29
  history.splice(0, history.length);
30
- history.push({ role: "user", content: `[이전 대화 요약]\n${summary}` });
30
+ history.push({ role: "user", content: `[이전 대화 요약]\n${summaryResult.text}` });
31
31
  history.push(...recentMessages);
32
32
  const newTokens = estimateMessagesTokens(history);
33
33
  await ctx.reply(`📦 자동 정리: ${tokens} → ${newTokens} 토큰`);
@@ -149,10 +149,18 @@ export function registerMessageHandlers(bot) {
149
149
  try {
150
150
  const systemPrompt = await buildSystemPrompt(modelId, history);
151
151
  const result = await chat(history, systemPrompt, modelId);
152
- history.push({ role: "assistant", content: result });
152
+ // 도구 사용 정보를 포함한 응답 기록
153
+ let assistantContent = result.text;
154
+ if (result.toolsUsed.length > 0) {
155
+ const toolsSummary = result.toolsUsed
156
+ .map(t => `[${t.name}] ${t.output.slice(0, 100)}...`)
157
+ .join("\n");
158
+ assistantContent = `[도구 사용: ${result.toolsUsed.map(t => t.name).join(", ")}]\n${toolsSummary}\n\n---\n${result.text}`;
159
+ }
160
+ history.push({ role: "assistant", content: assistantContent });
153
161
  // 토큰 기반 히스토리 트리밍
154
162
  trimHistoryByTokens(history);
155
- await ctx.reply(result);
163
+ await ctx.reply(result.text);
156
164
  }
157
165
  catch (innerError) {
158
166
  // 에러 발생해도 사용자 메시지는 보존 (대화 컨텍스트 유지)
@@ -206,35 +214,69 @@ export function registerMessageHandlers(bot) {
206
214
  updateLastMessageTime(chatId);
207
215
  const history = getHistory(chatId);
208
216
  const modelId = getModel(chatId);
217
+ // 중요 맥락 자동 감지 및 핀
218
+ const importantContext = detectImportantContext(userMessage);
219
+ if (importantContext) {
220
+ pinContext(chatId, importantContext, "auto");
221
+ console.log(`[AutoPin] chatId=${chatId}: ${importantContext.slice(0, 50)}...`);
222
+ }
209
223
  await ctx.replyWithChatAction("typing");
210
224
  // URL 감지 및 내용 가져오기 (병렬 처리)
211
225
  const urls = extractUrls(userMessage);
212
- let enrichedMessage = userMessage;
226
+ let messageForHistory = userMessage;
227
+ let urlContextForApi = ""; // 현재 요청에만 주입될 URL 내용
213
228
  if (urls.length > 0) {
214
229
  const urlsToFetch = urls.slice(0, 3); // 최대 3개 URL
215
230
  const contents = await Promise.all(urlsToFetch.map((url) => fetchWebContent(url)));
216
- const webContents = contents
217
- .map((content, index) => {
231
+ const urlRefs = [];
232
+ for (let i = 0; i < contents.length; i++) {
233
+ const content = contents[i];
218
234
  if (!content)
219
- return null;
220
- return `\n\n---\n📎 Link: ${urlsToFetch[index]}\n📌 Title: ${content.title}\n📄 Content:\n${content.content}\n---`;
221
- })
222
- .filter((item) => item !== null);
223
- if (webContents.length > 0) {
224
- enrichedMessage = userMessage + webContents.join("\n");
235
+ continue;
236
+ const formatted = formatUrlContent(urlsToFetch[i], content);
237
+ urlRefs.push(formatted.forHistory);
238
+ urlContextForApi += formatted.forContext;
239
+ }
240
+ // 히스토리에는 간략한 링크 참조만 저장
241
+ if (urlRefs.length > 0) {
242
+ messageForHistory = userMessage + "\n\n" + urlRefs.join("\n");
225
243
  }
226
244
  }
227
- // 사용자 메시지 추가 (URL 내용 포함)
228
- history.push({ role: "user", content: enrichedMessage });
245
+ // 히스토리에는 간략 버전 저장
246
+ history.push({ role: "user", content: messageForHistory });
229
247
  try {
230
248
  const systemPrompt = await buildSystemPrompt(modelId, history);
249
+ // API 호출용 메시지 준비 (URL 전체 내용 포함)
250
+ const messagesForApi = [...history];
251
+ if (urlContextForApi) {
252
+ // 마지막 user 메시지에 URL 내용 추가 (API 호출 시에만)
253
+ const lastIdx = messagesForApi.length - 1;
254
+ const lastMsg = messagesForApi[lastIdx];
255
+ if (typeof lastMsg.content === "string") {
256
+ messagesForApi[lastIdx] = {
257
+ ...lastMsg,
258
+ content: lastMsg.content + urlContextForApi
259
+ };
260
+ }
261
+ }
231
262
  // 스트리밍 응답 사용 (실시간 업데이트)
232
- const response = await sendStreamingResponse(ctx, history, systemPrompt, modelId);
263
+ const response = await sendStreamingResponse(ctx, messagesForApi, // URL 내용이 포함된 버전
264
+ systemPrompt, modelId);
233
265
  history.push({ role: "assistant", content: response });
234
- // 토큰 기반 히스토리 트리밍
235
- trimHistoryByTokens(history);
236
- // 자동 compaction 체크
237
- await autoCompactIfNeeded(ctx, history);
266
+ // 스마트 트리밍 (요약 포함) - autoCompactIfNeeded 대체
267
+ const summarizeFn = async (messages) => {
268
+ const summaryPrompt = "다음 대화를 핵심만 3-4문장으로 요약해. 중요한 정보(이름, 선호도, 약속 등)는 반드시 포함:\n\n" +
269
+ messages
270
+ .map((m) => `${m.role}: ${typeof m.content === "string" ? m.content : "[media]"}`)
271
+ .join("\n");
272
+ const result = await chat([{ role: "user", content: summaryPrompt }], "", "haiku");
273
+ return result.text;
274
+ };
275
+ const wasSummarized = await smartTrimHistory(chatId, summarizeFn);
276
+ if (!wasSummarized) {
277
+ // 요약 안 됐으면 기본 트리밍
278
+ trimHistoryByTokens(history);
279
+ }
238
280
  }
239
281
  catch (error) {
240
282
  recordError();
@@ -1,5 +1,5 @@
1
1
  // URL utilities
2
- export { extractUrls, fetchWebContent, isSafeUrl } from "./url.js";
2
+ export { extractUrls, fetchWebContent, isSafeUrl, formatUrlContent, clearUrlCache } from "./url.js";
3
3
  // Prompt utilities
4
4
  export { buildSystemPrompt, extractName } from "./prompt.js";
5
5
  // Cache utilities
@@ -4,9 +4,44 @@ import { getToolsDescription } from "../../tools/index.js";
4
4
  import { getWorkspace } from "./cache.js";
5
5
  import { embed } from "../../memory/embeddings.js";
6
6
  import { search } from "../../memory/vectorStore.js";
7
- /**
8
- * identity.md에서 이름을 추출합니다.
9
- */
7
+ import { buildContextForPrompt, getCurrentChatId } from "../../session/state.js";
8
+ import * as os from "os";
9
+ function getRuntimeInfo(modelId) {
10
+ const model = MODELS[modelId];
11
+ return {
12
+ host: os.hostname(),
13
+ os: `${os.type()} ${os.release()}`,
14
+ arch: os.arch(),
15
+ nodeVersion: process.version,
16
+ model: model.name,
17
+ channel: "telegram",
18
+ capabilities: ["markdown", "inline_keyboard", "reactions", "voice_messages"],
19
+ };
20
+ }
21
+ function buildRuntimeLine(runtime) {
22
+ return `Runtime: host=${runtime.host} | os=${runtime.os} (${runtime.arch}) | node=${runtime.nodeVersion} | model=${runtime.model} | channel=${runtime.channel} | capabilities=${runtime.capabilities.join(",")}`;
23
+ }
24
+ function getKoreanDateTime() {
25
+ const now = new Date();
26
+ const timezone = "Asia/Seoul";
27
+ const formatter = new Intl.DateTimeFormat("ko-KR", {
28
+ timeZone: timezone,
29
+ year: "numeric",
30
+ month: "long",
31
+ day: "numeric",
32
+ weekday: "long",
33
+ hour: "numeric",
34
+ minute: "2-digit",
35
+ second: "2-digit",
36
+ hour12: true,
37
+ });
38
+ return {
39
+ formatted: formatter.format(now),
40
+ timezone: `${timezone} (GMT+9)`,
41
+ iso: now.toISOString(),
42
+ };
43
+ }
44
+ // ============== 이름 추출 ==============
10
45
  export function extractName(identityContent) {
11
46
  if (!identityContent)
12
47
  return null;
@@ -19,9 +54,7 @@ export function extractName(identityContent) {
19
54
  }
20
55
  return null;
21
56
  }
22
- /**
23
- * 최근 대화에서 검색 쿼리 컨텍스트를 추출합니다.
24
- */
57
+ // ============== 메모리 검색 ==============
25
58
  function extractSearchContext(history) {
26
59
  const recent = history.slice(-3);
27
60
  return recent
@@ -30,110 +63,233 @@ function extractSearchContext(history) {
30
63
  .join(" ")
31
64
  .slice(0, 500);
32
65
  }
33
- /**
34
- * 대화 컨텍스트와 관련된 메모리를 검색합니다.
35
- */
36
66
  async function getRelevantMemories(history) {
37
67
  try {
38
68
  const context = extractSearchContext(history);
39
69
  if (!context.trim())
40
70
  return "";
41
71
  const queryEmbedding = await embed(context);
42
- const results = await search(queryEmbedding, 3, 0.4); // 상위 3개, 유사도 0.4 이상
72
+ const results = await search(queryEmbedding, 3, 0.4);
43
73
  if (results.length === 0)
44
74
  return "";
45
- return ("\n\n## 관련 기억\n" +
46
- results
47
- .map((r) => `- (${r.source}): ${r.text.slice(0, 200)}${r.text.length > 200 ? "..." : ""}`)
48
- .join("\n"));
75
+ return results
76
+ .map((r) => `- (${r.source}): ${r.text.slice(0, 200)}${r.text.length > 200 ? "..." : ""}`)
77
+ .join("\n");
49
78
  }
50
79
  catch {
51
80
  return "";
52
81
  }
53
82
  }
54
- /**
55
- * 현재 날짜/시간을 한국어 포맷으로 반환합니다.
56
- */
57
- function getKoreanDateTime() {
58
- const now = new Date();
59
- const timezone = "Asia/Seoul";
60
- const formatter = new Intl.DateTimeFormat("ko-KR", {
61
- timeZone: timezone,
62
- year: "numeric",
63
- month: "long",
64
- day: "numeric",
65
- weekday: "short",
66
- hour: "numeric",
67
- minute: "2-digit",
68
- hour12: true,
69
- });
70
- const formatted = formatter.format(now);
71
- return {
72
- formatted,
73
- timezone: `${timezone} (GMT+9)`,
74
- };
83
+ // ============== 도구 설명 섹션 ==============
84
+ const TOOL_SUMMARIES = {
85
+ read_file: "Read file contents",
86
+ write_file: "Create or overwrite files",
87
+ edit_file: "Make precise edits to files",
88
+ list_directory: "List directory contents",
89
+ run_command: "Run shell commands (supports background mode)",
90
+ list_sessions: "List background command sessions",
91
+ get_session_log: "Get logs from a background session",
92
+ kill_session: "Terminate a background session",
93
+ web_search: "Search the web (Brave API)",
94
+ web_fetch: "Fetch and extract readable content from a URL",
95
+ set_reminder: "Set a reminder (cron-based)",
96
+ list_reminders: "List active reminders",
97
+ delete_reminder: "Delete a reminder",
98
+ cron_add: "Add a cron job",
99
+ cron_list: "List cron jobs",
100
+ cron_remove: "Remove a cron job",
101
+ cron_toggle: "Enable/disable a cron job",
102
+ cron_run: "Run a cron job immediately",
103
+ calendar_today: "Get today's calendar events",
104
+ calendar_list: "List calendar events in a date range",
105
+ calendar_add: "Add a calendar event",
106
+ calendar_delete: "Delete a calendar event",
107
+ heartbeat_set: "Configure heartbeat polling",
108
+ heartbeat_get: "Get heartbeat configuration",
109
+ heartbeat_disable: "Disable heartbeat",
110
+ heartbeat_run: "Run heartbeat check now",
111
+ briefing_set: "Configure daily briefing",
112
+ briefing_get: "Get briefing configuration",
113
+ briefing_disable: "Disable briefing",
114
+ briefing_send: "Send briefing now",
115
+ save_persona: "Save persona during onboarding",
116
+ save_memory: "Append to memory file",
117
+ spawn_agent: "Spawn a background agent for complex tasks",
118
+ list_agents: "List running background agents",
119
+ cancel_agent: "Cancel a background agent",
120
+ memory_search: "Search memories by semantic similarity",
121
+ memory_reindex: "Reindex all memory files",
122
+ };
123
+ function buildToolAvailabilitySection() {
124
+ const lines = [
125
+ "## Tooling",
126
+ "Tool availability:",
127
+ "",
128
+ ];
129
+ for (const [name, description] of Object.entries(TOOL_SUMMARIES)) {
130
+ lines.push(`- ${name}: ${description}`);
131
+ }
132
+ return lines.join("\n");
133
+ }
134
+ // ============== 메시지 가이드 섹션 ==============
135
+ function buildMessagingSection() {
136
+ return `## Messaging
137
+ - Reply naturally in the conversation; your response is automatically sent to Telegram.
138
+ - Use \`spawn_agent\` for complex, long-running tasks that need background processing.
139
+ - Agent results are automatically reported back to the chat.
140
+
141
+ ## Tool Call Style
142
+ Default: do not narrate routine, low-risk tool calls (just call the tool).
143
+ Narrate only when it helps: multi-step work, complex problems, sensitive actions, or when the user explicitly asks.
144
+ Keep narration brief and value-dense; avoid repeating obvious steps.`;
145
+ }
146
+ // ============== 하트비트/침묵 응답 섹션 ==============
147
+ function buildHeartbeatSection() {
148
+ return `## Heartbeats
149
+ When you receive a heartbeat poll, and there is nothing that needs attention, reply exactly:
150
+ HEARTBEAT_OK
151
+
152
+ If something needs attention, reply with the alert text instead (do NOT include "HEARTBEAT_OK").
153
+
154
+ Things to check during heartbeats (rotate through these):
155
+ - Upcoming reminders or calendar events
156
+ - Pending tasks or follow-ups
157
+ - Anything noteworthy to proactively mention`;
75
158
  }
76
- /**
77
- * 시스템 프롬프트를 동적으로 생성합니다.
78
- * @param modelId 사용할 모델 ID
79
- * @param history 대화 히스토리 (관련 메모리 검색에 사용)
80
- */
159
+ // ============== 메인 빌드 함수 ==============
81
160
  export async function buildSystemPrompt(modelId, history) {
82
- const model = MODELS[modelId];
83
161
  const workspace = await getWorkspace();
84
- const parts = [];
85
- // 기본 정보
86
- parts.push(`You are a personal AI companion running on ${model.name}.`);
87
- parts.push(`Workspace: ${getWorkspacePath()}`);
88
- // 런타임 정보 (날짜/시간)
162
+ const runtime = getRuntimeInfo(modelId);
89
163
  const dateTime = getKoreanDateTime();
164
+ const parts = [];
165
+ // ===== 1. Core Identity =====
166
+ parts.push("You are a personal AI companion running on CompanionBot.");
167
+ parts.push("");
168
+ // ===== 2. Tooling Section =====
169
+ parts.push(buildToolAvailabilitySection());
170
+ parts.push("");
171
+ // ===== 3. Messaging & Tool Style =====
172
+ parts.push(buildMessagingSection());
173
+ parts.push("");
174
+ // ===== 4. Workspace =====
175
+ parts.push("## Workspace");
176
+ parts.push(`Your working directory is: ${getWorkspacePath()}`);
177
+ parts.push("Treat this directory as the single global workspace for file operations unless explicitly instructed otherwise.");
178
+ parts.push("");
179
+ // ===== 5. Current Date & Time =====
180
+ parts.push("## Current Date & Time");
181
+ parts.push(`Time zone: ${dateTime.timezone}`);
90
182
  parts.push(`Current time: ${dateTime.formatted}`);
91
- parts.push(`Timezone: ${dateTime.timezone}`);
92
- // 채널/플랫폼 정보
93
- parts.push(`Runtime: channel=telegram | capabilities=markdown,inline_keyboard,reactions | version=0.4.x`);
94
- // BOOTSTRAP 모드인 경우
183
+ parts.push("");
184
+ // ===== 6. Heartbeat Guide =====
185
+ parts.push(buildHeartbeatSection());
186
+ parts.push("");
187
+ // ===== 7. Runtime =====
188
+ parts.push("## Runtime");
189
+ parts.push(buildRuntimeLine(runtime));
190
+ parts.push("");
191
+ // ===== 8. Project Context (Workspace Files) =====
192
+ parts.push("# Project Context");
193
+ parts.push("");
194
+ parts.push("The following workspace files have been loaded:");
195
+ parts.push("");
196
+ // BOOTSTRAP 모드
95
197
  if (workspace.bootstrap) {
96
- parts.push("---");
97
- parts.push("# 온보딩 모드 활성화");
198
+ parts.push("## BOOTSTRAP.md (Onboarding Mode)");
199
+ parts.push("");
98
200
  parts.push(workspace.bootstrap);
201
+ parts.push("");
99
202
  parts.push("---");
100
- parts.push(`온보딩 완료 save_persona 도구를 사용하여 설정을 저장하세요.`);
203
+ parts.push("Complete onboarding, then use `save_persona` tool to save settings.");
204
+ parts.push("");
101
205
  }
102
206
  else {
103
- // 일반 모드: 워크스페이스 파일들 로드
207
+ // 일반 모드: 워크스페이스 파일들
104
208
  if (workspace.identity) {
105
- parts.push("---");
209
+ parts.push("## IDENTITY.md");
210
+ parts.push("");
106
211
  parts.push(workspace.identity);
212
+ parts.push("");
107
213
  }
108
214
  if (workspace.soul) {
109
- parts.push("---");
215
+ parts.push("## SOUL.md");
216
+ parts.push("");
217
+ parts.push("If SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies; follow its guidance.");
218
+ parts.push("");
110
219
  parts.push(workspace.soul);
220
+ parts.push("");
111
221
  }
112
222
  if (workspace.user) {
113
- parts.push("---");
223
+ parts.push("## USER.md");
224
+ parts.push("");
114
225
  parts.push(workspace.user);
226
+ parts.push("");
115
227
  }
116
228
  if (workspace.agents) {
117
- parts.push("---");
229
+ parts.push("## AGENTS.md");
230
+ parts.push("");
118
231
  parts.push(workspace.agents);
232
+ parts.push("");
233
+ }
234
+ // TOOLS.md - 도구 사용 로컬 설정/노트
235
+ if (workspace.tools) {
236
+ parts.push("## TOOLS.md");
237
+ parts.push("");
238
+ parts.push("Local notes for tool usage (camera names, SSH details, voice preferences, etc.)");
239
+ parts.push("");
240
+ parts.push(workspace.tools);
241
+ parts.push("");
242
+ }
243
+ // 핀된 맥락 (히스토리 트리밍과 무관하게 유지됨)
244
+ const chatId = getCurrentChatId();
245
+ if (chatId) {
246
+ const pinnedContext = buildContextForPrompt(chatId);
247
+ if (pinnedContext) {
248
+ parts.push("## 📌 Pinned Context (always remember)");
249
+ parts.push("");
250
+ parts.push(pinnedContext);
251
+ parts.push("");
252
+ }
253
+ }
254
+ // 최근 Daily Memory (오늘/어제 - 벡터 검색 없이 직접 포함)
255
+ if (workspace.recentDaily) {
256
+ parts.push("## Recent Daily Memory (Today/Yesterday)");
257
+ parts.push("");
258
+ parts.push("These are your recent conversation logs. Use them for context continuity.");
259
+ parts.push("");
260
+ parts.push(workspace.recentDaily);
261
+ parts.push("");
119
262
  }
120
- // 관련 기억 로드 (대화 컨텍스트 기반)
263
+ // 관련 기억 (벡터 검색 - 더 오래된 기록에서)
121
264
  if (history && history.length > 0) {
122
265
  const relevantMemories = await getRelevantMemories(history);
123
266
  if (relevantMemories) {
124
- parts.push("---");
125
- parts.push("# 관련 기억");
267
+ parts.push("## Relevant Memories (vector search from older records)");
268
+ parts.push("");
126
269
  parts.push(relevantMemories);
270
+ parts.push("");
127
271
  }
128
272
  }
273
+ // 장기 기억
129
274
  if (workspace.memory) {
130
- parts.push("---");
131
- parts.push("# 장기 기억");
275
+ parts.push("## MEMORY.md (Long-term Memory)");
276
+ parts.push("");
277
+ parts.push("Curated important information. Update this when you learn significant things about the user.");
278
+ parts.push("");
132
279
  parts.push(workspace.memory);
280
+ parts.push("");
133
281
  }
134
282
  }
135
- // 도구 설명
283
+ // 잘린 파일 경고
284
+ if (workspace.truncated && workspace.truncated.length > 0) {
285
+ parts.push("");
286
+ parts.push(`⚠️ Note: These files were truncated due to size limits: ${workspace.truncated.join(", ")}`);
287
+ parts.push("Use read_file tool to see full contents if needed.");
288
+ parts.push("");
289
+ }
290
+ // ===== 9. Tools Schema (for Claude) =====
136
291
  parts.push("---");
292
+ parts.push("");
137
293
  parts.push(getToolsDescription(modelId));
138
- return parts.join("\n\n");
294
+ return parts.join("\n");
139
295
  }