@co0ontty/wand 1.39.0 → 1.40.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.
- package/dist/cli.js +0 -0
- package/dist/git-quick-commit.js +27 -0
- package/dist/process-manager.d.ts +1 -0
- package/dist/process-manager.js +134 -11
- package/dist/resume-policy.d.ts +1 -0
- package/dist/resume-policy.js +7 -1
- package/dist/server-session-routes.js +30 -10
- package/dist/types.d.ts +4 -0
- package/dist/web-ui/content/scripts.js +563 -263
- package/dist/web-ui/content/styles.css +372 -356
- package/package.json +1 -1
- package/dist/acp-protocol.d.ts +0 -67
- package/dist/acp-protocol.js +0 -291
- package/dist/claude-stream-adapter.d.ts +0 -35
- package/dist/claude-stream-adapter.js +0 -153
- package/dist/claude-structured-runner.d.ts +0 -27
- package/dist/claude-structured-runner.js +0 -106
- package/dist/message-parser.d.ts +0 -2
- package/dist/message-parser.js +0 -329
- package/dist/message-queue.d.ts +0 -57
- package/dist/message-queue.js +0 -127
- package/dist/session-lifecycle.d.ts +0 -81
- package/dist/session-lifecycle.js +0 -176
package/dist/message-parser.js
DELETED
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
import { stripAnsi, isNoiseLine } from "./pty-text-utils.js";
|
|
2
|
-
function isCodexCommand(command) {
|
|
3
|
-
return /^codex\b/.test((command ?? "").trim());
|
|
4
|
-
}
|
|
5
|
-
const CODEX_FOOTER_RE = /\bgpt-\d+(?:\.\d+)?(?:\s+[a-z0-9.-]+)?\s+·\s+\d+%\s+left\s+·\s+(?:\/|~\/).+/i;
|
|
6
|
-
const CODEX_ACTIVITY_RE = /^(?:thinking|working|running|planning|applying|reading|searching|inspecting|reviewing|summarizing|editing|updating|writing|completed)\b/i;
|
|
7
|
-
function stripCodexSegment(raw) {
|
|
8
|
-
return raw
|
|
9
|
-
.replace(/\x1b\][^\x07]*(\x07|\x1b\\)/g, "")
|
|
10
|
-
.replace(/\x1b\[(\d+)C/g, (_match, count) => " ".repeat(Number(count) || 1))
|
|
11
|
-
.replace(/\x1b\[[0-9;?]*[AB]/g, "\n")
|
|
12
|
-
.replace(/\x1b\[[0-9;?]*[su]/g, "")
|
|
13
|
-
.replace(/\x1b\[[0-9;?]*[HfJKr]/g, "\n")
|
|
14
|
-
.replace(/\x1bM/g, "\n")
|
|
15
|
-
.replace(/\x1b\[[0-9;?]*[ST]/g, "\n")
|
|
16
|
-
.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, "")
|
|
17
|
-
.replace(/\x1b[><=ePX^_]/g, "")
|
|
18
|
-
.replace(/[\u00a0\u200b-\u200d\ufeff]/g, " ")
|
|
19
|
-
// eslint-disable-next-line no-control-regex
|
|
20
|
-
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "")
|
|
21
|
-
.replace(/[ \t]+\n/g, "\n");
|
|
22
|
-
}
|
|
23
|
-
function normalizeCodexText(text) {
|
|
24
|
-
return text
|
|
25
|
-
.replace(/\s+/g, " ")
|
|
26
|
-
.replace(/[M]+$/g, "")
|
|
27
|
-
.trim();
|
|
28
|
-
}
|
|
29
|
-
function isLikelyAssistantTailArtifact(longer, shorter) {
|
|
30
|
-
if (!longer.startsWith(shorter))
|
|
31
|
-
return false;
|
|
32
|
-
const suffix = longer.slice(shorter.length);
|
|
33
|
-
return /^[a-z]{1,4}$/i.test(suffix);
|
|
34
|
-
}
|
|
35
|
-
function parseCodexMessages(output) {
|
|
36
|
-
const messages = [];
|
|
37
|
-
function normalizeCodexAssistantLine(line) {
|
|
38
|
-
return line
|
|
39
|
-
.replace(/^[•◦·]\s*/, "")
|
|
40
|
-
.replace(/^⏺\s+/, "")
|
|
41
|
-
.replace(/^│\s*/, "")
|
|
42
|
-
.trim();
|
|
43
|
-
}
|
|
44
|
-
function normalizeCodexPromptLine(line) {
|
|
45
|
-
return line
|
|
46
|
-
.replace(/^›\s*/, "")
|
|
47
|
-
.replace(/^>\s*/, "")
|
|
48
|
-
.trim();
|
|
49
|
-
}
|
|
50
|
-
function shouldIgnoreCodexLine(line) {
|
|
51
|
-
const trimmed = line.trim();
|
|
52
|
-
if (!trimmed)
|
|
53
|
-
return true;
|
|
54
|
-
if (isNoiseLine(trimmed))
|
|
55
|
-
return true;
|
|
56
|
-
if (CODEX_FOOTER_RE.test(trimmed))
|
|
57
|
-
return true;
|
|
58
|
-
if (/^[╭╰│┌└┐┘├┤┬┴┼─═]/.test(trimmed))
|
|
59
|
-
return true;
|
|
60
|
-
if (/^\[>[0-9;?]*u$/i.test(trimmed))
|
|
61
|
-
return true;
|
|
62
|
-
if (/^M+$/i.test(trimmed))
|
|
63
|
-
return true;
|
|
64
|
-
if (/^(?:OpenAI Codex|Codex)\b/i.test(trimmed))
|
|
65
|
-
return true;
|
|
66
|
-
if (/^(?:tokens?|context window|remaining context|approvals?|sandbox|provider|session id):\s*/i.test(trimmed))
|
|
67
|
-
return true;
|
|
68
|
-
if (/^(?:thinking|working)\s*(?:\.\.\.|…)?$/i.test(trimmed))
|
|
69
|
-
return true;
|
|
70
|
-
if (/^[•◦·]\s+(?:thinking|working|running|planning|applying|reading|searching|inspecting|reviewing|summarizing|editing|updating|writing|completed)\b/i.test(trimmed))
|
|
71
|
-
return true;
|
|
72
|
-
if (/^(?:model|directory|tip|context|cwd|path):\s+/i.test(trimmed))
|
|
73
|
-
return true;
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
function extractCodexPromptCandidate(line) {
|
|
77
|
-
const trimmed = line.trim();
|
|
78
|
-
if (!/^›(?:\s|$)/.test(trimmed))
|
|
79
|
-
return null;
|
|
80
|
-
if (CODEX_FOOTER_RE.test(trimmed))
|
|
81
|
-
return null;
|
|
82
|
-
const prompt = normalizeCodexText(normalizeCodexPromptLine(trimmed));
|
|
83
|
-
if (!prompt || shouldIgnoreCodexLine(prompt))
|
|
84
|
-
return null;
|
|
85
|
-
return prompt;
|
|
86
|
-
}
|
|
87
|
-
function extractCodexAssistantCandidate(line) {
|
|
88
|
-
const trimmed = line.trim();
|
|
89
|
-
if (!/^[•◦·⏺]/.test(trimmed))
|
|
90
|
-
return null;
|
|
91
|
-
let assistant = normalizeCodexAssistantLine(trimmed);
|
|
92
|
-
if (!assistant || /^[•◦·⏺]$/.test(assistant))
|
|
93
|
-
return null;
|
|
94
|
-
assistant = assistant.replace(/\s*\(\d+[smh]?\s*•\s*esc to interrupt\)[\s\S]*$/i, "");
|
|
95
|
-
assistant = assistant.replace(/(?:[a-z]{1,6})?›[\s\S]*$/, "");
|
|
96
|
-
assistant = assistant.replace(/\s{2,}gpt-\d[\s\S]*$/i, "");
|
|
97
|
-
assistant = assistant.replace(/\b(?:OpenAI Codex|model:|directory:|Tip:)\b[\s\S]*$/i, "");
|
|
98
|
-
assistant = normalizeCodexText(assistant);
|
|
99
|
-
if (!assistant || assistant.length < 2 || CODEX_ACTIVITY_RE.test(assistant) || shouldIgnoreCodexLine(assistant)) {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
return assistant;
|
|
103
|
-
}
|
|
104
|
-
function extractCodexEchoCandidate(line) {
|
|
105
|
-
const trimmed = normalizeCodexText(line);
|
|
106
|
-
if (!trimmed || shouldIgnoreCodexLine(trimmed))
|
|
107
|
-
return null;
|
|
108
|
-
if (/^[•◦·⏺›]/.test(trimmed))
|
|
109
|
-
return null;
|
|
110
|
-
if (/^[\[\]<>0-9;?]+u?$/i.test(trimmed))
|
|
111
|
-
return null;
|
|
112
|
-
if (/^[╭╰│┌└┐┘├┤┬┴┼─═]/.test(trimmed))
|
|
113
|
-
return null;
|
|
114
|
-
if (trimmed.length > 500)
|
|
115
|
-
return null;
|
|
116
|
-
return trimmed;
|
|
117
|
-
}
|
|
118
|
-
function collectCodexCandidates() {
|
|
119
|
-
const candidates = [];
|
|
120
|
-
let order = 0;
|
|
121
|
-
for (const rawSegment of output.replace(/\r\n?/g, "\n").split("\n")) {
|
|
122
|
-
const cleanedSegment = stripCodexSegment(rawSegment);
|
|
123
|
-
const pieces = cleanedSegment.split("\n");
|
|
124
|
-
for (const piece of pieces) {
|
|
125
|
-
const line = piece.trim();
|
|
126
|
-
if (!line)
|
|
127
|
-
continue;
|
|
128
|
-
const prompt = extractCodexPromptCandidate(line);
|
|
129
|
-
if (prompt) {
|
|
130
|
-
candidates.push({ kind: "user", order, text: prompt });
|
|
131
|
-
order += 1;
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
const assistant = extractCodexAssistantCandidate(line);
|
|
135
|
-
if (assistant) {
|
|
136
|
-
candidates.push({ kind: "assistant", order, text: assistant });
|
|
137
|
-
order += 1;
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
const echo = extractCodexEchoCandidate(line);
|
|
141
|
-
if (echo) {
|
|
142
|
-
candidates.push({ kind: "echo", order, text: echo });
|
|
143
|
-
order += 1;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return candidates.filter((candidate, index, list) => {
|
|
148
|
-
const previous = list[index - 1];
|
|
149
|
-
return !previous || previous.kind !== candidate.kind || previous.text !== candidate.text;
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
function coalesceAssistantLines(lines) {
|
|
153
|
-
const collected = [];
|
|
154
|
-
for (const line of lines) {
|
|
155
|
-
const normalized = normalizeCodexText(line);
|
|
156
|
-
if (!normalized || normalized.length < 2 || shouldIgnoreCodexLine(normalized))
|
|
157
|
-
continue;
|
|
158
|
-
const previous = collected[collected.length - 1];
|
|
159
|
-
if (!previous) {
|
|
160
|
-
collected.push(normalized);
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
if (normalized === previous)
|
|
164
|
-
continue;
|
|
165
|
-
if (normalized.startsWith(previous)) {
|
|
166
|
-
collected[collected.length - 1] = normalized;
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
if (previous.startsWith(normalized)) {
|
|
170
|
-
if (isLikelyAssistantTailArtifact(previous, normalized)) {
|
|
171
|
-
collected[collected.length - 1] = normalized;
|
|
172
|
-
}
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
collected.push(normalized);
|
|
176
|
-
}
|
|
177
|
-
return collected.join("\n").trim();
|
|
178
|
-
}
|
|
179
|
-
function extractVisiblePrompt(lines) {
|
|
180
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
181
|
-
const line = lines[index].trim();
|
|
182
|
-
if (!line)
|
|
183
|
-
continue;
|
|
184
|
-
const inlinePrompt = extractCodexPromptCandidate(line);
|
|
185
|
-
if (inlinePrompt)
|
|
186
|
-
return inlinePrompt;
|
|
187
|
-
if (line === "›") {
|
|
188
|
-
for (let nextIndex = index + 1; nextIndex < lines.length; nextIndex += 1) {
|
|
189
|
-
const nextLine = normalizeCodexText(lines[nextIndex]);
|
|
190
|
-
if (!nextLine || CODEX_FOOTER_RE.test(nextLine) || shouldIgnoreCodexLine(nextLine))
|
|
191
|
-
continue;
|
|
192
|
-
return nextLine;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
return null;
|
|
197
|
-
}
|
|
198
|
-
function extractVisibleAssistantLines(lines) {
|
|
199
|
-
const assistantLines = [];
|
|
200
|
-
let collecting = false;
|
|
201
|
-
for (const rawLine of lines) {
|
|
202
|
-
const line = rawLine.trim();
|
|
203
|
-
if (!line) {
|
|
204
|
-
if (collecting)
|
|
205
|
-
break;
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
const assistant = extractCodexAssistantCandidate(line);
|
|
209
|
-
if (assistant) {
|
|
210
|
-
assistantLines.push(assistant);
|
|
211
|
-
collecting = true;
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
if (collecting) {
|
|
215
|
-
if (line === "›" ||
|
|
216
|
-
/^›(?:\s|$)/.test(line) ||
|
|
217
|
-
CODEX_FOOTER_RE.test(line) ||
|
|
218
|
-
shouldIgnoreCodexLine(line)) {
|
|
219
|
-
break;
|
|
220
|
-
}
|
|
221
|
-
assistantLines.push(normalizeCodexText(line));
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
return assistantLines;
|
|
225
|
-
}
|
|
226
|
-
const candidates = collectCodexCandidates();
|
|
227
|
-
const explicitUsers = candidates.filter((candidate) => candidate.kind === "user");
|
|
228
|
-
const assistantCandidates = candidates.filter((candidate) => candidate.kind === "assistant");
|
|
229
|
-
const echoCandidates = candidates.filter((candidate) => candidate.kind === "echo");
|
|
230
|
-
const strippedOutput = stripAnsi(output);
|
|
231
|
-
const strippedLines = strippedOutput.split("\n").map((line) => line.trimEnd());
|
|
232
|
-
const visiblePrompt = extractVisiblePrompt(strippedLines);
|
|
233
|
-
const latestExplicitUser = explicitUsers[explicitUsers.length - 1] ?? null;
|
|
234
|
-
const echoedUserCandidates = echoCandidates
|
|
235
|
-
.map((candidate) => candidate.text)
|
|
236
|
-
.filter((text) => text.length >= 3);
|
|
237
|
-
const latestEchoUser = [...echoedUserCandidates].reverse().find((text) => text !== visiblePrompt) ?? echoedUserCandidates[echoedUserCandidates.length - 1] ?? null;
|
|
238
|
-
const currentUser = latestExplicitUser?.text ?? latestEchoUser;
|
|
239
|
-
const rawAssistantLines = assistantCandidates
|
|
240
|
-
.filter((candidate) => !latestExplicitUser || candidate.order > latestExplicitUser.order)
|
|
241
|
-
.map((candidate) => candidate.text);
|
|
242
|
-
const visibleAssistantFallback = [...strippedOutput.matchAll(/^[ \t]*[•◦·⏺][ \t]*(.+)$/gm)]
|
|
243
|
-
.map((match) => normalizeCodexText(match[1] ?? ""))
|
|
244
|
-
.filter((line) => (!!line
|
|
245
|
-
&& !CODEX_ACTIVITY_RE.test(line)
|
|
246
|
-
&& !CODEX_FOOTER_RE.test(line)
|
|
247
|
-
&& !/\b(?:OpenAI Codex|model:|directory:|Tip:|esc to interrupt)\b/i.test(line)));
|
|
248
|
-
const assistant = coalesceAssistantLines(rawAssistantLines)
|
|
249
|
-
|| coalesceAssistantLines(extractVisibleAssistantLines(strippedLines))
|
|
250
|
-
|| visibleAssistantFallback[visibleAssistantFallback.length - 1]
|
|
251
|
-
|| null;
|
|
252
|
-
if (currentUser) {
|
|
253
|
-
messages.push({ role: "user", content: currentUser });
|
|
254
|
-
}
|
|
255
|
-
if (assistant) {
|
|
256
|
-
messages.push({ role: "assistant", content: assistant });
|
|
257
|
-
}
|
|
258
|
-
if (!messages.length && latestExplicitUser) {
|
|
259
|
-
messages.push({ role: "user", content: latestExplicitUser.text });
|
|
260
|
-
}
|
|
261
|
-
else if (!messages.length && latestEchoUser) {
|
|
262
|
-
messages.push({ role: "user", content: latestEchoUser });
|
|
263
|
-
}
|
|
264
|
-
const deduped = [];
|
|
265
|
-
for (const message of messages) {
|
|
266
|
-
const previous = deduped[deduped.length - 1];
|
|
267
|
-
if (!previous || previous.role !== message.role || previous.content !== message.content) {
|
|
268
|
-
deduped.push(message);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
return deduped;
|
|
272
|
-
}
|
|
273
|
-
export function parseMessages(output, command) {
|
|
274
|
-
const messages = [];
|
|
275
|
-
if (!output)
|
|
276
|
-
return messages;
|
|
277
|
-
if (isCodexCommand(command)) {
|
|
278
|
-
return parseCodexMessages(output);
|
|
279
|
-
}
|
|
280
|
-
const stripped = stripAnsi(output).replace(/\r/g, "\n");
|
|
281
|
-
const lines = stripped.split("\n");
|
|
282
|
-
const cleaned = lines.filter((line) => !isNoiseLine(line.trim()));
|
|
283
|
-
if (!cleaned.length)
|
|
284
|
-
return messages;
|
|
285
|
-
const turns = [];
|
|
286
|
-
let currentUserText = null;
|
|
287
|
-
let currentAssistantLines = [];
|
|
288
|
-
for (const rawLine of cleaned) {
|
|
289
|
-
const line = rawLine.trim();
|
|
290
|
-
if (line.startsWith("❯")) {
|
|
291
|
-
const afterPrompt = line.replace(/^❯\s*/, "").trim();
|
|
292
|
-
if (afterPrompt.startsWith("Try"))
|
|
293
|
-
continue;
|
|
294
|
-
if (currentUserText !== null && currentAssistantLines.length > 0) {
|
|
295
|
-
turns.push({ user: currentUserText, assistantLines: currentAssistantLines });
|
|
296
|
-
currentAssistantLines = [];
|
|
297
|
-
}
|
|
298
|
-
if (afterPrompt) {
|
|
299
|
-
currentUserText = afterPrompt;
|
|
300
|
-
}
|
|
301
|
-
else {
|
|
302
|
-
if (currentUserText !== null && currentAssistantLines.length > 0) {
|
|
303
|
-
turns.push({ user: currentUserText, assistantLines: currentAssistantLines });
|
|
304
|
-
currentAssistantLines = [];
|
|
305
|
-
}
|
|
306
|
-
currentUserText = null;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
else if (currentUserText !== null) {
|
|
310
|
-
const contentLine = rawLine.startsWith("⏺") ? rawLine.slice(1) : rawLine;
|
|
311
|
-
currentAssistantLines.push(contentLine);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
if (currentUserText !== null && currentAssistantLines.length > 0) {
|
|
315
|
-
turns.push({ user: currentUserText, assistantLines: currentAssistantLines });
|
|
316
|
-
}
|
|
317
|
-
else if (currentUserText !== null) {
|
|
318
|
-
// User input exists but no assistant response yet — still record the turn
|
|
319
|
-
turns.push({ user: currentUserText, assistantLines: currentAssistantLines });
|
|
320
|
-
}
|
|
321
|
-
for (const turn of turns) {
|
|
322
|
-
messages.push({ role: "user", content: turn.user });
|
|
323
|
-
const content = turn.assistantLines.join("\n").replace(/[ \t]+\n/g, "\n").replace(/[\n\s]+$/, "");
|
|
324
|
-
if (content) {
|
|
325
|
-
messages.push({ role: "assistant", content });
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
return messages;
|
|
329
|
-
}
|
package/dist/message-queue.d.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Message Queue for managing user inputs
|
|
3
|
-
* Inspired by Happy's MessageQueue2 implementation
|
|
4
|
-
*/
|
|
5
|
-
import type { QueuedMessage, AutonomyPolicy, ApprovalPolicy, EscalationScope } from "./types.js";
|
|
6
|
-
export interface MessageQueueOptions {
|
|
7
|
-
autonomyPolicy?: AutonomyPolicy;
|
|
8
|
-
approvalPolicy?: ApprovalPolicy;
|
|
9
|
-
allowedScopes?: EscalationScope[];
|
|
10
|
-
}
|
|
11
|
-
export declare class MessageQueue {
|
|
12
|
-
private queue;
|
|
13
|
-
private processing;
|
|
14
|
-
private onMessageCallback;
|
|
15
|
-
private lastMessageId;
|
|
16
|
-
/**
|
|
17
|
-
* Add a message to the queue
|
|
18
|
-
*/
|
|
19
|
-
enqueue(content: string, options?: MessageQueueOptions): QueuedMessage;
|
|
20
|
-
/**
|
|
21
|
-
* Add a high-priority message (like /compact, /clear)
|
|
22
|
-
*/
|
|
23
|
-
enqueuePriority(content: string, options?: MessageQueueOptions): QueuedMessage;
|
|
24
|
-
/**
|
|
25
|
-
* Clear the queue and add a new message
|
|
26
|
-
* Used for /compact and /clear commands
|
|
27
|
-
*/
|
|
28
|
-
clearAndEnqueue(content: string, options?: MessageQueueOptions): QueuedMessage;
|
|
29
|
-
/**
|
|
30
|
-
* Set the message handler
|
|
31
|
-
*/
|
|
32
|
-
onMessage(handler: (message: QueuedMessage) => Promise<void>): void;
|
|
33
|
-
/**
|
|
34
|
-
* Process the next message in the queue
|
|
35
|
-
*/
|
|
36
|
-
private processNext;
|
|
37
|
-
/**
|
|
38
|
-
* Get the current queue length
|
|
39
|
-
*/
|
|
40
|
-
get length(): number;
|
|
41
|
-
/**
|
|
42
|
-
* Check if the queue is empty
|
|
43
|
-
*/
|
|
44
|
-
get isEmpty(): boolean;
|
|
45
|
-
/**
|
|
46
|
-
* Check if a message is being processed
|
|
47
|
-
*/
|
|
48
|
-
get isProcessing(): boolean;
|
|
49
|
-
/**
|
|
50
|
-
* Clear all pending messages
|
|
51
|
-
*/
|
|
52
|
-
clear(): void;
|
|
53
|
-
/**
|
|
54
|
-
* Get all pending messages (for debugging)
|
|
55
|
-
*/
|
|
56
|
-
getPending(): QueuedMessage[];
|
|
57
|
-
}
|
package/dist/message-queue.js
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Message Queue for managing user inputs
|
|
3
|
-
* Inspired by Happy's MessageQueue2 implementation
|
|
4
|
-
*/
|
|
5
|
-
export class MessageQueue {
|
|
6
|
-
queue = [];
|
|
7
|
-
processing = false;
|
|
8
|
-
onMessageCallback = null;
|
|
9
|
-
lastMessageId = 0;
|
|
10
|
-
/**
|
|
11
|
-
* Add a message to the queue
|
|
12
|
-
*/
|
|
13
|
-
enqueue(content, options = {}) {
|
|
14
|
-
const message = {
|
|
15
|
-
id: `msg-${++this.lastMessageId}-${Date.now()}`,
|
|
16
|
-
content,
|
|
17
|
-
priority: 1,
|
|
18
|
-
timestamp: Date.now(),
|
|
19
|
-
metadata: {
|
|
20
|
-
autonomyPolicy: options.autonomyPolicy,
|
|
21
|
-
approvalPolicy: options.approvalPolicy,
|
|
22
|
-
allowedScopes: options.allowedScopes,
|
|
23
|
-
},
|
|
24
|
-
};
|
|
25
|
-
this.queue.push(message);
|
|
26
|
-
this.processNext();
|
|
27
|
-
return message;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Add a high-priority message (like /compact, /clear)
|
|
31
|
-
*/
|
|
32
|
-
enqueuePriority(content, options = {}) {
|
|
33
|
-
const message = {
|
|
34
|
-
id: `msg-priority-${++this.lastMessageId}-${Date.now()}`,
|
|
35
|
-
content,
|
|
36
|
-
priority: 10,
|
|
37
|
-
timestamp: Date.now(),
|
|
38
|
-
isolate: true,
|
|
39
|
-
metadata: {
|
|
40
|
-
autonomyPolicy: options.autonomyPolicy,
|
|
41
|
-
approvalPolicy: options.approvalPolicy,
|
|
42
|
-
allowedScopes: options.allowedScopes,
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
// Insert at the beginning of the queue
|
|
46
|
-
this.queue.unshift(message);
|
|
47
|
-
this.processNext();
|
|
48
|
-
return message;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Clear the queue and add a new message
|
|
52
|
-
* Used for /compact and /clear commands
|
|
53
|
-
*/
|
|
54
|
-
clearAndEnqueue(content, options = {}) {
|
|
55
|
-
const clearedCount = this.queue.length;
|
|
56
|
-
this.queue = [];
|
|
57
|
-
if (clearedCount > 0) {
|
|
58
|
-
console.error(`[MessageQueue] Cleared ${clearedCount} pending messages`);
|
|
59
|
-
}
|
|
60
|
-
return this.enqueuePriority(content, options);
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Set the message handler
|
|
64
|
-
*/
|
|
65
|
-
onMessage(handler) {
|
|
66
|
-
this.onMessageCallback = handler;
|
|
67
|
-
this.processNext();
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Process the next message in the queue
|
|
71
|
-
*/
|
|
72
|
-
async processNext() {
|
|
73
|
-
if (this.processing || this.queue.length === 0 || !this.onMessageCallback) {
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
this.processing = true;
|
|
77
|
-
// Sort by priority (higher first), then by timestamp
|
|
78
|
-
this.queue.sort((a, b) => {
|
|
79
|
-
if (a.priority !== b.priority) {
|
|
80
|
-
return b.priority - a.priority;
|
|
81
|
-
}
|
|
82
|
-
return a.timestamp - b.timestamp;
|
|
83
|
-
});
|
|
84
|
-
const message = this.queue.shift();
|
|
85
|
-
try {
|
|
86
|
-
await this.onMessageCallback(message);
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
console.error(`[MessageQueue] Error processing message ${message.id}:`, error);
|
|
90
|
-
}
|
|
91
|
-
finally {
|
|
92
|
-
this.processing = false;
|
|
93
|
-
// Process next message
|
|
94
|
-
this.processNext();
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Get the current queue length
|
|
99
|
-
*/
|
|
100
|
-
get length() {
|
|
101
|
-
return this.queue.length;
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Check if the queue is empty
|
|
105
|
-
*/
|
|
106
|
-
get isEmpty() {
|
|
107
|
-
return this.queue.length === 0;
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Check if a message is being processed
|
|
111
|
-
*/
|
|
112
|
-
get isProcessing() {
|
|
113
|
-
return this.processing;
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Clear all pending messages
|
|
117
|
-
*/
|
|
118
|
-
clear() {
|
|
119
|
-
this.queue = [];
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Get all pending messages (for debugging)
|
|
123
|
-
*/
|
|
124
|
-
getPending() {
|
|
125
|
-
return [...this.queue];
|
|
126
|
-
}
|
|
127
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session Lifecycle Manager
|
|
3
|
-
* Inspired by Happy's session lifecycle management
|
|
4
|
-
*/
|
|
5
|
-
import type { SessionLifecycleState, SessionLifecycle } from "./types.js";
|
|
6
|
-
export interface SessionLifecycleEvents {
|
|
7
|
-
onStateChange?: (sessionId: string, oldState: SessionLifecycleState, newState: SessionLifecycleState) => void;
|
|
8
|
-
onIdle?: (sessionId: string) => void;
|
|
9
|
-
onArchived?: (sessionId: string, reason: string) => void;
|
|
10
|
-
}
|
|
11
|
-
export declare class SessionLifecycleManager {
|
|
12
|
-
private sessions;
|
|
13
|
-
private events;
|
|
14
|
-
private idleTimeout;
|
|
15
|
-
private archiveTimeout;
|
|
16
|
-
private checkInterval;
|
|
17
|
-
constructor(events?: SessionLifecycleEvents, options?: {
|
|
18
|
-
idleTimeout?: number;
|
|
19
|
-
archiveTimeout?: number;
|
|
20
|
-
});
|
|
21
|
-
/**
|
|
22
|
-
* Register a new session
|
|
23
|
-
*/
|
|
24
|
-
register(sessionId: string, initialState?: SessionLifecycleState): void;
|
|
25
|
-
/**
|
|
26
|
-
* Update session state
|
|
27
|
-
*/
|
|
28
|
-
setState(sessionId: string, newState: SessionLifecycleState): void;
|
|
29
|
-
/**
|
|
30
|
-
* Update last activity timestamp
|
|
31
|
-
*/
|
|
32
|
-
touch(sessionId: string): void;
|
|
33
|
-
/**
|
|
34
|
-
* Mark session as thinking
|
|
35
|
-
*/
|
|
36
|
-
startThinking(sessionId: string): void;
|
|
37
|
-
/**
|
|
38
|
-
* Mark session as done thinking
|
|
39
|
-
*/
|
|
40
|
-
stopThinking(sessionId: string): void;
|
|
41
|
-
/**
|
|
42
|
-
* Mark session as waiting for input
|
|
43
|
-
*/
|
|
44
|
-
waitingInput(sessionId: string): void;
|
|
45
|
-
/**
|
|
46
|
-
* Archive a session
|
|
47
|
-
*/
|
|
48
|
-
archive(sessionId: string, reason: string, by?: "user" | "timeout" | "error"): void;
|
|
49
|
-
/**
|
|
50
|
-
* Unregister a session
|
|
51
|
-
*/
|
|
52
|
-
unregister(sessionId: string): void;
|
|
53
|
-
/**
|
|
54
|
-
* Get session lifecycle
|
|
55
|
-
*/
|
|
56
|
-
get(sessionId: string): SessionLifecycle | undefined;
|
|
57
|
-
/**
|
|
58
|
-
* Get all sessions
|
|
59
|
-
*/
|
|
60
|
-
getAll(): Map<string, SessionLifecycle>;
|
|
61
|
-
/**
|
|
62
|
-
* Get sessions by state
|
|
63
|
-
*/
|
|
64
|
-
getByState(state: SessionLifecycleState): string[];
|
|
65
|
-
/**
|
|
66
|
-
* Start periodic check for idle/archived sessions
|
|
67
|
-
*/
|
|
68
|
-
private startPeriodicCheck;
|
|
69
|
-
/**
|
|
70
|
-
* Stop periodic check
|
|
71
|
-
*/
|
|
72
|
-
stopPeriodicCheck(): void;
|
|
73
|
-
/**
|
|
74
|
-
* Check sessions for idle/archived status
|
|
75
|
-
*/
|
|
76
|
-
private checkSessions;
|
|
77
|
-
/**
|
|
78
|
-
* Cleanup all sessions
|
|
79
|
-
*/
|
|
80
|
-
cleanup(): void;
|
|
81
|
-
}
|