ework-daemon 0.4.23 → 0.4.25
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/package.json +1 -1
- package/src/config.ts +4 -1
- package/src/gitea.ts +2 -2
- package/src/op.ts +8 -0
- package/src/opencode.ts +41 -2
- package/src/trackers/gitea-tracker.ts +5 -0
- package/src/trackers/types.ts +1 -0
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -36,6 +36,7 @@ export const configSchema = z.object({
|
|
|
36
36
|
}),
|
|
37
37
|
work: z.object({
|
|
38
38
|
capacity: z.coerce.number().int().positive().default(4),
|
|
39
|
+
maxConcurrent: z.coerce.number().int().positive().default(4),
|
|
39
40
|
heartbeatMs: z.coerce.number().int().positive().default(10_000),
|
|
40
41
|
leaseTtlMs: z.coerce.number().int().positive().default(60_000),
|
|
41
42
|
}),
|
|
@@ -85,8 +86,10 @@ const TEST_DEFAULTS = {
|
|
|
85
86
|
};
|
|
86
87
|
|
|
87
88
|
function readWorkSection() {
|
|
89
|
+
const capacity = process.env.WORK_DAEMON_CAPACITY ? Number(process.env.WORK_DAEMON_CAPACITY) : 4;
|
|
88
90
|
return {
|
|
89
|
-
capacity
|
|
91
|
+
capacity,
|
|
92
|
+
maxConcurrent: process.env.WORK_MAX_CONCURRENT ? Number(process.env.WORK_MAX_CONCURRENT) : capacity,
|
|
90
93
|
heartbeatMs: process.env.WORK_DAEMON_HEARTBEAT_MS ? Number(process.env.WORK_DAEMON_HEARTBEAT_MS) : 10_000,
|
|
91
94
|
leaseTtlMs: process.env.WORK_DAEMON_LEASE_TTL_MS ? Number(process.env.WORK_DAEMON_LEASE_TTL_MS) : 60_000,
|
|
92
95
|
};
|
package/src/gitea.ts
CHANGED
|
@@ -75,8 +75,8 @@ export class GiteaClient {
|
|
|
75
75
|
status,
|
|
76
76
|
detail,
|
|
77
77
|
}, true);
|
|
78
|
-
} catch {
|
|
79
|
-
|
|
78
|
+
} catch (e) {
|
|
79
|
+
console.warn("[gitea] updateIssueStatus failed:", (e as Error).message);
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
package/src/op.ts
CHANGED
|
@@ -299,6 +299,14 @@ export class Store {
|
|
|
299
299
|
return row ? rowToMessage(row) : undefined;
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
+
async getGlobalPendingMessages(limit: number): Promise<Message[]> {
|
|
303
|
+
const rows = await getDB().all<MessageRow>(
|
|
304
|
+
"SELECT * FROM {{messages}} WHERE status = 'pending' ORDER BY created_at ASC LIMIT ?",
|
|
305
|
+
[limit]
|
|
306
|
+
);
|
|
307
|
+
return rows.map(rowToMessage);
|
|
308
|
+
}
|
|
309
|
+
|
|
302
310
|
async updateMessageStatus(id: string, status: Message["status"], error?: string): Promise<void> {
|
|
303
311
|
const row = await getDB().get<MessageRow>("SELECT * FROM {{messages}} WHERE uid = ?", [id]);
|
|
304
312
|
const attempts = row ? row.attempts + (status === "failed" ? 1 : 0) : 0;
|
package/src/opencode.ts
CHANGED
|
@@ -561,6 +561,11 @@ export class Engine {
|
|
|
561
561
|
return;
|
|
562
562
|
}
|
|
563
563
|
|
|
564
|
+
if (issueData.ai_status === "halted" && (event.type === "issue_opened" || event.type === "comment_created")) {
|
|
565
|
+
log.info(`engine: issue halted — skipping ${event.type} for ${ref.trackerType}:${scopeKey}#${ref.issueId}`);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
564
569
|
const issueMapKey = `${ref.trackerType}:${scopeKey}#${ref.issueId}`;
|
|
565
570
|
if (groupConfig) {
|
|
566
571
|
this.groupConfigs.set(issueMapKey, groupConfig);
|
|
@@ -884,6 +889,11 @@ export class Engine {
|
|
|
884
889
|
return;
|
|
885
890
|
}
|
|
886
891
|
|
|
892
|
+
if (this.running.size >= this.cfg.work.maxConcurrent) {
|
|
893
|
+
log.info(`engine: concurrency limit reached (${this.running.size}/${this.cfg.work.maxConcurrent}), message ${msg.id.slice(0, 8)} queued for ${k}`);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
|
|
887
897
|
// Not running — execute directly
|
|
888
898
|
await this.executeMessage(k, session, issue, msg);
|
|
889
899
|
}
|
|
@@ -1163,6 +1173,7 @@ export class Engine {
|
|
|
1163
1173
|
if (exitCode === null) {
|
|
1164
1174
|
log.info(`engine: spawn failed for ${k}, skipping completion check`);
|
|
1165
1175
|
await this.store.updateSession(session.id, { opencodePid: undefined });
|
|
1176
|
+
void tracker.updateStatus(ref, "failed", "spawn failed");
|
|
1166
1177
|
await this.deactivateIfIdle(k, session, issue);
|
|
1167
1178
|
return;
|
|
1168
1179
|
}
|
|
@@ -1185,6 +1196,7 @@ export class Engine {
|
|
|
1185
1196
|
this.nudgeRounds.delete(k);
|
|
1186
1197
|
this.emptyResponseRounds.delete(k);
|
|
1187
1198
|
await this.persistRuntimeState(session.id);
|
|
1199
|
+
void tracker.updateStatus(ref, "");
|
|
1188
1200
|
} else {
|
|
1189
1201
|
const sessionOutput = await this.checkSessionOutput(session.opencodeSessionId);
|
|
1190
1202
|
const emptyRound = this.emptyResponseRounds.get(k) ?? 0;
|
|
@@ -1207,6 +1219,7 @@ export class Engine {
|
|
|
1207
1219
|
this.emptyResponseRounds.delete(k);
|
|
1208
1220
|
this.nudgeRounds.delete(k);
|
|
1209
1221
|
await tracker.createComment(ref, `[system] ❌ **${session.name}** 模型返回空响应(0 token),已重试 ${emptyRound} 次。请检查模型配置或稍后重试。`).catch(() => {});
|
|
1222
|
+
void tracker.updateStatus(ref, "failed", "empty model response");
|
|
1210
1223
|
} else {
|
|
1211
1224
|
const nudgeRound = this.nudgeRounds.get(k) ?? 0;
|
|
1212
1225
|
if (exitCode === 0 && nudgeRound < Engine.MAX_NUDGE_ROUNDS) {
|
|
@@ -1225,6 +1238,7 @@ export class Engine {
|
|
|
1225
1238
|
this.nudgeRounds.delete(k);
|
|
1226
1239
|
const detail = exitCode === 0 ? "ran but did not post a reply" : `crashed (exit ${exitCode})`;
|
|
1227
1240
|
await tracker.createComment(ref, `[system] ❌ **${session.name}** ${detail}. Try posting again or @${session.name} to retry.`).catch(() => {});
|
|
1241
|
+
void tracker.updateStatus(ref, "failed", detail);
|
|
1228
1242
|
}
|
|
1229
1243
|
}
|
|
1230
1244
|
|
|
@@ -1258,13 +1272,38 @@ export class Engine {
|
|
|
1258
1272
|
if (nextMsg) {
|
|
1259
1273
|
const current = await this.store.getSession(session.id);
|
|
1260
1274
|
if (current && current.state !== "idle") {
|
|
1261
|
-
|
|
1262
|
-
|
|
1275
|
+
if (this.running.size >= this.cfg.work.maxConcurrent) {
|
|
1276
|
+
log.info(`engine: concurrency limit (${this.running.size}/${this.cfg.work.maxConcurrent}), keeping msg ${nextMsg.id.slice(0, 8)} pending for ${k}`);
|
|
1277
|
+
} else {
|
|
1278
|
+
await this.dequeueOrIdle(k, current, issue, nextMsg);
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1263
1281
|
}
|
|
1264
1282
|
}
|
|
1265
1283
|
|
|
1266
1284
|
this.clearRuntimeState(k);
|
|
1267
1285
|
await this.store.updateSession(session.id, { state: "idle" });
|
|
1286
|
+
|
|
1287
|
+
void this.drainGlobalPending();
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
private async drainGlobalPending(): Promise<void> {
|
|
1291
|
+
const slotsAvailable = this.cfg.work.maxConcurrent - this.running.size;
|
|
1292
|
+
if (slotsAvailable <= 0) return;
|
|
1293
|
+
const pending = await this.store.getGlobalPendingMessages(slotsAvailable);
|
|
1294
|
+
for (const msg of pending) {
|
|
1295
|
+
if (this.running.size >= this.cfg.work.maxConcurrent) break;
|
|
1296
|
+
const session = await this.store.getSession(msg.sessionId);
|
|
1297
|
+
if (!session || session.state === "running") continue;
|
|
1298
|
+
const issue = await this.store.getIssue(session.issueId);
|
|
1299
|
+
if (!issue || issue.state === "closed") continue;
|
|
1300
|
+
const k = this.sessionKey(session, issue);
|
|
1301
|
+
if (this.running.has(k)) continue;
|
|
1302
|
+
const won = await this.store.claimMessage(msg.id);
|
|
1303
|
+
if (!won) continue;
|
|
1304
|
+
log.info(`engine: drainGlobalPending picked up msg ${msg.id.slice(0, 8)} for ${k}`);
|
|
1305
|
+
await this.dequeueOrIdle(k, session, issue, msg);
|
|
1306
|
+
}
|
|
1268
1307
|
}
|
|
1269
1308
|
|
|
1270
1309
|
private async dequeueOrIdle(k: string, session: OpSession, issue: Issue, msg: Message) {
|
|
@@ -140,6 +140,7 @@ export class GiteaTracker implements IssueTracker {
|
|
|
140
140
|
};
|
|
141
141
|
|
|
142
142
|
const issueUser = issue.user as Record<string, string>;
|
|
143
|
+
const aiStatus = typeof issue.ai_status === "string" ? issue.ai_status : "";
|
|
143
144
|
|
|
144
145
|
if (action === "opened" || action === "reopened") {
|
|
145
146
|
return {
|
|
@@ -150,6 +151,7 @@ export class GiteaTracker implements IssueTracker {
|
|
|
150
151
|
body: (issue.body as string) ?? "",
|
|
151
152
|
state: (issue.state as string) ?? "open",
|
|
152
153
|
author: issueUser?.login ?? "",
|
|
154
|
+
ai_status: aiStatus,
|
|
153
155
|
},
|
|
154
156
|
model,
|
|
155
157
|
cloneUrl,
|
|
@@ -167,6 +169,7 @@ export class GiteaTracker implements IssueTracker {
|
|
|
167
169
|
body: (issue.body as string) ?? "",
|
|
168
170
|
state: (issue.state as string) ?? "open",
|
|
169
171
|
author: issueUser?.login ?? "",
|
|
172
|
+
ai_status: aiStatus,
|
|
170
173
|
},
|
|
171
174
|
comment: {
|
|
172
175
|
id: String(comment.id),
|
|
@@ -188,6 +191,7 @@ export class GiteaTracker implements IssueTracker {
|
|
|
188
191
|
body: (issue.body as string) ?? "",
|
|
189
192
|
state: "closed",
|
|
190
193
|
author: issueUser?.login ?? "",
|
|
194
|
+
ai_status: aiStatus,
|
|
191
195
|
},
|
|
192
196
|
model,
|
|
193
197
|
cloneUrl,
|
|
@@ -205,6 +209,7 @@ export class GiteaTracker implements IssueTracker {
|
|
|
205
209
|
body: (issue.body as string) ?? "",
|
|
206
210
|
state: (issue.state as string) ?? "open",
|
|
207
211
|
author: issueUser?.login ?? "",
|
|
212
|
+
ai_status: aiStatus,
|
|
208
213
|
},
|
|
209
214
|
status: {
|
|
210
215
|
from: status?.from ?? "",
|