ework-web 0.10.99 → 0.10.101
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/coordination.ts +53 -7
- package/src/render/layout.ts +2 -0
- package/src/upstream-sync.ts +4 -1
- package/src/views/issueThread.ts +3 -0
package/package.json
CHANGED
package/src/coordination.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getDB } from "./db";
|
|
2
2
|
import { log } from "./logger";
|
|
3
|
+
import { loadConfig } from "./config";
|
|
3
4
|
|
|
4
5
|
export interface DaemonInfo {
|
|
5
6
|
id: number;
|
|
@@ -19,6 +20,50 @@ export interface DaemonDetail {
|
|
|
19
20
|
|
|
20
21
|
const HEARTBEAT_STALE_MS = 120_000;
|
|
21
22
|
|
|
23
|
+
// Split-UID deployments keep the daemon registry in the daemon's own SQLite
|
|
24
|
+
// DB, unreachable (and table-absent) from the web process — the \{\{d_*\}\}
|
|
25
|
+
// queries then fail and every lookup silently degrades to "no daemons".
|
|
26
|
+
// Fall back to the router's registry API, which holds the same rows and is
|
|
27
|
+
// the natural source of truth when web and daemon share no database.
|
|
28
|
+
interface RouterDaemonRow {
|
|
29
|
+
id: number;
|
|
30
|
+
displayName: string;
|
|
31
|
+
endpoint: string;
|
|
32
|
+
capacity: number;
|
|
33
|
+
lastHeartbeat: string;
|
|
34
|
+
status: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let routerCache: { at: number; rows: RouterDaemonRow[] } | null = null;
|
|
38
|
+
|
|
39
|
+
async function routerDaemons(): Promise<RouterDaemonRow[]> {
|
|
40
|
+
if (routerCache && Date.now() - routerCache.at < 15_000) return routerCache.rows;
|
|
41
|
+
try {
|
|
42
|
+
const cfg = await loadConfig();
|
|
43
|
+
if (!cfg.daemonWebhookUrl) return [];
|
|
44
|
+
const res = await fetch(`${cfg.daemonWebhookUrl.replace(/\/$/, "")}/api/daemons`, {
|
|
45
|
+
signal: AbortSignal.timeout(5000),
|
|
46
|
+
});
|
|
47
|
+
const data = (await res.json()) as { daemons?: Array<Partial<RouterDaemonRow>> };
|
|
48
|
+
const rows: RouterDaemonRow[] = (data.daemons ?? [])
|
|
49
|
+
.filter((d): d is Partial<RouterDaemonRow> & { id: number; endpoint: string } =>
|
|
50
|
+
typeof d.id === "number" && typeof d.endpoint === "string")
|
|
51
|
+
.map((d) => ({
|
|
52
|
+
id: d.id,
|
|
53
|
+
displayName: typeof d.displayName === "string" ? d.displayName : `daemon-${d.id}`,
|
|
54
|
+
endpoint: d.endpoint,
|
|
55
|
+
capacity: typeof d.capacity === "number" ? d.capacity : 0,
|
|
56
|
+
lastHeartbeat: typeof d.lastHeartbeat === "string" ? d.lastHeartbeat : "",
|
|
57
|
+
status: typeof d.status === "string" ? d.status : "unknown",
|
|
58
|
+
}));
|
|
59
|
+
routerCache = { at: Date.now(), rows };
|
|
60
|
+
return rows;
|
|
61
|
+
} catch (e) {
|
|
62
|
+
log.info(`coordination: router registry fallback failed (${e instanceof Error ? e.message : String(e)})`);
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
22
67
|
export async function getActiveDaemons(): Promise<DaemonInfo[]> {
|
|
23
68
|
const stale = new Date(Date.now() - HEARTBEAT_STALE_MS);
|
|
24
69
|
const staleStr = stale.toISOString().slice(0, 19).replace("T", " ");
|
|
@@ -130,19 +175,20 @@ export async function getSessionDaemonMap(): Promise<Map<string, SessionDaemonIn
|
|
|
130
175
|
|
|
131
176
|
export async function resolveDaemonEndpoint(daemonId: number): Promise<string | null> {
|
|
132
177
|
try {
|
|
133
|
-
const
|
|
134
|
-
|
|
178
|
+
const local = await getDB().all<{ internal_endpoint: string }>(
|
|
179
|
+
"SELECT internal_endpoint FROM {{d_daemons}} WHERE id = ? LIMIT 1",
|
|
135
180
|
[daemonId],
|
|
136
181
|
);
|
|
137
|
-
const ep =
|
|
138
|
-
if (
|
|
139
|
-
return ep;
|
|
182
|
+
const ep = local[0]?.internal_endpoint;
|
|
183
|
+
if (ep) return ep;
|
|
140
184
|
} catch (e) {
|
|
141
|
-
log.info(`coordination:
|
|
142
|
-
return null;
|
|
185
|
+
log.info(`coordination: local daemon lookup failed (${e instanceof Error ? e.message : String(e)})`);
|
|
143
186
|
}
|
|
187
|
+
const row = (await routerDaemons()).find((d) => d.id === daemonId);
|
|
188
|
+
return row ? row.endpoint : null;
|
|
144
189
|
}
|
|
145
190
|
|
|
191
|
+
|
|
146
192
|
export interface RunningSessionInfo {
|
|
147
193
|
issueNumber: string;
|
|
148
194
|
sessionId: string;
|
package/src/render/layout.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface LayoutProps {
|
|
|
5
5
|
issueTitle: string;
|
|
6
6
|
repoPath: string;
|
|
7
7
|
issueNumber: number;
|
|
8
|
+
upstreamUrl?: string;
|
|
8
9
|
state: string;
|
|
9
10
|
totalComments: number;
|
|
10
11
|
descriptionHtml: string;
|
|
@@ -236,6 +237,7 @@ export function renderLayout(props: LayoutProps, inner: string, initialItems: st
|
|
|
236
237
|
<span style="opacity:.5">/</span>
|
|
237
238
|
<a href="${escapeAttr(repoIssuesHref)}" style="color:var(--header-text)">${escapeHtml(props.repoPath)}</a>
|
|
238
239
|
<span class="num">#${props.issueNumber}</span>
|
|
240
|
+
${props.upstreamUrl ? `<a href="${escapeAttr(props.upstreamUrl)}" target="_blank" rel="noopener" title="upstream" style="color:var(--header-text);text-decoration:none;font-size:.85em">↗</a>` : ""}
|
|
239
241
|
</header>
|
|
240
242
|
<div class="meta-bar">
|
|
241
243
|
<h1>${escapeHtml(props.issueTitle)}</h1>
|
package/src/upstream-sync.ts
CHANGED
|
@@ -123,7 +123,10 @@ export class UpstreamSync {
|
|
|
123
123
|
upstreamIssueNumber: gi.number,
|
|
124
124
|
}
|
|
125
125
|
);
|
|
126
|
-
|
|
126
|
+
// PRs the sandbox agent opens itself carry the ework-agent-pr marker;
|
|
127
|
+
// announcing them would wake the agent on its own artifact (feedback loop)
|
|
128
|
+
const agentAuthored = !!gi.pull_request && /<!--\s*ework-agent-pr\s*-->/.test(gi.body ?? "");
|
|
129
|
+
if (emit && !agentAuthored) {
|
|
127
130
|
const created = await getIssueByUpstreamNumber(this.project.id, gi.number);
|
|
128
131
|
if (created) void emitIssueEvent(this.project.id, created.id, "opened", this.origin);
|
|
129
132
|
}
|
package/src/views/issueThread.ts
CHANGED
|
@@ -157,6 +157,9 @@ export async function buildIssueThread(
|
|
|
157
157
|
{
|
|
158
158
|
title: `${issue.title} · ${owner}/${repo}#${number}`,
|
|
159
159
|
issueTitle: issue.title,
|
|
160
|
+
upstreamUrl: issue.upstream_issue_number && upstreamRefBase(project)
|
|
161
|
+
? `${upstreamRefBase(project)}/issues/${issue.upstream_issue_number}`
|
|
162
|
+
: undefined,
|
|
160
163
|
repoPath: `${owner}/${repo}`,
|
|
161
164
|
issueNumber: number,
|
|
162
165
|
state: issue.state,
|