ework-daemon 0.4.22 → 0.4.24
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/gitea.ts +17 -0
- package/src/opencode.ts +45 -0
- package/src/trackers/gitea-tracker.ts +31 -0
- package/src/trackers/types.ts +4 -1
package/package.json
CHANGED
package/src/gitea.ts
CHANGED
|
@@ -63,6 +63,23 @@ export class GiteaClient {
|
|
|
63
63
|
}, true);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
async updateIssueStatus(
|
|
67
|
+
owner: string,
|
|
68
|
+
repo: string,
|
|
69
|
+
issueNumber: number,
|
|
70
|
+
status: string,
|
|
71
|
+
detail?: string
|
|
72
|
+
): Promise<void> {
|
|
73
|
+
try {
|
|
74
|
+
await this.request("POST", `/repos/${owner}/${repo}/issues/${issueNumber}/status`, {
|
|
75
|
+
status,
|
|
76
|
+
detail,
|
|
77
|
+
}, true);
|
|
78
|
+
} catch (e) {
|
|
79
|
+
console.warn("[gitea] updateIssueStatus failed:", (e as Error).message);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
66
83
|
async editComment(
|
|
67
84
|
owner: string,
|
|
68
85
|
repo: string,
|
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);
|
|
@@ -579,6 +584,11 @@ export class Engine {
|
|
|
579
584
|
return this.handleCommented(ref, scopeKey, issueData, event.comment!, tracker, event.model);
|
|
580
585
|
case "issue_closed":
|
|
581
586
|
return this.handleClosed(ref, scopeKey, tracker);
|
|
587
|
+
case "status_changed": {
|
|
588
|
+
const to = event.status?.to;
|
|
589
|
+
if (to === "halted") return this.handleHalted(ref, scopeKey, tracker);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
582
592
|
}
|
|
583
593
|
}
|
|
584
594
|
|
|
@@ -629,6 +639,7 @@ export class Engine {
|
|
|
629
639
|
log.info(`engine: session "${session.name}" created for ${k}, workdir=${workdir}`);
|
|
630
640
|
|
|
631
641
|
await tracker.createComment(ref, `[system] 🔄 **${session.name}** picked up this issue.\n> session: ${this.sessionRef(session)} | workdir: ${this.workdirLink(workdir)}`);
|
|
642
|
+
void tracker.updateStatus(ref, "processing");
|
|
632
643
|
|
|
633
644
|
const instructions = tracker.getTrackerInstructions(ref);
|
|
634
645
|
const prompt = this.buildInitialPrompt(
|
|
@@ -813,6 +824,36 @@ export class Engine {
|
|
|
813
824
|
this.senders.delete(gcKey);
|
|
814
825
|
|
|
815
826
|
log.info(`engine: issue closed, ${sessions.length} sessions paused for ${scopeKey}#${ref.issueId}`);
|
|
827
|
+
void tracker.updateStatus(ref, "completed");
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
private async handleHalted(
|
|
831
|
+
ref: TrackerRef,
|
|
832
|
+
scopeKey: string,
|
|
833
|
+
tracker: IssueTracker
|
|
834
|
+
) {
|
|
835
|
+
const issue = await this.store.findIssue(ref.trackerType, scopeKey, ref.issueId);
|
|
836
|
+
if (!issue) return;
|
|
837
|
+
this.stopObserver(issue.id);
|
|
838
|
+
const sessions = await this.store.getSessionsForIssue(issue.id);
|
|
839
|
+
for (const session of sessions) {
|
|
840
|
+
const k = this.sessionKey(session, issue);
|
|
841
|
+
const proc = this.processes.get(k);
|
|
842
|
+
if (proc) {
|
|
843
|
+
this.stopping.add(k);
|
|
844
|
+
try { this.killProcessTree(proc.pid, "SIGTERM"); } catch { /* already dead */ }
|
|
845
|
+
}
|
|
846
|
+
this.clearRuntimeState(k);
|
|
847
|
+
const msgs = await this.store.getMessagesForSession(session.id);
|
|
848
|
+
for (const msg of msgs) {
|
|
849
|
+
if (msg.status === "pending" || msg.status === "running") {
|
|
850
|
+
await this.store.updateMessageStatus(msg.id, "interrupted", "halted by user");
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
await this.store.updateSession(session.id, { state: "idle", opencodePid: undefined });
|
|
854
|
+
}
|
|
855
|
+
log.info(`engine: issue halted, ${sessions.length} sessions killed for ${scopeKey}#${ref.issueId}`);
|
|
856
|
+
try { await tracker.createComment(ref, "[system] ⏸️ AI processing halted by user."); } catch { /* tracker unavailable */ }
|
|
816
857
|
}
|
|
817
858
|
|
|
818
859
|
private clearRuntimeState(k: string) {
|
|
@@ -1127,6 +1168,7 @@ export class Engine {
|
|
|
1127
1168
|
if (exitCode === null) {
|
|
1128
1169
|
log.info(`engine: spawn failed for ${k}, skipping completion check`);
|
|
1129
1170
|
await this.store.updateSession(session.id, { opencodePid: undefined });
|
|
1171
|
+
void tracker.updateStatus(ref, "failed", "spawn failed");
|
|
1130
1172
|
await this.deactivateIfIdle(k, session, issue);
|
|
1131
1173
|
return;
|
|
1132
1174
|
}
|
|
@@ -1149,6 +1191,7 @@ export class Engine {
|
|
|
1149
1191
|
this.nudgeRounds.delete(k);
|
|
1150
1192
|
this.emptyResponseRounds.delete(k);
|
|
1151
1193
|
await this.persistRuntimeState(session.id);
|
|
1194
|
+
void tracker.updateStatus(ref, "");
|
|
1152
1195
|
} else {
|
|
1153
1196
|
const sessionOutput = await this.checkSessionOutput(session.opencodeSessionId);
|
|
1154
1197
|
const emptyRound = this.emptyResponseRounds.get(k) ?? 0;
|
|
@@ -1171,6 +1214,7 @@ export class Engine {
|
|
|
1171
1214
|
this.emptyResponseRounds.delete(k);
|
|
1172
1215
|
this.nudgeRounds.delete(k);
|
|
1173
1216
|
await tracker.createComment(ref, `[system] ❌ **${session.name}** 模型返回空响应(0 token),已重试 ${emptyRound} 次。请检查模型配置或稍后重试。`).catch(() => {});
|
|
1217
|
+
void tracker.updateStatus(ref, "failed", "empty model response");
|
|
1174
1218
|
} else {
|
|
1175
1219
|
const nudgeRound = this.nudgeRounds.get(k) ?? 0;
|
|
1176
1220
|
if (exitCode === 0 && nudgeRound < Engine.MAX_NUDGE_ROUNDS) {
|
|
@@ -1189,6 +1233,7 @@ export class Engine {
|
|
|
1189
1233
|
this.nudgeRounds.delete(k);
|
|
1190
1234
|
const detail = exitCode === 0 ? "ran but did not post a reply" : `crashed (exit ${exitCode})`;
|
|
1191
1235
|
await tracker.createComment(ref, `[system] ❌ **${session.name}** ${detail}. Try posting again or @${session.name} to retry.`).catch(() => {});
|
|
1236
|
+
void tracker.updateStatus(ref, "failed", detail);
|
|
1192
1237
|
}
|
|
1193
1238
|
}
|
|
1194
1239
|
|
|
@@ -65,6 +65,12 @@ export class GiteaTracker implements IssueTracker {
|
|
|
65
65
|
);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
async updateStatus(ref: TrackerRef, status: string, detail?: string) {
|
|
69
|
+
await this.client.updateIssueStatus(
|
|
70
|
+
this.owner(ref), this.repo(ref), Number(ref.issueId), status, detail
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
68
74
|
async setReaction(ref: TrackerRef, commentId: string, content: string, remove = false) {
|
|
69
75
|
if (remove) {
|
|
70
76
|
await this.client.removeCommentReaction(
|
|
@@ -134,6 +140,7 @@ export class GiteaTracker implements IssueTracker {
|
|
|
134
140
|
};
|
|
135
141
|
|
|
136
142
|
const issueUser = issue.user as Record<string, string>;
|
|
143
|
+
const aiStatus = typeof issue.ai_status === "string" ? issue.ai_status : "";
|
|
137
144
|
|
|
138
145
|
if (action === "opened" || action === "reopened") {
|
|
139
146
|
return {
|
|
@@ -144,6 +151,7 @@ export class GiteaTracker implements IssueTracker {
|
|
|
144
151
|
body: (issue.body as string) ?? "",
|
|
145
152
|
state: (issue.state as string) ?? "open",
|
|
146
153
|
author: issueUser?.login ?? "",
|
|
154
|
+
ai_status: aiStatus,
|
|
147
155
|
},
|
|
148
156
|
model,
|
|
149
157
|
cloneUrl,
|
|
@@ -161,6 +169,7 @@ export class GiteaTracker implements IssueTracker {
|
|
|
161
169
|
body: (issue.body as string) ?? "",
|
|
162
170
|
state: (issue.state as string) ?? "open",
|
|
163
171
|
author: issueUser?.login ?? "",
|
|
172
|
+
ai_status: aiStatus,
|
|
164
173
|
},
|
|
165
174
|
comment: {
|
|
166
175
|
id: String(comment.id),
|
|
@@ -182,6 +191,7 @@ export class GiteaTracker implements IssueTracker {
|
|
|
182
191
|
body: (issue.body as string) ?? "",
|
|
183
192
|
state: "closed",
|
|
184
193
|
author: issueUser?.login ?? "",
|
|
194
|
+
ai_status: aiStatus,
|
|
185
195
|
},
|
|
186
196
|
model,
|
|
187
197
|
cloneUrl,
|
|
@@ -189,6 +199,27 @@ export class GiteaTracker implements IssueTracker {
|
|
|
189
199
|
};
|
|
190
200
|
}
|
|
191
201
|
|
|
202
|
+
if (action === "status_changed") {
|
|
203
|
+
const status = payload.status as { from?: string; to?: string; detail?: string } | undefined;
|
|
204
|
+
return {
|
|
205
|
+
type: "status_changed" as const,
|
|
206
|
+
ref,
|
|
207
|
+
issue: {
|
|
208
|
+
title: issue.title as string,
|
|
209
|
+
body: (issue.body as string) ?? "",
|
|
210
|
+
state: (issue.state as string) ?? "open",
|
|
211
|
+
author: issueUser?.login ?? "",
|
|
212
|
+
ai_status: aiStatus,
|
|
213
|
+
},
|
|
214
|
+
status: {
|
|
215
|
+
from: status?.from ?? "",
|
|
216
|
+
to: status?.to ?? "",
|
|
217
|
+
detail: status?.detail,
|
|
218
|
+
},
|
|
219
|
+
sender,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
192
223
|
return null;
|
|
193
224
|
}
|
|
194
225
|
|
package/src/trackers/types.ts
CHANGED
|
@@ -13,7 +13,7 @@ export interface TrackerRef {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/** Parsed, tracker-agnostic webhook event */
|
|
16
|
-
export type TrackerEventType = "issue_opened" | "comment_created" | "issue_closed";
|
|
16
|
+
export type TrackerEventType = "issue_opened" | "comment_created" | "issue_closed" | "status_changed";
|
|
17
17
|
|
|
18
18
|
export interface TrackerEvent {
|
|
19
19
|
type: TrackerEventType;
|
|
@@ -23,6 +23,7 @@ export interface TrackerEvent {
|
|
|
23
23
|
body: string;
|
|
24
24
|
state: string;
|
|
25
25
|
author: string;
|
|
26
|
+
ai_status?: string;
|
|
26
27
|
};
|
|
27
28
|
comment?: {
|
|
28
29
|
id: string;
|
|
@@ -39,6 +40,7 @@ export interface TrackerEvent {
|
|
|
39
40
|
// Webhook sender (who triggered the event). Forwarded to the daemon's
|
|
40
41
|
// task context so the AI knows who it's responding to.
|
|
41
42
|
sender?: string;
|
|
43
|
+
status?: { from: string; to: string; detail?: string };
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
/** Tracker-agnostic comment */
|
|
@@ -130,6 +132,7 @@ export interface IssueTracker {
|
|
|
130
132
|
deleteComment(ref: TrackerRef, commentId: string): Promise<void>;
|
|
131
133
|
listComments(ref: TrackerRef): Promise<TrackerComment[]>;
|
|
132
134
|
closeIssue(ref: TrackerRef): Promise<void>;
|
|
135
|
+
updateStatus(ref: TrackerRef, status: string, detail?: string): Promise<void>;
|
|
133
136
|
|
|
134
137
|
setReaction(ref: TrackerRef, commentId: string, content: string, remove?: boolean): Promise<void>;
|
|
135
138
|
|