@h-rig/server 0.0.6-alpha.13 → 0.0.6-alpha.14
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/src/index.js +678 -53
- package/dist/src/server-helpers/github-project-status-sync.js +3 -0
- package/dist/src/server-helpers/http-router.js +252 -22
- package/dist/src/server-helpers/run-io.js +13 -0
- package/dist/src/server-helpers/run-mutations.js +445 -32
- package/dist/src/server-helpers/run-writers.js +1 -2
- package/dist/src/server-helpers/ws-router.js +7 -1
- package/dist/src/server.js +676 -53
- package/package.json +4 -4
|
@@ -116,6 +116,7 @@ var DEFAULT_PROJECT_STATUSES = {
|
|
|
116
116
|
running: "In Progress",
|
|
117
117
|
prOpen: "In Review",
|
|
118
118
|
ciFixing: "In Review",
|
|
119
|
+
merging: "Merging",
|
|
119
120
|
done: "Done",
|
|
120
121
|
needsAttention: "Needs Attention"
|
|
121
122
|
};
|
|
@@ -129,6 +130,8 @@ function lifecycleStatusForTaskStatus(status) {
|
|
|
129
130
|
return "prOpen";
|
|
130
131
|
if (normalized === "ci_fixing" || normalized === "fixing")
|
|
131
132
|
return "ciFixing";
|
|
133
|
+
if (normalized === "merging" || normalized === "merge")
|
|
134
|
+
return "merging";
|
|
132
135
|
if (normalized === "failed" || normalized === "needs_attention" || normalized === "blocked")
|
|
133
136
|
return "needsAttention";
|
|
134
137
|
if (normalized === "in_progress" || normalized === "running" || normalized === "ready" || normalized === "open")
|
|
@@ -224,6 +224,18 @@ function readJsonlFileTail(path, options) {
|
|
|
224
224
|
const completeLines = start > 0 ? lines.slice(1) : lines;
|
|
225
225
|
return parseJsonlRecords(completeLines.filter(Boolean).slice(-limit));
|
|
226
226
|
}
|
|
227
|
+
async function readRunTimelinePage(projectRoot, runId, options = {}) {
|
|
228
|
+
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
|
|
229
|
+
const cursor = options.cursor == null ? 0 : Number.parseInt(options.cursor, 10);
|
|
230
|
+
const entries = readJsonlFile(runTimelinePath(projectRoot, runId)).filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
|
|
231
|
+
const startInclusive = Number.isFinite(cursor) ? Math.max(0, Math.min(cursor, entries.length)) : 0;
|
|
232
|
+
const endExclusive = Math.min(entries.length, startInclusive + limit);
|
|
233
|
+
return {
|
|
234
|
+
entries: entries.slice(startInclusive, endExclusive).map((entry, offset) => ({ ...entry, cursor: startInclusive + offset + 1 })),
|
|
235
|
+
nextCursor: String(endExclusive),
|
|
236
|
+
hasMore: endExclusive < entries.length
|
|
237
|
+
};
|
|
238
|
+
}
|
|
227
239
|
var INITIAL_RUN_LOG_TAIL_MAX_BYTES = 8 * 1024 * 1024;
|
|
228
240
|
async function readRunLogsPage(projectRoot, runId, options = {}) {
|
|
229
241
|
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
|
|
@@ -788,6 +800,11 @@ import {
|
|
|
788
800
|
buildTaskRunLifecycleComment,
|
|
789
801
|
updateConfiguredTaskSourceTask
|
|
790
802
|
} from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
803
|
+
import {
|
|
804
|
+
closeIssueAfterMergedPr,
|
|
805
|
+
commitRunChanges,
|
|
806
|
+
runPrAutomation
|
|
807
|
+
} from "@rig/runtime/control-plane/native/pr-automation";
|
|
791
808
|
|
|
792
809
|
// packages/server/src/scheduler.ts
|
|
793
810
|
import { normalizeTaskLifecycleStatus } from "@rig/runtime/control-plane/state-sync/types";
|
|
@@ -1027,6 +1044,9 @@ function asRecord(value) {
|
|
|
1027
1044
|
function asString(value) {
|
|
1028
1045
|
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
1029
1046
|
}
|
|
1047
|
+
function asNumber(value) {
|
|
1048
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
1049
|
+
}
|
|
1030
1050
|
async function defaultGraphQLFetch(query, variables, token) {
|
|
1031
1051
|
const response = await fetch("https://api.github.com/graphql", {
|
|
1032
1052
|
method: "POST",
|
|
@@ -1043,6 +1063,32 @@ async function defaultGraphQLFetch(query, variables, token) {
|
|
|
1043
1063
|
}
|
|
1044
1064
|
return json.data;
|
|
1045
1065
|
}
|
|
1066
|
+
function projectNodesFrom(data) {
|
|
1067
|
+
const root = asRecord(data);
|
|
1068
|
+
const owner = asRecord(root?.organization) ?? asRecord(root?.user);
|
|
1069
|
+
const projects = asRecord(owner?.projectsV2);
|
|
1070
|
+
const nodes = projects?.nodes;
|
|
1071
|
+
return Array.isArray(nodes) ? nodes : [];
|
|
1072
|
+
}
|
|
1073
|
+
async function listGitHubProjects(input) {
|
|
1074
|
+
const query = `
|
|
1075
|
+
query RigListProjects($owner: String!, $first: Int!) {
|
|
1076
|
+
organization(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
|
|
1077
|
+
user(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
|
|
1078
|
+
}
|
|
1079
|
+
`;
|
|
1080
|
+
const fetchGraphQL = input.fetchGraphQL ?? defaultGraphQLFetch;
|
|
1081
|
+
const data = await fetchGraphQL(query, { owner: input.owner, first: input.first ?? 20 }, input.token);
|
|
1082
|
+
return projectNodesFrom(data).flatMap((node) => {
|
|
1083
|
+
const record = asRecord(node);
|
|
1084
|
+
const id = asString(record?.id);
|
|
1085
|
+
const number = asNumber(record?.number);
|
|
1086
|
+
const title = asString(record?.title);
|
|
1087
|
+
if (!id || number === undefined || !title)
|
|
1088
|
+
return [];
|
|
1089
|
+
return [{ id, number, title, ...asString(record?.url) ? { url: asString(record?.url) } : {} }];
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1046
1092
|
async function resolveProjectStatusField(input) {
|
|
1047
1093
|
const query = `
|
|
1048
1094
|
query RigProjectStatusField($projectId: ID!) {
|
|
@@ -1137,6 +1183,7 @@ var DEFAULT_PROJECT_STATUSES = {
|
|
|
1137
1183
|
running: "In Progress",
|
|
1138
1184
|
prOpen: "In Review",
|
|
1139
1185
|
ciFixing: "In Review",
|
|
1186
|
+
merging: "Merging",
|
|
1140
1187
|
done: "Done",
|
|
1141
1188
|
needsAttention: "Needs Attention"
|
|
1142
1189
|
};
|
|
@@ -1150,6 +1197,8 @@ function lifecycleStatusForTaskStatus(status) {
|
|
|
1150
1197
|
return "prOpen";
|
|
1151
1198
|
if (normalized === "ci_fixing" || normalized === "fixing")
|
|
1152
1199
|
return "ciFixing";
|
|
1200
|
+
if (normalized === "merging" || normalized === "merge")
|
|
1201
|
+
return "merging";
|
|
1153
1202
|
if (normalized === "failed" || normalized === "needs_attention" || normalized === "blocked")
|
|
1154
1203
|
return "needsAttention";
|
|
1155
1204
|
if (normalized === "in_progress" || normalized === "running" || normalized === "ready" || normalized === "open")
|
|
@@ -1225,7 +1274,8 @@ var TERMINAL_RUN_STATUSES2 = new Set([
|
|
|
1225
1274
|
"needs-attention",
|
|
1226
1275
|
"stopped"
|
|
1227
1276
|
]);
|
|
1228
|
-
var
|
|
1277
|
+
var RESUMABLE_SERVER_CLOSEOUT_STATUSES = new Set(["pending", "running"]);
|
|
1278
|
+
var ACTIVE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
|
|
1229
1279
|
|
|
1230
1280
|
// packages/server/src/server-helpers/ws-router.ts
|
|
1231
1281
|
import {
|
|
@@ -2000,6 +2050,75 @@ function buildProjectConfigStatus(root) {
|
|
|
2000
2050
|
suggestion: kind === "missing" ? "Run `rig init` in the project root to scaffold rig.config.ts." : null
|
|
2001
2051
|
};
|
|
2002
2052
|
}
|
|
2053
|
+
var RIG_GITHUB_LIFECYCLE_LABELS = [
|
|
2054
|
+
"ready",
|
|
2055
|
+
"blocked",
|
|
2056
|
+
"in-progress",
|
|
2057
|
+
"under-review",
|
|
2058
|
+
"failed",
|
|
2059
|
+
"cancelled",
|
|
2060
|
+
"rig:running",
|
|
2061
|
+
"rig:pr-open",
|
|
2062
|
+
"rig:ci-fixing",
|
|
2063
|
+
"rig:merging",
|
|
2064
|
+
"rig:done",
|
|
2065
|
+
"rig:needs-attention"
|
|
2066
|
+
];
|
|
2067
|
+
function githubProjectsEnabled(config) {
|
|
2068
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
2069
|
+
return false;
|
|
2070
|
+
const root = config;
|
|
2071
|
+
const github = root.github && typeof root.github === "object" && !Array.isArray(root.github) ? root.github : null;
|
|
2072
|
+
const projects = github?.projects && typeof github.projects === "object" && !Array.isArray(github.projects) ? github.projects : null;
|
|
2073
|
+
return projects?.enabled === true;
|
|
2074
|
+
}
|
|
2075
|
+
function githubIssueSourceRepo(config) {
|
|
2076
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
2077
|
+
return null;
|
|
2078
|
+
const root = config;
|
|
2079
|
+
const taskSource = root.taskSource && typeof root.taskSource === "object" && !Array.isArray(root.taskSource) ? root.taskSource : null;
|
|
2080
|
+
const owner = normalizeString(taskSource?.owner);
|
|
2081
|
+
const repo = normalizeString(taskSource?.repo);
|
|
2082
|
+
if (taskSource?.kind === "github-issues" && owner && repo)
|
|
2083
|
+
return { owner, repo };
|
|
2084
|
+
const project = root.project && typeof root.project === "object" && !Array.isArray(root.project) ? root.project : null;
|
|
2085
|
+
const slug = normalizeString(project?.repo) ?? normalizeString(project?.name);
|
|
2086
|
+
const match = slug?.match(/^([^/]+)\/([^/]+)$/);
|
|
2087
|
+
return match ? { owner: match[1], repo: match[2] } : null;
|
|
2088
|
+
}
|
|
2089
|
+
async function ensureGitHubLifecycleLabels(projectRoot, config) {
|
|
2090
|
+
const repo = githubIssueSourceRepo(config);
|
|
2091
|
+
if (!repo)
|
|
2092
|
+
return { ok: false, ready: false, labelsReady: false, reason: "not-github-issues-source", labels: RIG_GITHUB_LIFECYCLE_LABELS };
|
|
2093
|
+
const token = createGitHubAuthStore(projectRoot).readToken();
|
|
2094
|
+
if (!token)
|
|
2095
|
+
return { ok: false, ready: false, labelsReady: false, reason: "missing-token", repo, labels: RIG_GITHUB_LIFECYCLE_LABELS };
|
|
2096
|
+
const existingResponse = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels?per_page=100`, {
|
|
2097
|
+
headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "user-agent": "rig-server" }
|
|
2098
|
+
});
|
|
2099
|
+
const existingJson = await existingResponse.json().catch(() => []);
|
|
2100
|
+
const existing = new Set(Array.isArray(existingJson) ? existingJson.flatMap((entry) => entry && typeof entry === "object" && typeof entry.name === "string" ? [entry.name] : []) : []);
|
|
2101
|
+
const created = [];
|
|
2102
|
+
const alreadyPresent = [];
|
|
2103
|
+
const failed = [];
|
|
2104
|
+
for (const label of RIG_GITHUB_LIFECYCLE_LABELS) {
|
|
2105
|
+
if (existing.has(label)) {
|
|
2106
|
+
alreadyPresent.push(label);
|
|
2107
|
+
continue;
|
|
2108
|
+
}
|
|
2109
|
+
const response = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels`, {
|
|
2110
|
+
method: "POST",
|
|
2111
|
+
headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "content-type": "application/json", "user-agent": "rig-server" },
|
|
2112
|
+
body: JSON.stringify({ name: label, color: label.startsWith("rig:") ? "6f42c1" : "ededed", description: label.startsWith("rig:") ? "Task status managed by Rig" : "Task lifecycle status managed by Rig" })
|
|
2113
|
+
});
|
|
2114
|
+
if (response.ok || response.status === 422) {
|
|
2115
|
+
(response.status === 422 ? alreadyPresent : created).push(label);
|
|
2116
|
+
} else {
|
|
2117
|
+
failed.push({ label, error: await response.text().catch(() => response.statusText) });
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
return { ok: failed.length === 0, ready: failed.length === 0, labelsReady: failed.length === 0, repo, labels: RIG_GITHUB_LIFECYCLE_LABELS, created, existing: alreadyPresent, failed };
|
|
2121
|
+
}
|
|
2003
2122
|
function normalizeCommit(value) {
|
|
2004
2123
|
const raw = normalizeString(value);
|
|
2005
2124
|
return raw && /^[0-9a-f]{7,40}$/i.test(raw) ? raw : null;
|
|
@@ -3145,16 +3264,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3145
3264
|
if (!source) {
|
|
3146
3265
|
return deps.badRequest("No task source is configured");
|
|
3147
3266
|
}
|
|
3267
|
+
if (!source.updateTask && !(update.status && source.updateStatus)) {
|
|
3268
|
+
return deps.badRequest("Configured task source does not support updates");
|
|
3269
|
+
}
|
|
3148
3270
|
const taskBeforeUpdate = source.get ? await source.get(id).catch(() => {
|
|
3149
3271
|
return;
|
|
3150
3272
|
}) : (await deps.snapshotService.getWorkspaceTasks().catch(() => [])).find((task) => task.id === id);
|
|
3151
|
-
if (source.updateTask) {
|
|
3152
|
-
await source.updateTask(id, update);
|
|
3153
|
-
} else if (update.status && source.updateStatus) {
|
|
3154
|
-
await source.updateStatus(id, update.status);
|
|
3155
|
-
} else {
|
|
3156
|
-
return deps.badRequest("Configured task source does not support updates");
|
|
3157
|
-
}
|
|
3158
3273
|
const issueNodeId = normalizeString(body.issueNodeId) ?? extractGitHubIssueNodeId(taskBeforeUpdate);
|
|
3159
3274
|
const projectSync = update.status ? await syncGitHubProjectStatusForTaskUpdate({
|
|
3160
3275
|
taskId: id,
|
|
@@ -3163,6 +3278,35 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3163
3278
|
token: createGitHubAuthStore(state.projectRoot).readToken(),
|
|
3164
3279
|
config: ctx?.config
|
|
3165
3280
|
}).catch((error) => ({ synced: false, reason: `error:${error instanceof Error ? error.message : String(error)}` })) : { synced: false, reason: "missing-status" };
|
|
3281
|
+
if (update.status && githubProjectsEnabled(ctx?.config) && projectSync.synced === false) {
|
|
3282
|
+
return deps.jsonResponse({ ok: false, id, projectSync, error: `GitHub Project status sync failed: ${String(projectSync.reason)}` }, 502);
|
|
3283
|
+
}
|
|
3284
|
+
try {
|
|
3285
|
+
if (source.updateTask) {
|
|
3286
|
+
await source.updateTask(id, update);
|
|
3287
|
+
} else if (update.status && source.updateStatus) {
|
|
3288
|
+
await source.updateStatus(id, update.status);
|
|
3289
|
+
}
|
|
3290
|
+
} catch (error) {
|
|
3291
|
+
let rollback = null;
|
|
3292
|
+
const previousStatus = normalizeString(taskBeforeUpdate?.status) ?? normalizeString(taskBeforeUpdate?.sourceStatus);
|
|
3293
|
+
if (update.status && previousStatus && githubProjectsEnabled(ctx?.config) && projectSync.synced !== false) {
|
|
3294
|
+
rollback = await syncGitHubProjectStatusForTaskUpdate({
|
|
3295
|
+
taskId: id,
|
|
3296
|
+
status: previousStatus,
|
|
3297
|
+
issueNodeId,
|
|
3298
|
+
token: createGitHubAuthStore(state.projectRoot).readToken(),
|
|
3299
|
+
config: ctx?.config
|
|
3300
|
+
}).catch((rollbackError) => ({ synced: false, reason: `rollback-error:${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}` }));
|
|
3301
|
+
}
|
|
3302
|
+
return deps.jsonResponse({
|
|
3303
|
+
ok: false,
|
|
3304
|
+
id,
|
|
3305
|
+
projectSync,
|
|
3306
|
+
rollback,
|
|
3307
|
+
error: `Task source update failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3308
|
+
}, 502);
|
|
3309
|
+
}
|
|
3166
3310
|
deps.snapshotService.invalidate("github-issue-updated");
|
|
3167
3311
|
await state.taskProjectionReconciler?.tick("github-issue-updated").catch(() => {
|
|
3168
3312
|
return;
|
|
@@ -3171,25 +3315,40 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3171
3315
|
return deps.jsonResponse({ ok: true, id, projectSync });
|
|
3172
3316
|
}
|
|
3173
3317
|
if (url.pathname === "/api/workspace/task-labels") {
|
|
3318
|
+
const ctx = await getCachedPluginHostContext(state.projectRoot).catch(() => null);
|
|
3319
|
+
if (url.searchParams.get("ensure") === "1" || req.method === "POST") {
|
|
3320
|
+
return deps.jsonResponse(await ensureGitHubLifecycleLabels(state.projectRoot, ctx?.config));
|
|
3321
|
+
}
|
|
3174
3322
|
return deps.jsonResponse({
|
|
3175
3323
|
ok: true,
|
|
3176
3324
|
ready: true,
|
|
3177
3325
|
labelsReady: true,
|
|
3178
|
-
labels: [
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
note: "GitHub issue lifecycle labels are created on demand by the configured task source when supported."
|
|
3326
|
+
labels: [...RIG_GITHUB_LIFECYCLE_LABELS],
|
|
3327
|
+
note: "Lifecycle labels are required during init; call POST /api/workspace/task-labels or ?ensure=1 to proactively create them."
|
|
3328
|
+
});
|
|
3329
|
+
}
|
|
3330
|
+
if (url.pathname === "/api/github/projects" && req.method === "GET") {
|
|
3331
|
+
const owner = normalizeString(url.searchParams.get("owner"));
|
|
3332
|
+
if (!owner)
|
|
3333
|
+
return deps.badRequest("owner is required");
|
|
3334
|
+
const token = createGitHubAuthStore(state.projectRoot).readToken();
|
|
3335
|
+
if (!token)
|
|
3336
|
+
return deps.jsonResponse({ ok: false, error: "missing-token", projects: [] }, 401);
|
|
3337
|
+
const projects = await listGitHubProjects({ owner, token }).catch((error) => {
|
|
3338
|
+
throw new Error(error instanceof Error ? error.message : String(error));
|
|
3192
3339
|
});
|
|
3340
|
+
return deps.jsonResponse({ ok: true, projects });
|
|
3341
|
+
}
|
|
3342
|
+
const projectStatusMatch = url.pathname.match(/^\/api\/github\/projects\/([^/]+)\/status-field$/);
|
|
3343
|
+
if (projectStatusMatch && req.method === "GET") {
|
|
3344
|
+
const projectId = decodeURIComponent(projectStatusMatch[1]);
|
|
3345
|
+
const token = createGitHubAuthStore(state.projectRoot).readToken();
|
|
3346
|
+
if (!token)
|
|
3347
|
+
return deps.jsonResponse({ ok: false, error: "missing-token" }, 401);
|
|
3348
|
+
const field = await resolveProjectStatusField({ projectId, token }).catch((error) => {
|
|
3349
|
+
throw new Error(error instanceof Error ? error.message : String(error));
|
|
3350
|
+
});
|
|
3351
|
+
return deps.jsonResponse({ ok: true, field });
|
|
3193
3352
|
}
|
|
3194
3353
|
if (url.pathname === "/api/workspace/issue-analysis/run" && req.method === "POST") {
|
|
3195
3354
|
const body = await deps.readJsonBody(req);
|
|
@@ -4355,6 +4514,69 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
4355
4514
|
}
|
|
4356
4515
|
const run = leaseValidation.run;
|
|
4357
4516
|
const completedAt = new Date().toISOString();
|
|
4517
|
+
const workspaceDir = normalizeString(body.workspaceDir) ?? normalizeString(body.runtimeWorkspace) ?? normalizeString(run.worktreePath);
|
|
4518
|
+
if (run.taskId && workspaceDir) {
|
|
4519
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4520
|
+
status: "reviewing",
|
|
4521
|
+
completedAt: null,
|
|
4522
|
+
hostId,
|
|
4523
|
+
endpointId: leaseId,
|
|
4524
|
+
worktreePath: workspaceDir,
|
|
4525
|
+
serverCloseout: {
|
|
4526
|
+
status: "pending",
|
|
4527
|
+
phase: "queued",
|
|
4528
|
+
requestedAt: completedAt,
|
|
4529
|
+
updatedAt: completedAt,
|
|
4530
|
+
runtimeWorkspace: workspaceDir,
|
|
4531
|
+
branch: normalizeString(body.branch) ?? normalizeString(run.branch) ?? `rig/${run.taskId}-${runId}`,
|
|
4532
|
+
taskId: run.taskId,
|
|
4533
|
+
source: "remote-complete"
|
|
4534
|
+
}
|
|
4535
|
+
});
|
|
4536
|
+
deps.appendRunLogEntryAndBroadcast(state, runId, {
|
|
4537
|
+
id: `log:${runId}:remote-server-closeout-requested`,
|
|
4538
|
+
title: "Server-owned closeout requested",
|
|
4539
|
+
detail: "Remote run completed provider work and handed commit/PR/review/merge closeout to the Rig server.",
|
|
4540
|
+
tone: "info",
|
|
4541
|
+
status: "reviewing",
|
|
4542
|
+
createdAt: completedAt,
|
|
4543
|
+
payload: { workspaceDir, hostId, leaseId }
|
|
4544
|
+
}, "remote-server-closeout-requested");
|
|
4545
|
+
deps.runServerOwnedPrCloseout(state, runId).catch((error) => {
|
|
4546
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
4547
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4548
|
+
status: "failed",
|
|
4549
|
+
completedAt: new Date().toISOString(),
|
|
4550
|
+
errorText: detail,
|
|
4551
|
+
serverCloseout: {
|
|
4552
|
+
status: "failed",
|
|
4553
|
+
phase: "failed",
|
|
4554
|
+
updatedAt: new Date().toISOString(),
|
|
4555
|
+
error: detail
|
|
4556
|
+
}
|
|
4557
|
+
});
|
|
4558
|
+
deps.appendRunLogEntryAndBroadcast(state, runId, {
|
|
4559
|
+
id: `log:${runId}:remote-server-closeout-failed`,
|
|
4560
|
+
title: "Server-owned closeout failed",
|
|
4561
|
+
detail,
|
|
4562
|
+
tone: "error",
|
|
4563
|
+
status: "failed",
|
|
4564
|
+
createdAt: new Date().toISOString()
|
|
4565
|
+
}, "remote-server-closeout-failed");
|
|
4566
|
+
}).finally(() => {
|
|
4567
|
+
deps.reconcileScheduler(state, "remote-server-closeout-terminal");
|
|
4568
|
+
});
|
|
4569
|
+
deps.broadcastSnapshotInvalidation(state);
|
|
4570
|
+
return deps.jsonResponse({
|
|
4571
|
+
ok: true,
|
|
4572
|
+
workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
|
|
4573
|
+
hostId,
|
|
4574
|
+
runId,
|
|
4575
|
+
leaseId,
|
|
4576
|
+
closeout: "server-owned",
|
|
4577
|
+
acceptedAt: new Date().toISOString()
|
|
4578
|
+
});
|
|
4579
|
+
}
|
|
4358
4580
|
patchRunRecord(state.projectRoot, runId, {
|
|
4359
4581
|
status: "completed",
|
|
4360
4582
|
completedAt,
|
|
@@ -4497,6 +4719,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
4497
4719
|
const page = await readRunLogsPage(state.projectRoot, runId, { limit, cursor });
|
|
4498
4720
|
return deps.jsonResponse(page);
|
|
4499
4721
|
}
|
|
4722
|
+
const runTimelineMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/timeline$/);
|
|
4723
|
+
if (runTimelineMatch) {
|
|
4724
|
+
const runId = decodeURIComponent(runTimelineMatch[1]);
|
|
4725
|
+
const limit = Number.parseInt(url.searchParams.get("limit") || "500", 10);
|
|
4726
|
+
const cursor = normalizeString(url.searchParams.get("cursor"));
|
|
4727
|
+
const page = await readRunTimelinePage(state.projectRoot, runId, { limit, cursor });
|
|
4728
|
+
return deps.jsonResponse(page);
|
|
4729
|
+
}
|
|
4500
4730
|
const runSteerMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/steer$/);
|
|
4501
4731
|
if (runSteerMatch && req.method === "POST") {
|
|
4502
4732
|
const runId = decodeURIComponent(runSteerMatch[1]);
|
|
@@ -167,6 +167,18 @@ function readJsonlFileTail(path, options) {
|
|
|
167
167
|
function readRawRunLogs(projectRoot, runId) {
|
|
168
168
|
return readJsonlFile(runLogsPath(projectRoot, runId)).filter((entry) => Boolean(entry && typeof entry === "object"));
|
|
169
169
|
}
|
|
170
|
+
async function readRunTimelinePage(projectRoot, runId, options = {}) {
|
|
171
|
+
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
|
|
172
|
+
const cursor = options.cursor == null ? 0 : Number.parseInt(options.cursor, 10);
|
|
173
|
+
const entries = readJsonlFile(runTimelinePath(projectRoot, runId)).filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
|
|
174
|
+
const startInclusive = Number.isFinite(cursor) ? Math.max(0, Math.min(cursor, entries.length)) : 0;
|
|
175
|
+
const endExclusive = Math.min(entries.length, startInclusive + limit);
|
|
176
|
+
return {
|
|
177
|
+
entries: entries.slice(startInclusive, endExclusive).map((entry, offset) => ({ ...entry, cursor: startInclusive + offset + 1 })),
|
|
178
|
+
nextCursor: String(endExclusive),
|
|
179
|
+
hasMore: endExclusive < entries.length
|
|
180
|
+
};
|
|
181
|
+
}
|
|
170
182
|
var INITIAL_RUN_LOG_TAIL_MAX_BYTES = 8 * 1024 * 1024;
|
|
171
183
|
async function readRunLogsPage(projectRoot, runId, options = {}) {
|
|
172
184
|
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
|
|
@@ -252,6 +264,7 @@ export {
|
|
|
252
264
|
remoteArtifactsRoot,
|
|
253
265
|
readUserInputsForRuns,
|
|
254
266
|
readUserInputs,
|
|
267
|
+
readRunTimelinePage,
|
|
255
268
|
readRunLogsPage,
|
|
256
269
|
readRunDetails,
|
|
257
270
|
readRawRunLogs,
|