@lwmxiaobei/xbcode 1.0.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.
@@ -0,0 +1,280 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import lockfile from "proper-lockfile";
4
+ const STATUS_SYMBOLS = {
5
+ pending: "[ ]",
6
+ assigned: "[=]",
7
+ in_progress: "[>]",
8
+ blocked: "[! ]",
9
+ completed: "[x]",
10
+ failed: "[-]",
11
+ };
12
+ const LOCK_OPTIONS = {
13
+ retries: {
14
+ retries: 100,
15
+ minTimeout: 5,
16
+ maxTimeout: 200,
17
+ },
18
+ };
19
+ function createThreadId(taskId) {
20
+ return `task_${taskId}`;
21
+ }
22
+ export class TaskManager {
23
+ dir;
24
+ lockFilePath;
25
+ constructor(tasksDir) {
26
+ this.dir = tasksDir;
27
+ fs.mkdirSync(this.dir, { recursive: true });
28
+ this.lockFilePath = path.join(this.dir, "tasks.lock");
29
+ if (!fs.existsSync(this.lockFilePath)) {
30
+ fs.writeFileSync(this.lockFilePath, "", "utf8");
31
+ }
32
+ }
33
+ async withLock(fn) {
34
+ const release = await lockfile.lock(this.lockFilePath, LOCK_OPTIONS);
35
+ try {
36
+ return await fn();
37
+ }
38
+ finally {
39
+ await release();
40
+ }
41
+ }
42
+ nextId() {
43
+ const files = fs.readdirSync(this.dir).filter((f) => /^task_\d+\.json$/.test(f));
44
+ if (files.length === 0)
45
+ return 1;
46
+ const ids = files.map((f) => Number(f.match(/task_(\d+)\.json/)?.[1] ?? 0));
47
+ return Math.max(...ids) + 1;
48
+ }
49
+ taskPath(id) {
50
+ return path.join(this.dir, `task_${id}.json`);
51
+ }
52
+ normalizeTask(task) {
53
+ const now = new Date().toISOString();
54
+ return {
55
+ id: task.id,
56
+ subject: task.subject,
57
+ description: task.description ?? "",
58
+ status: task.status ?? "pending",
59
+ owner: task.owner ?? "lead",
60
+ assignee: task.assignee,
61
+ threadId: task.threadId ?? createThreadId(task.id),
62
+ resultSummary: task.resultSummary,
63
+ blockedReason: task.blockedReason,
64
+ createdAt: task.createdAt ?? now,
65
+ updatedAt: task.updatedAt ?? task.createdAt ?? now,
66
+ blockedBy: Array.isArray(task.blockedBy) ? task.blockedBy : [],
67
+ blocks: Array.isArray(task.blocks) ? task.blocks : [],
68
+ };
69
+ }
70
+ load(id) {
71
+ const content = fs.readFileSync(this.taskPath(id), "utf8");
72
+ return this.normalizeTask(JSON.parse(content));
73
+ }
74
+ save(task) {
75
+ const normalized = this.normalizeTask(task);
76
+ fs.writeFileSync(this.taskPath(normalized.id), JSON.stringify(normalized, null, 2), "utf8");
77
+ }
78
+ allTasks() {
79
+ const files = fs.readdirSync(this.dir).filter((f) => /^task_\d+\.json$/.test(f));
80
+ return files.map((f) => {
81
+ const id = Number(f.match(/task_(\d+)\.json/)?.[1] ?? 0);
82
+ return this.load(id);
83
+ });
84
+ }
85
+ getTaskInternal(taskId) {
86
+ try {
87
+ return this.load(taskId);
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ }
93
+ async create(subject, description, owner = "lead", assignee) {
94
+ return this.withLock(() => {
95
+ const id = this.nextId();
96
+ const now = new Date().toISOString();
97
+ const task = {
98
+ id,
99
+ subject,
100
+ description: description ?? "",
101
+ status: assignee ? "assigned" : "pending",
102
+ owner,
103
+ assignee,
104
+ threadId: createThreadId(id),
105
+ createdAt: now,
106
+ updatedAt: now,
107
+ blockedBy: [],
108
+ blocks: [],
109
+ };
110
+ this.save(task);
111
+ return JSON.stringify(task, null, 2);
112
+ });
113
+ }
114
+ async update(taskId, status, addBlockedBy, addBlocks, assignee, resultSummary, blockedReason) {
115
+ return this.withLock(() => {
116
+ let task;
117
+ try {
118
+ task = this.load(taskId);
119
+ }
120
+ catch {
121
+ return `Error: Task ${taskId} not found.`;
122
+ }
123
+ if (addBlockedBy) {
124
+ for (const depId of addBlockedBy) {
125
+ if (!task.blockedBy.includes(depId))
126
+ task.blockedBy.push(depId);
127
+ try {
128
+ const dep = this.load(depId);
129
+ if (!dep.blocks.includes(taskId)) {
130
+ dep.blocks.push(taskId);
131
+ dep.updatedAt = new Date().toISOString();
132
+ this.save(dep);
133
+ }
134
+ }
135
+ catch { }
136
+ }
137
+ }
138
+ if (addBlocks) {
139
+ for (const depId of addBlocks) {
140
+ if (!task.blocks.includes(depId))
141
+ task.blocks.push(depId);
142
+ try {
143
+ const dep = this.load(depId);
144
+ if (!dep.blockedBy.includes(taskId)) {
145
+ dep.blockedBy.push(taskId);
146
+ dep.updatedAt = new Date().toISOString();
147
+ this.save(dep);
148
+ }
149
+ }
150
+ catch { }
151
+ }
152
+ }
153
+ if (typeof assignee === "string" && assignee.trim()) {
154
+ task.assignee = assignee.trim();
155
+ if (task.status === "pending") {
156
+ task.status = "assigned";
157
+ }
158
+ }
159
+ if (typeof resultSummary === "string") {
160
+ task.resultSummary = resultSummary;
161
+ }
162
+ if (typeof blockedReason === "string") {
163
+ task.blockedReason = blockedReason;
164
+ }
165
+ if (status) {
166
+ task.status = status;
167
+ if (status !== "blocked") {
168
+ task.blockedReason = blockedReason ?? task.blockedReason;
169
+ }
170
+ }
171
+ task.updatedAt = new Date().toISOString();
172
+ this.save(task);
173
+ if (task.status === "completed") {
174
+ this.clearDependency(task.id);
175
+ }
176
+ return JSON.stringify(task, null, 2);
177
+ });
178
+ }
179
+ clearDependency(completedId) {
180
+ const all = this.allTasks();
181
+ for (const t of all) {
182
+ const idx = t.blockedBy.indexOf(completedId);
183
+ if (idx !== -1) {
184
+ t.blockedBy.splice(idx, 1);
185
+ t.updatedAt = new Date().toISOString();
186
+ this.save(t);
187
+ }
188
+ }
189
+ }
190
+ async list() {
191
+ return this.withLock(() => {
192
+ const tasks = this.allTasks();
193
+ if (tasks.length === 0)
194
+ return "(no tasks)";
195
+ return tasks
196
+ .map((t) => {
197
+ let line = `${STATUS_SYMBOLS[t.status]} #${t.id}: ${t.subject}`;
198
+ if (t.assignee)
199
+ line += ` assignee=${t.assignee}`;
200
+ if (t.blockedBy.length > 0)
201
+ line += ` (blocked by: ${t.blockedBy.join(", ")})`;
202
+ if (t.blockedReason)
203
+ line += ` reason=${t.blockedReason}`;
204
+ return line;
205
+ })
206
+ .join("\n");
207
+ });
208
+ }
209
+ async getTask(taskId) {
210
+ return this.withLock(() => {
211
+ return this.getTaskInternal(taskId);
212
+ });
213
+ }
214
+ async listTasks() {
215
+ return this.withLock(() => {
216
+ return this.allTasks().sort((left, right) => left.id - right.id);
217
+ });
218
+ }
219
+ async listAssignedTo(name) {
220
+ return this.withLock(() => {
221
+ const normalized = name.trim();
222
+ return this.allTasks()
223
+ .filter((task) => task.assignee === normalized && ["assigned", "in_progress", "blocked"].includes(task.status))
224
+ .sort((left, right) => left.id - right.id);
225
+ });
226
+ }
227
+ async claimNext(assignee) {
228
+ return this.withLock(() => {
229
+ const nextTask = this.allTasks()
230
+ .filter((task) => task.assignee === assignee && task.status === "assigned")
231
+ .sort((left, right) => left.id - right.id)[0];
232
+ if (!nextTask) {
233
+ return null;
234
+ }
235
+ nextTask.status = "in_progress";
236
+ nextTask.updatedAt = new Date().toISOString();
237
+ this.save(nextTask);
238
+ return nextTask;
239
+ });
240
+ }
241
+ async formatTask(taskId) {
242
+ return this.withLock(() => {
243
+ const task = this.getTaskInternal(taskId);
244
+ if (!task) {
245
+ return `Error: Task ${taskId} not found.`;
246
+ }
247
+ const lines = [
248
+ `#${task.id} ${task.subject}`,
249
+ `status=${task.status}`,
250
+ `owner=${task.owner}`,
251
+ `assignee=${task.assignee ?? "-"}`,
252
+ `thread=${task.threadId}`,
253
+ `created_at=${task.createdAt}`,
254
+ `updated_at=${task.updatedAt}`,
255
+ ];
256
+ if (task.description)
257
+ lines.push("", task.description);
258
+ if (task.resultSummary)
259
+ lines.push("", `result: ${task.resultSummary}`);
260
+ if (task.blockedReason)
261
+ lines.push("", `blocked: ${task.blockedReason}`);
262
+ if (task.blockedBy.length > 0)
263
+ lines.push(`blocked_by=${task.blockedBy.join(", ")}`);
264
+ if (task.blocks.length > 0)
265
+ lines.push(`blocks=${task.blocks.join(", ")}`);
266
+ return lines.join("\n");
267
+ });
268
+ }
269
+ async get(taskId) {
270
+ return this.withLock(() => {
271
+ const task = this.getTaskInternal(taskId);
272
+ return task ? JSON.stringify(task, null, 2) : `Error: Task ${taskId} not found.`;
273
+ });
274
+ }
275
+ async hasActiveTasks() {
276
+ return this.withLock(() => {
277
+ return this.allTasks().some((t) => ["pending", "assigned", "in_progress", "blocked"].includes(t.status));
278
+ });
279
+ }
280
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,266 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createSessionId } from "./session-store.js";
4
+ function sortMembers(members) {
5
+ return [...members].sort((left, right) => left.name.localeCompare(right.name));
6
+ }
7
+ export class TeammateManager {
8
+ teamDir;
9
+ messageBus;
10
+ leadName;
11
+ configPath;
12
+ runtimeControls = new Map();
13
+ constructor(teamDir, messageBus, leadName = "lead") {
14
+ this.teamDir = teamDir;
15
+ this.messageBus = messageBus;
16
+ this.leadName = leadName;
17
+ fs.mkdirSync(this.teamDir, { recursive: true });
18
+ this.configPath = path.join(this.teamDir, "config.json");
19
+ this.ensureConfig();
20
+ this.messageBus.ensureInbox(this.leadName);
21
+ this.resetEphemeralStatuses();
22
+ }
23
+ getLeadName() {
24
+ return this.leadName;
25
+ }
26
+ defaultConfig() {
27
+ return {
28
+ version: 2,
29
+ leadName: this.leadName,
30
+ members: [],
31
+ };
32
+ }
33
+ ensureConfig() {
34
+ if (fs.existsSync(this.configPath)) {
35
+ return;
36
+ }
37
+ fs.writeFileSync(this.configPath, `${JSON.stringify(this.defaultConfig(), null, 2)}\n`, "utf8");
38
+ }
39
+ loadConfig() {
40
+ this.ensureConfig();
41
+ try {
42
+ const content = fs.readFileSync(this.configPath, "utf8");
43
+ const parsed = JSON.parse(content);
44
+ return {
45
+ version: 2,
46
+ leadName: this.leadName,
47
+ members: Array.isArray(parsed.members) ? sortMembers(parsed.members) : [],
48
+ };
49
+ }
50
+ catch {
51
+ return this.defaultConfig();
52
+ }
53
+ }
54
+ saveConfig(config) {
55
+ const normalized = {
56
+ version: 2,
57
+ leadName: this.leadName,
58
+ members: sortMembers(config.members),
59
+ };
60
+ fs.writeFileSync(this.configPath, `${JSON.stringify(normalized, null, 2)}\n`, "utf8");
61
+ }
62
+ resetEphemeralStatuses() {
63
+ const config = this.loadConfig();
64
+ let changed = false;
65
+ for (const member of config.members) {
66
+ if (member.status !== "stopped") {
67
+ member.status = "stopped";
68
+ member.currentTaskId = undefined;
69
+ member.currentThreadId = undefined;
70
+ changed = true;
71
+ }
72
+ }
73
+ if (changed) {
74
+ this.saveConfig(config);
75
+ }
76
+ }
77
+ createRuntimeState(name, role) {
78
+ return {
79
+ name,
80
+ role,
81
+ sessionId: createSessionId(),
82
+ responseHistory: [],
83
+ chatHistory: [],
84
+ turnCount: 0,
85
+ launchedAt: Date.now(),
86
+ roundsSinceTask: 0,
87
+ compactCount: 0,
88
+ };
89
+ }
90
+ listMembers() {
91
+ return this.loadConfig().members;
92
+ }
93
+ getMember(name) {
94
+ return this.listMembers().find((member) => member.name === name);
95
+ }
96
+ ensureMember(name, role) {
97
+ const now = new Date().toISOString();
98
+ const config = this.loadConfig();
99
+ const existing = config.members.find((member) => member.name === name);
100
+ if (existing) {
101
+ existing.role = role;
102
+ existing.lastActiveAt = now;
103
+ existing.lastError = undefined;
104
+ if (existing.status === "error" || existing.status === "stopped") {
105
+ existing.status = "idle";
106
+ }
107
+ this.saveConfig(config);
108
+ this.messageBus.ensureInbox(name);
109
+ return existing;
110
+ }
111
+ const member = {
112
+ name,
113
+ role,
114
+ status: "idle",
115
+ createdAt: now,
116
+ lastActiveAt: now,
117
+ };
118
+ config.members.push(member);
119
+ this.saveConfig(config);
120
+ this.messageBus.ensureInbox(name);
121
+ return member;
122
+ }
123
+ setStatus(name, status, lastError) {
124
+ const config = this.loadConfig();
125
+ const member = config.members.find((entry) => entry.name === name);
126
+ if (!member) {
127
+ return;
128
+ }
129
+ member.status = status;
130
+ member.lastActiveAt = new Date().toISOString();
131
+ if (lastError) {
132
+ member.lastError = lastError;
133
+ }
134
+ else if (status !== "error") {
135
+ member.lastError = undefined;
136
+ }
137
+ if (status === "idle" || status === "stopped") {
138
+ member.currentTaskId = undefined;
139
+ member.currentThreadId = undefined;
140
+ }
141
+ this.saveConfig(config);
142
+ }
143
+ setCurrentWork(name, taskId, threadId, summary) {
144
+ const config = this.loadConfig();
145
+ const member = config.members.find((entry) => entry.name === name);
146
+ if (!member) {
147
+ return;
148
+ }
149
+ member.lastActiveAt = new Date().toISOString();
150
+ member.currentTaskId = taskId;
151
+ member.currentThreadId = threadId;
152
+ if (typeof summary === "string") {
153
+ member.lastSummary = summary;
154
+ }
155
+ this.saveConfig(config);
156
+ }
157
+ markWorking(name) {
158
+ this.setStatus(name, "working");
159
+ }
160
+ markBlocked(name, summary) {
161
+ this.setStatus(name, "blocked");
162
+ if (summary) {
163
+ this.setCurrentWork(name, this.getMember(name)?.currentTaskId, this.getMember(name)?.currentThreadId, summary);
164
+ }
165
+ }
166
+ markIdle(name) {
167
+ this.setStatus(name, "idle");
168
+ }
169
+ markStopped(name) {
170
+ this.setStatus(name, "stopped");
171
+ }
172
+ markError(name, message) {
173
+ this.setStatus(name, "error", message);
174
+ }
175
+ isRunning(name) {
176
+ return Boolean(this.runtimeControls.get(name)?.running);
177
+ }
178
+ startRuntime(name, role, runner) {
179
+ const current = this.runtimeControls.get(name);
180
+ if (current?.running) {
181
+ return { started: false, control: current };
182
+ }
183
+ const control = {
184
+ name,
185
+ role,
186
+ stopRequested: false,
187
+ waiters: new Set(),
188
+ running: undefined,
189
+ state: this.createRuntimeState(name, role),
190
+ };
191
+ this.runtimeControls.set(name, control);
192
+ control.running = Promise.resolve()
193
+ .then(() => runner(control))
194
+ .catch((error) => {
195
+ const message = error instanceof Error ? error.message : String(error);
196
+ this.markError(name, message);
197
+ // P1:删除 task_failed 协议字段(eventType/taskId/threadId),仅给 lead 邮箱
198
+ // 写入人类可读的失败通知。任务级失败状态依然通过 task manager 体现。
199
+ // void:runner 失败本身已由 markError 记录,这里 send 失败也只是丢条通知,不阻断。
200
+ void this.messageBus.send({
201
+ from: name,
202
+ to: this.leadName,
203
+ content: `Teammate ${name} failed: ${message}`,
204
+ });
205
+ })
206
+ .finally(() => {
207
+ control.running = undefined;
208
+ control.waiters.clear();
209
+ const currentMember = this.getMember(name);
210
+ if (currentMember && currentMember.status !== "error") {
211
+ this.markStopped(name);
212
+ }
213
+ });
214
+ return { started: true, control };
215
+ }
216
+ wake(name) {
217
+ const control = this.runtimeControls.get(name);
218
+ if (!control) {
219
+ return;
220
+ }
221
+ const waiters = [...control.waiters];
222
+ control.waiters.clear();
223
+ for (const waiter of waiters) {
224
+ waiter();
225
+ }
226
+ }
227
+ async waitForWake(control) {
228
+ // 进入等待前先看一眼未读数:若有未读消息则不应该睡,立刻返回让上层 drain。
229
+ if (control.stopRequested || (await this.messageBus.unreadCount(control.name)) > 0) {
230
+ return;
231
+ }
232
+ await new Promise((resolve) => {
233
+ control.waiters.add(resolve);
234
+ });
235
+ }
236
+ requestStop(name) {
237
+ const control = this.runtimeControls.get(name);
238
+ if (!control) {
239
+ this.markStopped(name);
240
+ return false;
241
+ }
242
+ control.stopRequested = true;
243
+ this.wake(name);
244
+ return true;
245
+ }
246
+ shouldStop(control) {
247
+ return control.stopRequested;
248
+ }
249
+ // P1:异步化。inboxSize 同步 API 已废弃,改为 await unreadCount。
250
+ // 用 Promise.all 并发查询所有成员未读数,避免 N 次串行 fs 读放大延迟。
251
+ async formatTeamStatus() {
252
+ const members = this.listMembers();
253
+ if (members.length === 0) {
254
+ return `team_dir ${this.teamDir}\n(no teammates)`;
255
+ }
256
+ const lines = await Promise.all(members.map(async (member) => {
257
+ const inboxSize = await this.messageBus.unreadCount(member.name);
258
+ const errorSuffix = member.lastError ? ` error=${member.lastError}` : "";
259
+ const workSuffix = member.currentTaskId ? ` task=${member.currentTaskId}` : "";
260
+ const threadSuffix = member.currentThreadId ? ` thread=${member.currentThreadId}` : "";
261
+ const summarySuffix = member.lastSummary ? ` summary=${member.lastSummary}` : "";
262
+ return `- ${member.name} [${member.status}] role=${member.role} inbox=${inboxSize} last_active=${member.lastActiveAt}${workSuffix}${threadSuffix}${summarySuffix}${errorSuffix}`;
263
+ }));
264
+ return [`team_dir ${this.teamDir}`, ...lines].join("\n");
265
+ }
266
+ }