edsger 0.28.0 → 0.28.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.
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* - Worker sends: { type: 'command:resume_feature', featureId }
|
|
15
15
|
*/
|
|
16
16
|
import { randomUUID } from 'node:crypto';
|
|
17
|
-
import { getFeatureChannel, claimPendingMessages, sendSystemMessage, } from '../../api/chat.js';
|
|
17
|
+
import { getFeatureChannel, claimPendingMessages, listChannels, sendSystemMessage, } from '../../api/chat.js';
|
|
18
18
|
import { processHumanMessages, processPhaseCompletion, } from '../../phases/chat-processor/index.js';
|
|
19
19
|
function sendMessage(msg) {
|
|
20
20
|
if (process.send) {
|
|
@@ -36,12 +36,20 @@ const WORKER_ID = `chat-worker-${process.pid}-${randomUUID().slice(0, 8)}`;
|
|
|
36
36
|
const activeChannels = new Map();
|
|
37
37
|
// Poll interval in ms
|
|
38
38
|
const POLL_INTERVAL = 5000;
|
|
39
|
+
// Refresh channel list every N polls (~30s at 5s intervals)
|
|
40
|
+
const CHANNEL_REFRESH_INTERVAL = 6;
|
|
41
|
+
let pollCount = 0;
|
|
39
42
|
// ============================================================
|
|
40
43
|
// Message Polling Loop
|
|
41
44
|
// ============================================================
|
|
42
45
|
async function pollForMessages() {
|
|
43
46
|
if (!config || !isRunning)
|
|
44
47
|
return;
|
|
48
|
+
// Periodically refresh the channel list to pick up newly created channels
|
|
49
|
+
pollCount++;
|
|
50
|
+
if (pollCount % CHANNEL_REFRESH_INTERVAL === 0) {
|
|
51
|
+
await refreshChannels();
|
|
52
|
+
}
|
|
45
53
|
for (const [featureId, channelId] of activeChannels) {
|
|
46
54
|
try {
|
|
47
55
|
// Atomically claim messages — other workers won't see these
|
|
@@ -88,6 +96,8 @@ function stopPolling() {
|
|
|
88
96
|
async function handleInit(msg) {
|
|
89
97
|
config = msg.config;
|
|
90
98
|
log('info', `Chat worker initialized (id: ${WORKER_ID})`);
|
|
99
|
+
// Load existing channels before starting the poll loop
|
|
100
|
+
await refreshChannels();
|
|
91
101
|
startPolling();
|
|
92
102
|
}
|
|
93
103
|
async function handlePhaseCompleted(msg) {
|
|
@@ -138,6 +148,30 @@ async function handleFeatureDone(msg) {
|
|
|
138
148
|
// ============================================================
|
|
139
149
|
// Channel Management
|
|
140
150
|
// ============================================================
|
|
151
|
+
/**
|
|
152
|
+
* Refresh the active channels list from the server.
|
|
153
|
+
* Called on init and periodically during polling to discover
|
|
154
|
+
* newly created channels (e.g., when a user opens a feature chat on the web).
|
|
155
|
+
*/
|
|
156
|
+
async function refreshChannels() {
|
|
157
|
+
try {
|
|
158
|
+
const channels = await listChannels('feature');
|
|
159
|
+
let added = 0;
|
|
160
|
+
for (const channel of channels) {
|
|
161
|
+
if (channel.channel_ref_id && !activeChannels.has(channel.channel_ref_id)) {
|
|
162
|
+
activeChannels.set(channel.channel_ref_id, channel.id);
|
|
163
|
+
added++;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (added > 0) {
|
|
167
|
+
log('info', `Discovered ${added} new channel(s) (total: ${activeChannels.size})`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
172
|
+
log('error', `Failed to refresh channels: ${msg}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
141
175
|
async function ensureFeatureChannel(featureId) {
|
|
142
176
|
if (activeChannels.has(featureId)) {
|
|
143
177
|
return activeChannels.get(featureId);
|
|
@@ -62,12 +62,32 @@ export async function processHumanMessages(messages, featureId, config, verbose)
|
|
|
62
62
|
return `[Message ${i + 1} from ${m.sender_name}]: ${m.content}`;
|
|
63
63
|
});
|
|
64
64
|
const userPrompt = messageParts.join('\n\n');
|
|
65
|
-
// On first message: include full context so the session
|
|
66
|
-
//
|
|
65
|
+
// On first message: include full context + chat history so the session
|
|
66
|
+
// starts informed (covers CLI restart where in-memory session is lost).
|
|
67
|
+
// On resume: just send the new message — the session already has context.
|
|
67
68
|
let fullPrompt;
|
|
68
69
|
if (!existingSessionId) {
|
|
69
70
|
const context = await buildChatContext(featureId, channelId, verbose);
|
|
70
71
|
const contextStr = formatContextForAI(context);
|
|
72
|
+
// Load recent chat history so the AI has conversational continuity
|
|
73
|
+
const recentMessages = await listChatMessages(channelId, { limit: 30 }, verbose);
|
|
74
|
+
// Exclude the messages we're about to process (they'll be in userPrompt)
|
|
75
|
+
const currentIds = new Set(messages.map((m) => m.id));
|
|
76
|
+
const historyMessages = recentMessages.filter((m) => !currentIds.has(m.id));
|
|
77
|
+
let historySection = '';
|
|
78
|
+
if (historyMessages.length > 0) {
|
|
79
|
+
const historyLines = historyMessages.map((m) => {
|
|
80
|
+
const role = m.sender_type === 'ai' ? 'AI' : m.sender_type === 'system' ? 'System' : (m.sender_name || 'User');
|
|
81
|
+
return `[${role}]: ${m.content}`;
|
|
82
|
+
});
|
|
83
|
+
historySection = [
|
|
84
|
+
`## Previous Chat History`,
|
|
85
|
+
`The following is the recent conversation history in this channel. Continue the conversation naturally.`,
|
|
86
|
+
'',
|
|
87
|
+
...historyLines,
|
|
88
|
+
'',
|
|
89
|
+
].join('\n');
|
|
90
|
+
}
|
|
71
91
|
fullPrompt = [
|
|
72
92
|
`## Current Feature Context`,
|
|
73
93
|
contextStr,
|
|
@@ -75,6 +95,7 @@ export async function processHumanMessages(messages, featureId, config, verbose)
|
|
|
75
95
|
`## Channel ID: ${channelId}`,
|
|
76
96
|
`## Feature ID: ${featureId}`,
|
|
77
97
|
'',
|
|
98
|
+
historySection,
|
|
78
99
|
`## Human Message`,
|
|
79
100
|
userPrompt,
|
|
80
101
|
'',
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
|
|
9
9
|
import { z } from 'zod';
|
|
10
10
|
import { callMcpEndpoint } from '../../api/mcp-client.js';
|
|
11
|
-
import { sendAiMessage } from '../../api/chat.js';
|
|
11
|
+
import { sendAiMessage, listChatMessages } from '../../api/chat.js';
|
|
12
12
|
/**
|
|
13
13
|
* Create an in-process MCP server with chat-specific tools.
|
|
14
14
|
* Pass the returned config to query() options.mcpServers.
|
|
@@ -241,6 +241,36 @@ export function createChatMcpServer() {
|
|
|
241
241
|
],
|
|
242
242
|
};
|
|
243
243
|
}),
|
|
244
|
+
tool('get_chat_history', 'Retrieve older chat messages from the channel. Use this when you need more context about what was discussed earlier — for example, to understand referenced bugs, prior decisions, or feedback the user gave in earlier messages.', {
|
|
245
|
+
channel_id: z.string().describe('Chat channel ID'),
|
|
246
|
+
limit: z
|
|
247
|
+
.number()
|
|
248
|
+
.optional()
|
|
249
|
+
.describe('Number of messages to retrieve (default 50, max 100)'),
|
|
250
|
+
before: z
|
|
251
|
+
.string()
|
|
252
|
+
.optional()
|
|
253
|
+
.describe('Fetch messages before this ISO timestamp for pagination'),
|
|
254
|
+
}, async (args) => {
|
|
255
|
+
const limit = Math.min(args.limit || 50, 100);
|
|
256
|
+
const messages = await listChatMessages(args.channel_id, { limit, ...(args.before ? { since: args.before } : {}) });
|
|
257
|
+
const formatted = messages.map((m) => ({
|
|
258
|
+
id: m.id,
|
|
259
|
+
sender: m.sender_type === 'ai' ? 'AI' : m.sender_type === 'system' ? 'System' : (m.sender_name || 'User'),
|
|
260
|
+
sender_type: m.sender_type,
|
|
261
|
+
content: m.content,
|
|
262
|
+
message_type: m.message_type,
|
|
263
|
+
created_at: m.created_at,
|
|
264
|
+
}));
|
|
265
|
+
return {
|
|
266
|
+
content: [
|
|
267
|
+
{
|
|
268
|
+
type: 'text',
|
|
269
|
+
text: JSON.stringify({ messages: formatted, count: formatted.length }, null, 2),
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
};
|
|
273
|
+
}),
|
|
244
274
|
tool('trigger_phase_rerun', 'Reset a workflow phase to pending so it will re-run.', {
|
|
245
275
|
feature_id: z.string().describe('Feature ID'),
|
|
246
276
|
phase: z.string().describe('Phase name to reset'),
|