@offerpilot/axiomruntime 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +185 -0
  2. package/dist/cli/commands/add.js +29 -0
  3. package/dist/cli/commands/context.js +29 -0
  4. package/dist/cli/commands/doctor.js +62 -0
  5. package/dist/cli/commands/edit.js +85 -0
  6. package/dist/cli/commands/help.js +16 -0
  7. package/dist/cli/commands/list.js +25 -0
  8. package/dist/cli/commands/log.js +63 -0
  9. package/dist/cli/commands/memory.js +241 -0
  10. package/dist/cli/commands/report.js +35 -0
  11. package/dist/cli/commands/session.js +74 -0
  12. package/dist/cli/commands/setup.js +86 -0
  13. package/dist/cli/commands/status.js +50 -0
  14. package/dist/cli/commands/telegram.js +701 -0
  15. package/dist/cli/commands/use.js +108 -0
  16. package/dist/cli/commands/version.js +12 -0
  17. package/dist/cli/index.js +276 -0
  18. package/dist/cli/output/table.js +22 -0
  19. package/dist/cli/prompts/prompt.js +37 -0
  20. package/dist/cli/registry.js +16 -0
  21. package/dist/core/config/cache-store.js +193 -0
  22. package/dist/core/config/json-store.js +114 -0
  23. package/dist/core/config/paths.js +85 -0
  24. package/dist/core/config/providers-store.js +89 -0
  25. package/dist/core/config/schema.js +60 -0
  26. package/dist/core/config/session-store.js +30 -0
  27. package/dist/core/config/usage-store.js +18 -0
  28. package/dist/core/context/context-service.js +186 -0
  29. package/dist/core/integrations/integration-state.js +105 -0
  30. package/dist/core/logs/log-service.js +56 -0
  31. package/dist/core/memory/embedding-check.js +121 -0
  32. package/dist/core/memory/memory-config.js +122 -0
  33. package/dist/core/models/model-discovery.js +430 -0
  34. package/dist/core/models/model-filter.js +13 -0
  35. package/dist/core/providers/provider-service.js +212 -0
  36. package/dist/core/reports/report-service.js +166 -0
  37. package/dist/core/runner/command-resolver.js +60 -0
  38. package/dist/core/runner/engine-registry.js +93 -0
  39. package/dist/core/runner/fallback.js +114 -0
  40. package/dist/core/runner/openai-usage-http.js +82 -0
  41. package/dist/core/runner/openai-usage-proxy.js +1 -0
  42. package/dist/core/runner/openai-usage-recording.js +172 -0
  43. package/dist/core/runner/openai-usage-responses.js +469 -0
  44. package/dist/core/runner/openai-usage-server.js +319 -0
  45. package/dist/core/runner/openai-usage-types.js +1 -0
  46. package/dist/core/runner/tool-runner.js +138 -0
  47. package/dist/core/sessions/session-service.js +47 -0
  48. package/dist/core/status/doctor-service.js +391 -0
  49. package/dist/core/status/status-service.js +60 -0
  50. package/dist/core/types.js +1 -0
  51. package/dist/core/usage/pricing.js +113 -0
  52. package/dist/core/usage/usage-service.js +30 -0
  53. package/dist/core/utils/is-record.js +3 -0
  54. package/dist/server/index.js +28 -0
  55. package/dist/server/runtime-server.js +430 -0
  56. package/dist/telegram/bot-registry.js +80 -0
  57. package/dist/telegram/bot.js +128 -0
  58. package/dist/telegram/config.js +235 -0
  59. package/dist/telegram/engine/claude-engine.js +240 -0
  60. package/dist/telegram/engine/codex-engine.js +437 -0
  61. package/dist/telegram/engine/engine-utils.js +67 -0
  62. package/dist/telegram/engine/process-utils.js +132 -0
  63. package/dist/telegram/engine/registry.js +31 -0
  64. package/dist/telegram/engine/types.js +1 -0
  65. package/dist/telegram/handler-registry.js +28 -0
  66. package/dist/telegram/handlers/callback.js +311 -0
  67. package/dist/telegram/handlers/command.js +272 -0
  68. package/dist/telegram/handlers/document.js +108 -0
  69. package/dist/telegram/handlers/memory.js +305 -0
  70. package/dist/telegram/handlers/message.js +701 -0
  71. package/dist/telegram/handlers/provider.js +332 -0
  72. package/dist/telegram/handlers/setup.js +527 -0
  73. package/dist/telegram/handlers/usage.js +124 -0
  74. package/dist/telegram/index.js +93 -0
  75. package/dist/telegram/interaction/approval.js +108 -0
  76. package/dist/telegram/interaction/command-menu.js +253 -0
  77. package/dist/telegram/interaction/formatter.js +487 -0
  78. package/dist/telegram/interaction/keyboards.js +145 -0
  79. package/dist/telegram/interaction/progress-reporter.js +160 -0
  80. package/dist/telegram/interaction/prompt-middleware.js +168 -0
  81. package/dist/telegram/interaction/result-store.js +41 -0
  82. package/dist/telegram/interaction/token-budget.js +21 -0
  83. package/dist/telegram/interaction/tool-name.js +41 -0
  84. package/dist/telegram/lifecycle-registry.js +47 -0
  85. package/dist/telegram/log.js +46 -0
  86. package/dist/telegram/memory/memory-inject.js +52 -0
  87. package/dist/telegram/memory/memory-service.js +413 -0
  88. package/dist/telegram/memory/memory-store.js +216 -0
  89. package/dist/telegram/memory/types.js +1 -0
  90. package/dist/telegram/network-retry.js +22 -0
  91. package/dist/telegram/network.js +53 -0
  92. package/dist/telegram/session/manager.js +229 -0
  93. package/dist/telegram/session/store.js +363 -0
  94. package/dist/telegram/session/types.js +1 -0
  95. package/dist/telegram/supervisor.js +57 -0
  96. package/dist/telegram/templates/messages.js +1 -0
  97. package/docs/README.md +98 -0
  98. package/docs/USAGE.html +853 -0
  99. package/package.json +57 -0
@@ -0,0 +1,160 @@
1
+ import { withTelegramNetworkRetry } from "../network-retry.js";
2
+ import { escapeHtml, Formatter, truncateText } from "./formatter.js";
3
+ const MAX_TIMELINE_ENTRIES = 8;
4
+ export class ProgressReporter {
5
+ api;
6
+ chatId;
7
+ messageId;
8
+ pending;
9
+ timer;
10
+ finalized = false;
11
+ lastSignature = "";
12
+ toolCount = 0;
13
+ throttleMs;
14
+ silent;
15
+ timeline = [];
16
+ lastTimelinePhase;
17
+ responsePreview = "";
18
+ startTime = Date.now();
19
+ constructor(api, chatId, options = {}) {
20
+ this.api = api;
21
+ this.chatId = chatId;
22
+ this.silent = options.silent ?? false;
23
+ this.throttleMs = options.throttleMs ?? 1500;
24
+ }
25
+ async update(update) {
26
+ if (this.silent || this.finalized || update.phase === "done")
27
+ return;
28
+ if (update.phase === "tool_calling")
29
+ this.toolCount += 1;
30
+ this.pending = update;
31
+ if (this.timer)
32
+ return;
33
+ this.timer = setTimeout(() => {
34
+ this.timer = undefined;
35
+ void this.flush();
36
+ }, this.throttleMs);
37
+ this.timer.unref?.();
38
+ }
39
+ async finalize() {
40
+ if (this.finalized)
41
+ return;
42
+ this.finalized = true;
43
+ this.clearPending();
44
+ this.pending = undefined;
45
+ if (!this.messageId || this.silent)
46
+ return;
47
+ try {
48
+ await this.api.deleteMessage(this.chatId, this.messageId);
49
+ }
50
+ catch {
51
+ // Progress messages are best-effort and must not affect task completion.
52
+ }
53
+ }
54
+ async replaceWithHtml(html, fallbackText, extra) {
55
+ if (this.silent || this.finalized)
56
+ return false;
57
+ this.clearPending();
58
+ this.pending = undefined;
59
+ if (!this.messageId)
60
+ return false;
61
+ try {
62
+ await withTelegramNetworkRetry(() => this.api.editMessageText(this.chatId, this.messageId, html, { parse_mode: "HTML", ...extra }));
63
+ this.finalized = true;
64
+ return true;
65
+ }
66
+ catch (error) {
67
+ this.handleTelegramError(error);
68
+ }
69
+ try {
70
+ await withTelegramNetworkRetry(() => this.api.editMessageText(this.chatId, this.messageId, fallbackText || "(empty)", extra ? { ...extra } : undefined));
71
+ this.finalized = true;
72
+ return true;
73
+ }
74
+ catch (error) {
75
+ this.handleTelegramError(error);
76
+ return false;
77
+ }
78
+ }
79
+ async flush() {
80
+ if (this.silent || this.finalized || !this.pending)
81
+ return;
82
+ const update = this.pending;
83
+ this.pending = undefined;
84
+ const signature = buildProgressSignature(update, this.toolCount);
85
+ if (signature === this.lastSignature)
86
+ return;
87
+ if (update.responseText?.trim()) {
88
+ this.responsePreview = update.responseText.trim();
89
+ }
90
+ if (this.lastTimelinePhase !== update.phase || update.phase !== "writing") {
91
+ const entry = Formatter.progress(update, { toolCount: this.toolCount });
92
+ this.appendTimeline(entry);
93
+ this.lastTimelinePhase = update.phase;
94
+ }
95
+ const preview = this.responsePreview
96
+ ? `<blockquote>${escapeHtml(truncateText(this.responsePreview, 1200))}</blockquote>`
97
+ : "";
98
+ const text = [Formatter.progressTimeline(this.getDisplayEntries()), preview].filter(Boolean).join("\n\n");
99
+ try {
100
+ if (!this.messageId) {
101
+ const message = await withTelegramNetworkRetry(() => this.api.sendMessage(this.chatId, text, { parse_mode: "HTML" }));
102
+ this.messageId = readMessageId(message);
103
+ }
104
+ else {
105
+ await withTelegramNetworkRetry(() => this.api.editMessageText(this.chatId, this.messageId, text, { parse_mode: "HTML" }));
106
+ }
107
+ this.lastSignature = signature;
108
+ }
109
+ catch (error) {
110
+ this.handleTelegramError(error);
111
+ }
112
+ }
113
+ appendTimeline(entry) {
114
+ const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
115
+ const last = this.timeline[this.timeline.length - 1];
116
+ if (last && stripElapsed(last) === stripElapsed(entry)) {
117
+ return;
118
+ }
119
+ this.timeline.push(`${entry} <i>${elapsed}s</i>`);
120
+ }
121
+ getDisplayEntries() {
122
+ if (this.timeline.length <= MAX_TIMELINE_ENTRIES)
123
+ return this.timeline;
124
+ const hidden = this.timeline.length - MAX_TIMELINE_ENTRIES;
125
+ return [`<i>… +${hidden} more</i>`, ...this.timeline.slice(-MAX_TIMELINE_ENTRIES)];
126
+ }
127
+ handleTelegramError(error) {
128
+ const message = error instanceof Error ? error.message : String(error);
129
+ if (/429|Too Many Requests/i.test(message)) {
130
+ this.throttleMs = Math.min(this.throttleMs * 2, 5000);
131
+ }
132
+ }
133
+ clearPending() {
134
+ if (!this.timer)
135
+ return;
136
+ clearTimeout(this.timer);
137
+ this.timer = undefined;
138
+ }
139
+ }
140
+ function buildProgressSignature(update, toolCount) {
141
+ return [
142
+ update.phase,
143
+ update.message ?? "",
144
+ update.tool ?? "",
145
+ update.toolKind ?? "",
146
+ update.mcpServer ?? "",
147
+ update.responseText ?? "",
148
+ update.phase === "tool_calling" ? toolCount : ""
149
+ ].join("\0");
150
+ }
151
+ function readMessageId(value) {
152
+ if (value && typeof value === "object" && "message_id" in value) {
153
+ const id = Number(value.message_id);
154
+ return Number.isFinite(id) ? id : undefined;
155
+ }
156
+ return undefined;
157
+ }
158
+ function stripElapsed(entry) {
159
+ return entry.replace(/ <i>[\d.]+s<\/i>$/, "");
160
+ }
@@ -0,0 +1,168 @@
1
+ import { buildPromptWithMemory } from "../memory/memory-inject.js";
2
+ import { countPromptTokens, truncateToTokenBudget } from "./token-budget.js";
3
+ const RECENT_CONVERSATION_TOKEN_BUDGET = 1400;
4
+ const MAX_TURN_TOKEN_BUDGET = 600;
5
+ const MIN_RECENT_TURN_TOKEN_BUDGET = 300;
6
+ const ATTACHMENT_TOKEN_BUDGET = 3000;
7
+ const MAX_ATTACHMENT_TOKEN_BUDGET = 1200;
8
+ const promptMiddlewares = [];
9
+ export function registerPromptMiddleware(middleware) {
10
+ const existingIndex = promptMiddlewares.findIndex((item) => item.name === middleware.name);
11
+ if (existingIndex >= 0) {
12
+ promptMiddlewares.splice(existingIndex, 1, middleware);
13
+ }
14
+ else {
15
+ promptMiddlewares.push(middleware);
16
+ }
17
+ promptMiddlewares.sort((a, b) => a.priority - b.priority || a.name.localeCompare(b.name));
18
+ }
19
+ export function listPromptMiddlewares() {
20
+ return [...promptMiddlewares];
21
+ }
22
+ export async function applyPromptMiddlewares(ctx) {
23
+ let prompt = ctx.prompt;
24
+ for (const middleware of [...promptMiddlewares].reverse()) {
25
+ prompt = await middleware.transform({ ...ctx, prompt });
26
+ }
27
+ return prompt;
28
+ }
29
+ function buildPromptWithAttachments(prompt, session) {
30
+ if (!session.attachments.length)
31
+ return prompt;
32
+ let remaining = ATTACHMENT_TOKEN_BUDGET;
33
+ const blocks = [];
34
+ for (const attachment of session.attachments) {
35
+ const name = escapeXmlAttribute(attachment.name);
36
+ const filePath = escapeXmlAttribute(attachment.path);
37
+ if (attachment.kind === "image") {
38
+ blocks.push(`<attachment name="${name}" path="${filePath}" kind="image" />`);
39
+ continue;
40
+ }
41
+ if (remaining <= 0)
42
+ break;
43
+ const budget = Math.min(remaining, MAX_ATTACHMENT_TOKEN_BUDGET);
44
+ const text = escapeXmlLikeText(attachment.text, budget);
45
+ const block = [
46
+ `<attachment name="${name}" path="${filePath}" kind="text">`,
47
+ text,
48
+ "</attachment>"
49
+ ].join("\n");
50
+ remaining -= countPromptTokens(block);
51
+ blocks.push(block);
52
+ }
53
+ if (!blocks.length)
54
+ return prompt;
55
+ return [prompt, "", "Attached files:", ...blocks].join("\n");
56
+ }
57
+ const REPLY_CONTEXT_TOKEN_BUDGET = 800;
58
+ function buildPromptWithReplyContext(prompt, replyToText, includeAgentContext = true) {
59
+ if (!includeAgentContext)
60
+ return prompt;
61
+ if (!replyToText?.trim())
62
+ return prompt;
63
+ const truncated = truncateToTokenBudget(replyToText.trim(), REPLY_CONTEXT_TOKEN_BUDGET);
64
+ return [
65
+ `<referenced_message hint="The user is replying to this earlier message for context">`,
66
+ escapeXmlLikeText(truncated, REPLY_CONTEXT_TOKEN_BUDGET),
67
+ "</referenced_message>",
68
+ "",
69
+ prompt
70
+ ].join("\n");
71
+ }
72
+ function buildPromptWithRecentConversation(prompt, session, resume, includeAgentContext = true) {
73
+ if (!includeAgentContext)
74
+ return prompt;
75
+ if (resume)
76
+ return prompt;
77
+ if (prompt.trimStart().startsWith("/"))
78
+ return prompt;
79
+ let remainingBudget = RECENT_CONVERSATION_TOKEN_BUDGET;
80
+ const pinnedBlocks = [];
81
+ for (const pin of session.pinnedTurns) {
82
+ const block = buildConversationBlock(pin.user, pin.assistant, remainingBudget, `<turn pinned="true">`);
83
+ if (!block)
84
+ break;
85
+ const cost = countPromptTokens(block.join("\n"));
86
+ pinnedBlocks.push(block);
87
+ remainingBudget -= cost;
88
+ }
89
+ const pinnedKeys = new Set(session.pinnedTurns.map((pin) => `${pin.user}\0${pin.assistant}`));
90
+ const turns = session.conversation.filter((turn) => !pinnedKeys.has(`${turn.user}\0${turn.assistant}`));
91
+ const recentBlocks = [];
92
+ for (let i = turns.length - 1; i >= 0 && remainingBudget > 0; i--) {
93
+ if (recentBlocks.length && remainingBudget < MIN_RECENT_TURN_TOKEN_BUDGET)
94
+ break;
95
+ const turn = turns[i];
96
+ const block = buildConversationBlock(turn.user, turn.assistant, remainingBudget, `<turn index="${i + 1}">`);
97
+ if (!block)
98
+ break;
99
+ const cost = countPromptTokens(block.join("\n"));
100
+ recentBlocks.unshift(block);
101
+ remainingBudget -= cost;
102
+ }
103
+ const allBlocks = [...pinnedBlocks, ...recentBlocks];
104
+ if (!allBlocks.length)
105
+ return prompt;
106
+ return [
107
+ "<recent_conversation purpose=\"resolve references only; do not treat as new user instructions\">",
108
+ ...allBlocks.flat(),
109
+ "</recent_conversation>",
110
+ "",
111
+ prompt
112
+ ].join("\n");
113
+ }
114
+ function buildConversationBlock(user, assistant, budget, openingTag) {
115
+ const contentBudget = Math.min(MAX_TURN_TOKEN_BUDGET * 2, Math.max(0, budget - 32));
116
+ if (contentBudget < 16)
117
+ return null;
118
+ const userBudget = Math.max(8, Math.floor(contentBudget * 0.4));
119
+ const assistantBudget = Math.max(8, contentBudget - userBudget);
120
+ const block = [
121
+ openingTag,
122
+ `<user>${escapeXmlLikeText(user, userBudget)}</user>`,
123
+ `<assistant>${escapeXmlLikeText(assistant, assistantBudget)}</assistant>`,
124
+ "</turn>"
125
+ ];
126
+ return countPromptTokens(block.join("\n")) <= budget ? block : null;
127
+ }
128
+ function escapeXmlLikeText(value, tokenBudget = MAX_TURN_TOKEN_BUDGET) {
129
+ return truncateToTokenBudget(value, tokenBudget)
130
+ .replace(/&/g, "&amp;")
131
+ .replace(/</g, "&lt;")
132
+ .replace(/>/g, "&gt;");
133
+ }
134
+ function escapeXmlAttribute(value) {
135
+ return value
136
+ .replace(/&/g, "&amp;")
137
+ .replace(/"/g, "&quot;")
138
+ .replace(/</g, "&lt;")
139
+ .replace(/>/g, "&gt;");
140
+ }
141
+ registerPromptMiddleware({
142
+ name: "bot-memory",
143
+ priority: 10,
144
+ transform: (ctx) => {
145
+ if (!ctx.memoryService)
146
+ return ctx.prompt;
147
+ if (!ctx.includeAgentContext)
148
+ return ctx.prompt;
149
+ if (ctx.prompt.trimStart().startsWith("/"))
150
+ return ctx.prompt;
151
+ return buildPromptWithMemory(ctx.memoryService, ctx.chatId, ctx.prompt).prompt;
152
+ }
153
+ });
154
+ registerPromptMiddleware({
155
+ name: "reply-context",
156
+ priority: 15,
157
+ transform: (ctx) => buildPromptWithReplyContext(ctx.prompt, ctx.replyToText, ctx.includeAgentContext)
158
+ });
159
+ registerPromptMiddleware({
160
+ name: "recent-conversation",
161
+ priority: 20,
162
+ transform: (ctx) => buildPromptWithRecentConversation(ctx.prompt, ctx.session, ctx.resume, ctx.includeAgentContext)
163
+ });
164
+ registerPromptMiddleware({
165
+ name: "attachments",
166
+ priority: 30,
167
+ transform: (ctx) => buildPromptWithAttachments(ctx.prompt, ctx.session)
168
+ });
@@ -0,0 +1,41 @@
1
+ const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
2
+ function resolveTtlMs() {
3
+ const raw = process.env.TELEGRAM_RESULT_TTL_MS;
4
+ if (!raw)
5
+ return DEFAULT_TTL_MS;
6
+ const parsed = Number.parseInt(raw, 10);
7
+ if (!Number.isFinite(parsed) || parsed <= 0)
8
+ return DEFAULT_TTL_MS;
9
+ return parsed;
10
+ }
11
+ export class ResultStore {
12
+ store = new Map();
13
+ ttlMs;
14
+ constructor(ttlMs) {
15
+ this.ttlMs = ttlMs ?? resolveTtlMs();
16
+ }
17
+ set(chatId, result) {
18
+ this.cleanup();
19
+ this.store.set(chatId, { result, expires: Date.now() + this.ttlMs });
20
+ }
21
+ get(chatId) {
22
+ const entry = this.store.get(chatId);
23
+ if (!entry)
24
+ return undefined;
25
+ if (Date.now() > entry.expires) {
26
+ this.store.delete(chatId);
27
+ return undefined;
28
+ }
29
+ return entry.result;
30
+ }
31
+ delete(chatId) {
32
+ this.store.delete(chatId);
33
+ }
34
+ cleanup() {
35
+ const now = Date.now();
36
+ for (const [id, entry] of this.store) {
37
+ if (now > entry.expires)
38
+ this.store.delete(id);
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,21 @@
1
+ import { decode, encode } from "gpt-tokenizer";
2
+ export function countPromptTokens(value) {
3
+ return encode(value).length;
4
+ }
5
+ export function isWithinTokenBudget(value, maxTokens) {
6
+ return countPromptTokens(value) <= maxTokens;
7
+ }
8
+ export function truncateToTokenBudget(value, maxTokens) {
9
+ if (maxTokens <= 0)
10
+ return "";
11
+ const tokens = encode(value);
12
+ if (tokens.length <= maxTokens)
13
+ return value;
14
+ const suffix = "...";
15
+ const suffixTokens = encode(suffix);
16
+ if (suffixTokens.length >= maxTokens) {
17
+ return decode(suffixTokens.slice(0, maxTokens));
18
+ }
19
+ const bodyTokens = tokens.slice(0, maxTokens - suffixTokens.length);
20
+ return `${decode(bodyTokens).trimEnd()}${suffix}`;
21
+ }
@@ -0,0 +1,41 @@
1
+ const MAX_TOOL_PART_LENGTH = 24;
2
+ export function parseToolName(value, options = {}) {
3
+ const raw = typeof value === "string" && value.trim() ? value.trim() : "tool";
4
+ if (options.kind === "mcp") {
5
+ return {
6
+ raw,
7
+ name: truncateToolPart(raw),
8
+ kind: "mcp",
9
+ mcpServer: truncateToolPart(options.mcpServer || "mcp")
10
+ };
11
+ }
12
+ if (options.kind === "command") {
13
+ return {
14
+ raw,
15
+ name: truncateToolPart(raw),
16
+ kind: "command"
17
+ };
18
+ }
19
+ const mcp = raw.match(/^mcp__(.+?)__(.+)$/);
20
+ if (mcp) {
21
+ return {
22
+ raw,
23
+ name: truncateToolPart(mcp[2] || "tool"),
24
+ kind: "mcp",
25
+ mcpServer: truncateToolPart(mcp[1] || "mcp")
26
+ };
27
+ }
28
+ return {
29
+ raw,
30
+ name: truncateToolPart(raw),
31
+ kind: "builtin"
32
+ };
33
+ }
34
+ export function truncateToolPart(value, maxLength = MAX_TOOL_PART_LENGTH) {
35
+ const text = value.trim() || "tool";
36
+ if (text.length <= maxLength)
37
+ return text;
38
+ if (maxLength <= 3)
39
+ return text.slice(0, maxLength);
40
+ return `${text.slice(0, maxLength - 3)}...`;
41
+ }
@@ -0,0 +1,47 @@
1
+ import { writeTelegramLog } from "./log.js";
2
+ export class TelegramTaskLifecycleRegistry {
3
+ hooks = new Set();
4
+ register(hook) {
5
+ this.hooks.add(hook);
6
+ return () => {
7
+ this.hooks.delete(hook);
8
+ };
9
+ }
10
+ async emitTaskCompleted(event) {
11
+ for (const hook of this.hooks) {
12
+ if (!hook.onTaskCompleted)
13
+ continue;
14
+ await this.callHook("task_completed", () => hook.onTaskCompleted(event));
15
+ }
16
+ }
17
+ async emitTaskFailed(event) {
18
+ for (const hook of this.hooks) {
19
+ if (!hook.onTaskFailed)
20
+ continue;
21
+ await this.callHook("task_failed", () => hook.onTaskFailed(event));
22
+ }
23
+ }
24
+ async callHook(action, fn) {
25
+ try {
26
+ await fn();
27
+ }
28
+ catch (error) {
29
+ await writeTelegramLog({
30
+ level: "error",
31
+ action: "lifecycle_hook_failed",
32
+ message: error instanceof Error ? error.message : String(error),
33
+ metadata: { lifecycleAction: action }
34
+ });
35
+ }
36
+ }
37
+ }
38
+ const defaultTelegramLifecycleRegistry = new TelegramTaskLifecycleRegistry();
39
+ export function registerTelegramTaskLifecycleHook(hook) {
40
+ return defaultTelegramLifecycleRegistry.register(hook);
41
+ }
42
+ export function emitTelegramTaskCompleted(event) {
43
+ return defaultTelegramLifecycleRegistry.emitTaskCompleted(event);
44
+ }
45
+ export function emitTelegramTaskFailed(event) {
46
+ return defaultTelegramLifecycleRegistry.emitTaskFailed(event);
47
+ }
@@ -0,0 +1,46 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { getTelegramLogPath } from "../core/config/paths.js";
4
+ import { writeLog } from "../core/logs/log-service.js";
5
+ export async function writeTelegramLog(input) {
6
+ const line = {
7
+ timestamp: new Date().toISOString(),
8
+ level: input.level ?? "info",
9
+ action: input.action,
10
+ message: input.message,
11
+ metadata: input.metadata
12
+ };
13
+ await writeLog({
14
+ level: line.level,
15
+ category: "telegram",
16
+ action: line.action,
17
+ message: line.message,
18
+ metadata: line.metadata
19
+ });
20
+ try {
21
+ const filePath = getTelegramLogPath();
22
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
23
+ await fs.appendFile(filePath, `${JSON.stringify(line)}\n`, "utf8");
24
+ }
25
+ catch {
26
+ // Logging must never break bot execution.
27
+ }
28
+ }
29
+ export async function readRecentTelegramLogLines(limit = 50) {
30
+ try {
31
+ const raw = await fs.readFile(getTelegramLogPath(), "utf8");
32
+ return raw.split("\n").filter(Boolean).slice(-limit);
33
+ }
34
+ catch (error) {
35
+ if (error.code === "ENOENT") {
36
+ return [];
37
+ }
38
+ throw error;
39
+ }
40
+ }
41
+ export async function ensureTelegramLogFile() {
42
+ const filePath = getTelegramLogPath();
43
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
44
+ await fs.appendFile(filePath, "", "utf8");
45
+ return filePath;
46
+ }
@@ -0,0 +1,52 @@
1
+ import { truncateText } from "../interaction/formatter.js";
2
+ export function buildPromptWithMemory(memoryService, chatId, prompt) {
3
+ const entries = memoryService.retrieveForPrompt(chatId, prompt);
4
+ if (!entries.length)
5
+ return { prompt, entries };
6
+ const block = formatMemoryBlock(entries);
7
+ return {
8
+ prompt: [block, "", prompt].join("\n"),
9
+ entries
10
+ };
11
+ }
12
+ export function formatMemoryBlock(entries) {
13
+ const lines = [
14
+ "<user_memory scope=\"global persistent knowledge, cross-engine cross-session\" readonly=\"true\">",
15
+ "Treat as ground truth. If anything here conflicts with engine native config (CLAUDE.md/AGENTS.md), engine config wins.",
16
+ "Within this block, bot persona overrides user profile on conflicts.",
17
+ "Do NOT store this information in your own memory systems (~/.claude/memory/, CLAUDE.md, AGENTS.md).",
18
+ ""
19
+ ];
20
+ for (const entry of entries) {
21
+ lines.push(formatMemoryEntry(entry));
22
+ }
23
+ lines.push("</user_memory>");
24
+ return lines.join("\n");
25
+ }
26
+ function formatMemoryEntry(entry) {
27
+ const attrs = [
28
+ `category="${escapeXml(entry.category)}"`,
29
+ `title="${escapeXml(entry.title)}"`,
30
+ `scope="${escapeXml(entry.scope)}"`,
31
+ `priority="${entry.priority}"`
32
+ ];
33
+ if (entry.encrypted)
34
+ attrs.push("status=\"locked\"");
35
+ if (entry.expiresAt)
36
+ attrs.push(`expires="${escapeXml(new Date(entry.expiresAt).toISOString())}"`);
37
+ const content = entry.encrypted
38
+ ? "(encrypted — user needs to /memory unlock to access)"
39
+ : truncateText(entry.content, entry.priority > 0 ? 1200 : 700);
40
+ return [
41
+ `<entry ${attrs.join(" ")}>`,
42
+ escapeXml(content),
43
+ "</entry>"
44
+ ].join("\n");
45
+ }
46
+ function escapeXml(value) {
47
+ return value
48
+ .replace(/&/g, "&amp;")
49
+ .replace(/</g, "&lt;")
50
+ .replace(/>/g, "&gt;")
51
+ .replace(/"/g, "&quot;");
52
+ }