talon-agent 1.0.0 → 1.2.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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -0
  3. package/package.json +15 -11
  4. package/prompts/dream.md +7 -3
  5. package/prompts/heartbeat.md +30 -0
  6. package/prompts/identity.md +1 -0
  7. package/prompts/teams.md +3 -0
  8. package/prompts/telegram.md +1 -0
  9. package/src/__tests__/chat-settings.test.ts +108 -2
  10. package/src/__tests__/cleanup-registry.test.ts +58 -0
  11. package/src/__tests__/config.test.ts +118 -52
  12. package/src/__tests__/cron-store-extended.test.ts +661 -0
  13. package/src/__tests__/cron-store.test.ts +145 -11
  14. package/src/__tests__/daily-log.test.ts +224 -13
  15. package/src/__tests__/dispatcher.test.ts +424 -23
  16. package/src/__tests__/dream.test.ts +1028 -0
  17. package/src/__tests__/errors-extended.test.ts +428 -0
  18. package/src/__tests__/errors.test.ts +95 -3
  19. package/src/__tests__/fuzz.test.ts +87 -15
  20. package/src/__tests__/gateway-actions.test.ts +1174 -433
  21. package/src/__tests__/gateway-http.test.ts +210 -19
  22. package/src/__tests__/gateway-retry.test.ts +359 -0
  23. package/src/__tests__/gateway-withRetry-extended.test.ts +343 -0
  24. package/src/__tests__/graph.test.ts +830 -0
  25. package/src/__tests__/handlers-stream.test.ts +208 -0
  26. package/src/__tests__/handlers.test.ts +2539 -70
  27. package/src/__tests__/heartbeat.test.ts +364 -0
  28. package/src/__tests__/history-extended.test.ts +775 -0
  29. package/src/__tests__/history-persistence.test.ts +74 -19
  30. package/src/__tests__/history.test.ts +113 -79
  31. package/src/__tests__/integration.test.ts +43 -8
  32. package/src/__tests__/log-init.test.ts +129 -0
  33. package/src/__tests__/log.test.ts +23 -5
  34. package/src/__tests__/media-index.test.ts +317 -35
  35. package/src/__tests__/plugin.test.ts +314 -0
  36. package/src/__tests__/prompt-builder-extended.test.ts +296 -0
  37. package/src/__tests__/prompt-builder.test.ts +44 -9
  38. package/src/__tests__/sessions.test.ts +258 -4
  39. package/src/__tests__/storage-save-errors.test.ts +342 -0
  40. package/src/__tests__/teams-frontend.test.ts +526 -31
  41. package/src/__tests__/telegram-formatting.test.ts +82 -0
  42. package/src/__tests__/terminal-commands.test.ts +208 -1
  43. package/src/__tests__/terminal-renderer.test.ts +223 -0
  44. package/src/__tests__/time.test.ts +107 -0
  45. package/src/__tests__/workspace-migrate.test.ts +256 -0
  46. package/src/__tests__/workspace.test.ts +63 -1
  47. package/src/backend/claude-sdk/tools.ts +64 -18
  48. package/src/bootstrap.ts +14 -14
  49. package/src/cli.ts +440 -125
  50. package/src/core/cron.ts +20 -5
  51. package/src/core/dispatcher.ts +27 -9
  52. package/src/core/dream.ts +79 -24
  53. package/src/core/errors.ts +12 -2
  54. package/src/core/gateway-actions.ts +182 -46
  55. package/src/core/gateway.ts +93 -41
  56. package/src/core/heartbeat.ts +515 -0
  57. package/src/core/plugin.ts +1 -1
  58. package/src/core/prompt-builder.ts +1 -4
  59. package/src/core/pulse.ts +4 -3
  60. package/src/frontend/teams/actions.ts +3 -1
  61. package/src/frontend/teams/formatting.ts +47 -8
  62. package/src/frontend/teams/graph.ts +35 -11
  63. package/src/frontend/teams/index.ts +155 -57
  64. package/src/frontend/teams/tools.ts +4 -6
  65. package/src/frontend/telegram/actions.ts +358 -82
  66. package/src/frontend/telegram/admin.ts +162 -72
  67. package/src/frontend/telegram/callbacks.ts +16 -10
  68. package/src/frontend/telegram/commands.ts +37 -21
  69. package/src/frontend/telegram/formatting.ts +2 -4
  70. package/src/frontend/telegram/handlers.ts +262 -66
  71. package/src/frontend/telegram/index.ts +39 -14
  72. package/src/frontend/telegram/middleware.ts +14 -4
  73. package/src/frontend/telegram/userbot.ts +16 -4
  74. package/src/frontend/terminal/renderer.ts +1 -4
  75. package/src/index.ts +28 -4
  76. package/src/storage/chat-settings.ts +32 -9
  77. package/src/storage/cron-store.ts +53 -11
  78. package/src/storage/daily-log.ts +72 -19
  79. package/src/storage/history.ts +39 -21
  80. package/src/storage/media-index.ts +37 -12
  81. package/src/storage/sessions.ts +3 -2
  82. package/src/util/cleanup-registry.ts +34 -0
  83. package/src/util/config.ts +85 -23
  84. package/src/util/log.ts +47 -17
  85. package/src/util/paths.ts +10 -0
  86. package/src/util/time.ts +29 -6
  87. package/src/util/watchdog.ts +5 -1
  88. package/src/util/workspace.ts +51 -10
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Tests for createStreamCallbacks — specifically the onStreamDelta path that
3
+ * requires draftsSupported === null (fresh module state).
4
+ *
5
+ * This file uses vi.resetModules() in beforeEach to guarantee a fresh handlers.ts
6
+ * where the module-level `draftsSupported` variable starts at null.
7
+ */
8
+ import { describe, it, expect, vi, beforeEach } from "vitest";
9
+
10
+ // These mocks are hoisted and apply to every fresh import within the test.
11
+ vi.mock("../util/log.js", () => ({
12
+ log: vi.fn(),
13
+ logError: vi.fn(),
14
+ logWarn: vi.fn(),
15
+ logDebug: vi.fn(),
16
+ }));
17
+
18
+ vi.mock("../core/dispatcher.js", () => ({
19
+ execute: vi.fn(),
20
+ }));
21
+
22
+ vi.mock("../core/errors.js", () => ({
23
+ classify: vi.fn(() => ({
24
+ reason: "unknown",
25
+ message: "error",
26
+ retryable: false,
27
+ })),
28
+ friendlyMessage: vi.fn(() => "Something went wrong"),
29
+ TalonError: class TalonError extends Error {},
30
+ }));
31
+
32
+ vi.mock("../core/prompt-builder.js", () => ({
33
+ enrichDMPrompt: vi.fn((p: string) => p),
34
+ enrichGroupPrompt: vi.fn((p: string) => p),
35
+ }));
36
+
37
+ vi.mock("../storage/daily-log.js", () => ({
38
+ appendDailyLog: vi.fn(),
39
+ appendDailyLogResponse: vi.fn(),
40
+ }));
41
+
42
+ vi.mock("../storage/history.js", () => ({
43
+ setMessageFilePath: vi.fn(),
44
+ getHistory: vi.fn(() => []),
45
+ }));
46
+
47
+ vi.mock("../storage/media-index.js", () => ({
48
+ addMedia: vi.fn(),
49
+ }));
50
+
51
+ vi.mock("../storage/sessions.js", () => ({
52
+ getSession: vi.fn(() => ({
53
+ turns: 0,
54
+ lastActive: Date.now(),
55
+ createdAt: Date.now(),
56
+ })),
57
+ incrementTurns: vi.fn(),
58
+ recordUsage: vi.fn(),
59
+ resetSession: vi.fn(),
60
+ getSessionInfo: vi.fn(() => ({
61
+ sessionId: undefined,
62
+ turns: 0,
63
+ lastActive: 0,
64
+ createdAt: 0,
65
+ usage: {},
66
+ })),
67
+ setSessionId: vi.fn(),
68
+ setLastBotMessageId: vi.fn(),
69
+ getLastBotMessageId: vi.fn(),
70
+ getActiveSessionCount: vi.fn(() => 0),
71
+ setSessionName: vi.fn(),
72
+ getAllSessions: vi.fn(() => []),
73
+ loadSessions: vi.fn(),
74
+ flushSessions: vi.fn(),
75
+ }));
76
+
77
+ vi.mock("../util/watchdog.js", () => ({
78
+ recordMessageProcessed: vi.fn(),
79
+ recordError: vi.fn(),
80
+ getHealthStatus: vi.fn(() => ({
81
+ healthy: true,
82
+ totalMessagesProcessed: 0,
83
+ recentErrorCount: 0,
84
+ msSinceLastMessage: 0,
85
+ })),
86
+ }));
87
+
88
+ vi.mock("node:fs", () => ({
89
+ existsSync: vi.fn(() => false),
90
+ writeFileSync: vi.fn(),
91
+ mkdirSync: vi.fn(),
92
+ readFileSync: vi.fn(() => ""),
93
+ }));
94
+
95
+ vi.mock("write-file-atomic", () => ({
96
+ default: { sync: vi.fn() },
97
+ }));
98
+
99
+ vi.mock("../storage/cron-store.js", () => ({
100
+ addCronJob: vi.fn(),
101
+ getCronJob: vi.fn(),
102
+ getCronJobsForChat: vi.fn(() => []),
103
+ updateCronJob: vi.fn(),
104
+ deleteCronJob: vi.fn(),
105
+ validateCronExpression: vi.fn(() => ({
106
+ valid: true,
107
+ next: new Date().toISOString(),
108
+ })),
109
+ generateCronId: vi.fn(() => "test-id"),
110
+ loadCronJobs: vi.fn(),
111
+ }));
112
+
113
+ const mockConfig = {
114
+ botToken: "test-token",
115
+ allowedUserIds: [],
116
+ model: "claude-sonnet-4-6",
117
+ systemPrompt: "You are helpful.",
118
+ maxTokens: 8096,
119
+ workspaceDir: "/tmp/test-workspace",
120
+ plugins: [],
121
+ };
122
+
123
+ describe("createStreamCallbacks — onStreamDelta streaming disabled path", () => {
124
+ let handleTextMessage: (
125
+ ctx: unknown,
126
+ bot: unknown,
127
+ config: unknown,
128
+ ) => Promise<void>;
129
+ let executeMock: ReturnType<typeof vi.fn>;
130
+ let sendMessageDraftMock: ReturnType<typeof vi.fn>;
131
+ let logWarnMock: ReturnType<typeof vi.fn>;
132
+
133
+ beforeEach(async () => {
134
+ vi.resetModules();
135
+ // Fresh import gives us draftsSupported === null
136
+ const handlers = await import("../frontend/telegram/handlers.js");
137
+ handleTextMessage = handlers.handleTextMessage as unknown as (
138
+ ctx: unknown,
139
+ bot: unknown,
140
+ config: unknown,
141
+ ) => Promise<void>;
142
+
143
+ const dispatcher = await import("../core/dispatcher.js");
144
+ executeMock = dispatcher.execute as ReturnType<typeof vi.fn>;
145
+
146
+ const log = await import("../util/log.js");
147
+ logWarnMock = log.logWarn as ReturnType<typeof vi.fn>;
148
+ });
149
+
150
+ it("disables streaming and logs warning when sendMessageDraft throws (draftsSupported=null)", async () => {
151
+ sendMessageDraftMock = vi.fn(async () => {
152
+ throw new Error("method not found");
153
+ });
154
+
155
+ const mockBot = {
156
+ api: {
157
+ sendMessage: vi.fn(async () => ({ message_id: 1 })),
158
+ sendMessageDraft: sendMessageDraftMock,
159
+ getFile: vi.fn(async () => ({ file_path: "test" })),
160
+ },
161
+ };
162
+
163
+ executeMock.mockImplementationOnce(
164
+ async (params: Record<string, unknown>) => {
165
+ const onStreamDelta = params.onStreamDelta as (
166
+ acc: string,
167
+ phase?: string,
168
+ ) => Promise<void>;
169
+ // Wait for the 1000ms stream.started timer to fire
170
+ await new Promise((r) => setTimeout(r, 1100));
171
+ // With draftsSupported === null and state.started = true, onStreamDelta runs
172
+ if (onStreamDelta) await onStreamDelta("x".repeat(50), "text");
173
+ return {
174
+ text: "",
175
+ durationMs: 10,
176
+ inputTokens: 1,
177
+ outputTokens: 1,
178
+ cacheRead: 0,
179
+ cacheWrite: 0,
180
+ bridgeMessageCount: 0,
181
+ };
182
+ },
183
+ );
184
+
185
+ const ctx = {
186
+ chat: { id: 99001, type: "private" },
187
+ message: {
188
+ text: "streaming disabled test",
189
+ message_id: 980,
190
+ reply_to_message: null,
191
+ },
192
+ me: { id: 999, username: "testbot" },
193
+ from: { id: 96, first_name: "TestUser" },
194
+ };
195
+
196
+ await handleTextMessage(ctx, mockBot, mockConfig);
197
+ // Wait for debounce (500ms) + executeMock sleep (1100ms) + buffer
198
+ await new Promise((r) => setTimeout(r, 2000));
199
+
200
+ // sendMessageDraft was called, then threw → draftsSupported set to false
201
+ expect(sendMessageDraftMock).toHaveBeenCalled();
202
+ // logWarn should be called with the "streaming disabled" message
203
+ expect(logWarnMock).toHaveBeenCalledWith(
204
+ "bot",
205
+ expect.stringContaining("streaming disabled"),
206
+ );
207
+ }, 5000);
208
+ });