@reconcrap/boss-recommend-mcp 1.3.39 → 2.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.
Files changed (85) hide show
  1. package/README.md +53 -33
  2. package/package.json +61 -9
  3. package/skills/boss-recommend-pipeline/SKILL.md +4 -0
  4. package/src/chat-mcp.js +1333 -0
  5. package/src/chat-runtime-config.js +559 -0
  6. package/src/cli.js +1095 -196
  7. package/src/core/browser/index.js +378 -0
  8. package/src/core/capture/index.js +298 -0
  9. package/src/core/cv-acquisition/index.js +219 -0
  10. package/src/core/greet-quota/index.js +54 -0
  11. package/src/core/infinite-list/index.js +459 -0
  12. package/src/core/reporting/legacy-csv.js +332 -0
  13. package/src/core/run/index.js +286 -0
  14. package/src/core/screening/index.js +1166 -0
  15. package/src/core/self-heal/index.js +848 -0
  16. package/src/domains/chat/cards.js +129 -0
  17. package/src/domains/chat/constants.js +183 -0
  18. package/src/domains/chat/detail.js +1369 -0
  19. package/src/domains/chat/index.js +7 -0
  20. package/src/domains/chat/jobs.js +334 -0
  21. package/src/domains/chat/page-guard.js +88 -0
  22. package/src/domains/chat/roots.js +56 -0
  23. package/src/domains/chat/run-service.js +1101 -0
  24. package/src/domains/recommend/actions.js +457 -0
  25. package/src/domains/recommend/cards.js +228 -0
  26. package/src/domains/recommend/constants.js +141 -0
  27. package/src/domains/recommend/detail.js +341 -0
  28. package/src/domains/recommend/filters.js +581 -0
  29. package/src/domains/recommend/index.js +10 -0
  30. package/src/domains/recommend/jobs.js +232 -0
  31. package/src/domains/recommend/refresh.js +204 -0
  32. package/src/domains/recommend/roots.js +78 -0
  33. package/src/domains/recommend/run-service.js +903 -0
  34. package/src/domains/recommend/scopes.js +245 -0
  35. package/src/domains/recruit/actions.js +277 -0
  36. package/src/domains/recruit/cards.js +67 -0
  37. package/src/domains/recruit/constants.js +130 -0
  38. package/src/domains/recruit/detail.js +414 -0
  39. package/src/domains/recruit/index.js +9 -0
  40. package/src/domains/recruit/instruction-parser.js +451 -0
  41. package/src/domains/recruit/refresh.js +40 -0
  42. package/src/domains/recruit/roots.js +68 -0
  43. package/src/domains/recruit/run-service.js +580 -0
  44. package/src/domains/recruit/search.js +1149 -0
  45. package/src/index.js +578 -419
  46. package/src/recommend-mcp.js +1257 -0
  47. package/src/recruit-mcp.js +1035 -0
  48. package/src/adapters.js +0 -3079
  49. package/src/boss-chat.js +0 -1037
  50. package/src/pipeline.js +0 -2249
  51. package/src/recommend-healing-config.js +0 -131
  52. package/src/recommend-healing-rules.json +0 -261
  53. package/src/self-heal.js +0 -2237
  54. package/src/test-adapters-runtime.js +0 -628
  55. package/src/test-boss-chat.js +0 -3196
  56. package/src/test-index-async.js +0 -498
  57. package/src/test-parser.js +0 -742
  58. package/src/test-pipeline.js +0 -2703
  59. package/src/test-run-state.js +0 -152
  60. package/src/test-self-heal.js +0 -224
  61. package/vendor/boss-chat-cli/README.md +0 -134
  62. package/vendor/boss-chat-cli/package.json +0 -53
  63. package/vendor/boss-chat-cli/src/app.js +0 -1501
  64. package/vendor/boss-chat-cli/src/browser/chat-page.js +0 -3562
  65. package/vendor/boss-chat-cli/src/cli.js +0 -1713
  66. package/vendor/boss-chat-cli/src/mcp/server.js +0 -149
  67. package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +0 -193
  68. package/vendor/boss-chat-cli/src/runtime/async-run-state.js +0 -260
  69. package/vendor/boss-chat-cli/src/runtime/interaction.js +0 -102
  70. package/vendor/boss-chat-cli/src/runtime/run-control.js +0 -102
  71. package/vendor/boss-chat-cli/src/services/chrome-client.js +0 -107
  72. package/vendor/boss-chat-cli/src/services/llm.js +0 -1292
  73. package/vendor/boss-chat-cli/src/services/llm.test.js +0 -326
  74. package/vendor/boss-chat-cli/src/services/profile-store.js +0 -173
  75. package/vendor/boss-chat-cli/src/services/report-store.js +0 -317
  76. package/vendor/boss-chat-cli/src/services/resume-capture.js +0 -469
  77. package/vendor/boss-chat-cli/src/services/resume-network.js +0 -727
  78. package/vendor/boss-chat-cli/src/services/state-store.js +0 -90
  79. package/vendor/boss-chat-cli/src/utils/customer-key.js +0 -82
  80. package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +0 -7072
  81. package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +0 -817
  82. package/vendor/boss-recommend-screen-cli/scripts/stitch_resume_chunks.py +0 -141
  83. package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +0 -2423
  84. package/vendor/boss-recommend-search-cli/src/cli.js +0 -1698
  85. package/vendor/boss-recommend-search-cli/src/test-job-selection.js +0 -211
@@ -0,0 +1,332 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export const LEGACY_INPUT_HEADER = ["运行输入字段", "运行输入值"];
5
+
6
+ export const LEGACY_RESULT_HEADER = [
7
+ "姓名",
8
+ "最高学历学校",
9
+ "最高学历专业",
10
+ "最近工作公司",
11
+ "最近工作职位",
12
+ "评估通过详细原因",
13
+ "处理结果",
14
+ "判断依据(CoT)",
15
+ "动作执行结果",
16
+ "简历来源",
17
+ "原始判定通过",
18
+ "最终判定通过",
19
+ "证据总数",
20
+ "证据命中数",
21
+ "证据门控降级",
22
+ "错误码",
23
+ "错误信息",
24
+ "候选人ID",
25
+ "总耗时ms",
26
+ "候选卡片读取ms",
27
+ "点击候选人ms",
28
+ "详情打开ms",
29
+ "network简历等待ms",
30
+ "文本模型ms",
31
+ "截图获取ms",
32
+ "视觉模型ms",
33
+ "late network retry ms",
34
+ "DOM fallback ms",
35
+ "通过后动作ms",
36
+ "关闭详情ms",
37
+ "休息ms",
38
+ "checkpoint保存ms"
39
+ ];
40
+
41
+ const SEARCH_PARAM_ORDER = [
42
+ "school_tag",
43
+ "degree",
44
+ "degrees",
45
+ "gender",
46
+ "recent_not_view",
47
+ "city",
48
+ "schools",
49
+ "keyword",
50
+ "filter_recent_viewed",
51
+ "job",
52
+ "start_from",
53
+ "target_count",
54
+ "detail_source"
55
+ ];
56
+
57
+ const SCREEN_PARAM_ORDER = [
58
+ "criteria",
59
+ "target_count",
60
+ "post_action",
61
+ "max_greet_count"
62
+ ];
63
+
64
+ function normalizeText(value) {
65
+ return String(value || "").replace(/\s+/g, " ").trim();
66
+ }
67
+
68
+ function normalizeBlockText(value) {
69
+ return String(value ?? "").trim();
70
+ }
71
+
72
+ function csvCell(value) {
73
+ const text = String(value ?? "");
74
+ return `"${text.replace(/"/g, '""')}"`;
75
+ }
76
+
77
+ function ensureDirectory(dirPath) {
78
+ fs.mkdirSync(dirPath, { recursive: true });
79
+ }
80
+
81
+ function cloneJson(value, fallback = null) {
82
+ try {
83
+ return value === undefined ? fallback : JSON.parse(JSON.stringify(value));
84
+ } catch {
85
+ return fallback;
86
+ }
87
+ }
88
+
89
+ function formatInputValue(value) {
90
+ if (value === undefined) return "";
91
+ if (value === null) return "null";
92
+ if (typeof value === "string") return value;
93
+ return JSON.stringify(value);
94
+ }
95
+
96
+ function appendInputRow(rows, field, value) {
97
+ if (!field || value === undefined) return;
98
+ rows.push({
99
+ field,
100
+ value: formatInputValue(value)
101
+ });
102
+ }
103
+
104
+ function appendPrefixedRows(rows, prefix, values = {}, order = []) {
105
+ const source = values && typeof values === "object" && !Array.isArray(values) ? values : {};
106
+ const emitted = new Set();
107
+ for (const key of order) {
108
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
109
+ appendInputRow(rows, `${prefix}.${key}`, source[key]);
110
+ emitted.add(key);
111
+ }
112
+ }
113
+ for (const key of Object.keys(source).sort()) {
114
+ if (emitted.has(key)) continue;
115
+ appendInputRow(rows, `${prefix}.${key}`, source[key]);
116
+ }
117
+ }
118
+
119
+ export function buildLegacyScreenInputRows({
120
+ instruction = "",
121
+ selectedPage = "",
122
+ selectedJob = null,
123
+ userSearchParams = {},
124
+ effectiveSearchParams = {},
125
+ screenParams = {},
126
+ followUp = null,
127
+ extraRows = []
128
+ } = {}) {
129
+ const rows = [];
130
+ appendInputRow(rows, "instruction", instruction);
131
+ appendInputRow(rows, "selected_page", selectedPage);
132
+
133
+ if (selectedJob && typeof selectedJob === "object") {
134
+ appendInputRow(rows, "selected_job.value", selectedJob.value);
135
+ appendInputRow(rows, "selected_job.title", selectedJob.title);
136
+ appendInputRow(rows, "selected_job.label", selectedJob.label);
137
+ } else if (selectedJob) {
138
+ appendInputRow(rows, "selected_job.label", selectedJob);
139
+ }
140
+
141
+ appendPrefixedRows(rows, "user_search_params", userSearchParams, SEARCH_PARAM_ORDER);
142
+ appendPrefixedRows(rows, "effective_search_params", effectiveSearchParams, SEARCH_PARAM_ORDER);
143
+ appendPrefixedRows(rows, "screen_params", screenParams, SCREEN_PARAM_ORDER);
144
+ appendInputRow(rows, "follow_up", followUp);
145
+
146
+ for (const row of extraRows || []) {
147
+ if (Array.isArray(row)) appendInputRow(rows, row[0], row[1]);
148
+ else appendInputRow(rows, row?.field, row?.value);
149
+ }
150
+ return rows;
151
+ }
152
+
153
+ export function defaultLegacyCsvPathForReport(reportPath) {
154
+ const resolved = path.resolve(reportPath);
155
+ const parsed = path.parse(resolved);
156
+ return path.join(parsed.dir, `${parsed.name}.csv`);
157
+ }
158
+
159
+ function firstDefined(...values) {
160
+ for (const value of values) {
161
+ if (value !== undefined && value !== null) return value;
162
+ }
163
+ return "";
164
+ }
165
+
166
+ function firstText(...values) {
167
+ for (const value of values) {
168
+ const text = normalizeBlockText(value);
169
+ if (text) return text;
170
+ }
171
+ return "";
172
+ }
173
+
174
+ function firstBoolean(...values) {
175
+ for (const value of values) {
176
+ if (typeof value === "boolean") return value;
177
+ if (typeof value === "number") return value !== 0;
178
+ const text = normalizeText(value).toLowerCase();
179
+ if (["true", "pass", "passed", "yes", "是", "通过", "符合"].includes(text)) return true;
180
+ if (["false", "fail", "failed", "no", "否", "不通过", "不符合"].includes(text)) return false;
181
+ }
182
+ return "";
183
+ }
184
+
185
+ function evidenceCount(llm = {}) {
186
+ if (Number.isFinite(llm.evidence_count)) return llm.evidence_count;
187
+ if (Array.isArray(llm.evidence)) return llm.evidence.length;
188
+ return "";
189
+ }
190
+
191
+ function actionResultText(row = {}) {
192
+ const action = row.post_action || row.action || {};
193
+ if (action.requested === true && !action.skipped) {
194
+ return firstText(action.reason, action.kind, action.type, "requested");
195
+ }
196
+ if (action.skipped) {
197
+ return firstText(action.reason, action.kind, action.type, "skipped");
198
+ }
199
+ if (action.action_clicked || action.clicked) {
200
+ return firstText(action.effective, action.requested, action.kind, action.type, "clicked");
201
+ }
202
+ if (action.action_attempted || action.attempted) return "failed";
203
+ if (action.requested && action.requested !== "none") return "not_attempted";
204
+ return "";
205
+ }
206
+
207
+ function pickLlm(row = {}) {
208
+ return row.llm
209
+ || row.llm_screening
210
+ || row.detail?.llm_screening
211
+ || row.screening?.llm
212
+ || {};
213
+ }
214
+
215
+ function pickCandidate(row = {}) {
216
+ const screeningCandidate = row.screening?.candidate || {};
217
+ const candidate = row.candidate || row.card_candidate || {};
218
+ return {
219
+ ...screeningCandidate,
220
+ ...candidate,
221
+ identity: {
222
+ ...(screeningCandidate.identity || {}),
223
+ ...(candidate.identity || {})
224
+ }
225
+ };
226
+ }
227
+
228
+ function timingValue(row = {}, ...keys) {
229
+ const timings = row.timings || row.timing || {};
230
+ for (const key of keys) {
231
+ const value = firstDefined(row[key], timings[key]);
232
+ if (value !== "") return value;
233
+ }
234
+ return "";
235
+ }
236
+
237
+ export function legacyScreenResultRow(row = {}) {
238
+ const candidate = pickCandidate(row);
239
+ const identity = candidate.identity || {};
240
+ const detail = row.detail || {};
241
+ const screening = row.screening || {};
242
+ const llm = pickLlm(row);
243
+ const rawPassed = firstBoolean(llm.passed, screening.passed, row.raw_passed, row.passed);
244
+ const finalPassed = firstBoolean(row.final_passed, row.finalPassed, rawPassed);
245
+ const hasError = Boolean(row.error || row.error_code || row.error_message);
246
+ const processResult = hasError
247
+ ? "error"
248
+ : finalPassed === true
249
+ ? "passed"
250
+ : "skipped";
251
+ const cot = firstText(
252
+ llm.reasoning_content,
253
+ llm.raw_reasoning_content,
254
+ llm.decision_cot,
255
+ llm.cot,
256
+ llm.raw_model_output,
257
+ llm.raw_content,
258
+ row.decision_cot,
259
+ row.cot,
260
+ screening.decision_cot,
261
+ screening.cot
262
+ );
263
+ const error = row.error || {};
264
+ const cvSource = firstText(
265
+ detail.cv_acquisition?.source,
266
+ row.cv_source,
267
+ candidate.source,
268
+ screening.candidate?.source
269
+ );
270
+ const totalEvidence = evidenceCount(llm);
271
+ return [
272
+ identity.name,
273
+ identity.school,
274
+ identity.major,
275
+ identity.current_company,
276
+ identity.current_position,
277
+ "",
278
+ processResult,
279
+ cot,
280
+ actionResultText(row),
281
+ cvSource,
282
+ rawPassed,
283
+ finalPassed,
284
+ totalEvidence,
285
+ totalEvidence,
286
+ "",
287
+ row.error_code || error.code || error.name || "",
288
+ row.error_message || error.message || "",
289
+ candidate.id || row.candidate_id || "",
290
+ timingValue(row, "total_ms"),
291
+ timingValue(row, "card_read_ms"),
292
+ timingValue(row, "candidate_click_ms"),
293
+ timingValue(row, "detail_open_ms"),
294
+ timingValue(row, "network_cv_wait_ms"),
295
+ timingValue(row, "text_model_ms"),
296
+ timingValue(row, "screenshot_capture_ms"),
297
+ timingValue(row, "vision_model_ms"),
298
+ timingValue(row, "late_network_retry_ms"),
299
+ timingValue(row, "dom_fallback_ms"),
300
+ timingValue(row, "post_action_ms"),
301
+ timingValue(row, "close_detail_ms"),
302
+ timingValue(row, "sleep_ms"),
303
+ timingValue(row, "checkpoint_save_ms")
304
+ ];
305
+ }
306
+
307
+ export function writeLegacyScreenCsv(filePath, {
308
+ inputRows = [],
309
+ results = []
310
+ } = {}) {
311
+ const resolved = path.resolve(filePath);
312
+ ensureDirectory(path.dirname(resolved));
313
+ const normalizedInputRows = (inputRows || []).map((row) => ({
314
+ field: row?.field ?? row?.[0] ?? "",
315
+ value: row?.value ?? row?.[1] ?? ""
316
+ }));
317
+ const lines = [
318
+ LEGACY_INPUT_HEADER.map(csvCell).join(","),
319
+ ...normalizedInputRows.map((row) => [row.field, row.value].map(csvCell).join(",")),
320
+ "",
321
+ LEGACY_RESULT_HEADER.map(csvCell).join(","),
322
+ ...(results || []).map((row) => legacyScreenResultRow(row).map(csvCell).join(","))
323
+ ];
324
+ const tempPath = `${resolved}.tmp`;
325
+ fs.writeFileSync(tempPath, `\uFEFF${lines.join("\n")}\n`, "utf8");
326
+ fs.renameSync(tempPath, resolved);
327
+ return resolved;
328
+ }
329
+
330
+ export function cloneReportInput(value, fallback = {}) {
331
+ return cloneJson(value, fallback);
332
+ }
@@ -0,0 +1,286 @@
1
+ export const RUN_STATUS_QUEUED = "queued";
2
+ export const RUN_STATUS_RUNNING = "running";
3
+ export const RUN_STATUS_PAUSED = "paused";
4
+ export const RUN_STATUS_COMPLETED = "completed";
5
+ export const RUN_STATUS_CANCELING = "canceling";
6
+ export const RUN_STATUS_CANCELED = "canceled";
7
+ export const RUN_STATUS_FAILED = "failed";
8
+
9
+ const TERMINAL_STATUSES = new Set([
10
+ RUN_STATUS_COMPLETED,
11
+ RUN_STATUS_CANCELED,
12
+ RUN_STATUS_FAILED
13
+ ]);
14
+
15
+ export class RunCanceledError extends Error {
16
+ constructor(message = "Run canceled") {
17
+ super(message);
18
+ this.name = "RunCanceledError";
19
+ }
20
+ }
21
+
22
+ function nowIso() {
23
+ return new Date().toISOString();
24
+ }
25
+
26
+ function createRunId(prefix = "run") {
27
+ const random = Math.random().toString(36).slice(2, 10);
28
+ return `${prefix}_${Date.now().toString(36)}_${random}`;
29
+ }
30
+
31
+ function clone(value) {
32
+ return JSON.parse(JSON.stringify(value));
33
+ }
34
+
35
+ function createDeferred() {
36
+ let resolve;
37
+ let reject;
38
+ const promise = new Promise((promiseResolve, promiseReject) => {
39
+ resolve = promiseResolve;
40
+ reject = promiseReject;
41
+ });
42
+ return { promise, resolve, reject };
43
+ }
44
+
45
+ function snapshotFromEntry(entry) {
46
+ const run = entry.run;
47
+ return clone({
48
+ runId: run.runId,
49
+ name: run.name,
50
+ status: run.status,
51
+ phase: run.phase,
52
+ progress: run.progress,
53
+ context: run.context,
54
+ checkpoint: run.checkpoint,
55
+ startedAt: run.startedAt,
56
+ updatedAt: run.updatedAt,
57
+ completedAt: run.completedAt,
58
+ canPause: run.status === RUN_STATUS_RUNNING,
59
+ canResume: run.status === RUN_STATUS_PAUSED,
60
+ canCancel: !TERMINAL_STATUSES.has(run.status),
61
+ error: run.error,
62
+ summary: run.summary
63
+ });
64
+ }
65
+
66
+ export function createRunLifecycleManager({
67
+ idPrefix = "run",
68
+ now = nowIso
69
+ } = {}) {
70
+ const runs = new Map();
71
+
72
+ function getEntry(runId) {
73
+ const entry = runs.get(runId);
74
+ if (!entry) throw new Error(`Unknown runId: ${runId}`);
75
+ return entry;
76
+ }
77
+
78
+ function touch(entry) {
79
+ entry.run.updatedAt = now();
80
+ }
81
+
82
+ function setStatus(entry, status, patch = {}) {
83
+ entry.run.status = status;
84
+ Object.assign(entry.run, patch);
85
+ touch(entry);
86
+ }
87
+
88
+ function createControls(entry) {
89
+ return {
90
+ signal: entry.controller.signal,
91
+ get runId() {
92
+ return entry.run.runId;
93
+ },
94
+ get status() {
95
+ return entry.run.status;
96
+ },
97
+ setPhase(phase) {
98
+ entry.run.phase = phase;
99
+ touch(entry);
100
+ },
101
+ updateProgress(progressPatch = {}) {
102
+ entry.run.progress = {
103
+ ...entry.run.progress,
104
+ ...progressPatch
105
+ };
106
+ touch(entry);
107
+ return snapshotFromEntry(entry);
108
+ },
109
+ checkpoint(checkpointPatch = {}) {
110
+ entry.run.checkpoint = {
111
+ ...entry.run.checkpoint,
112
+ ...checkpointPatch,
113
+ updatedAt: now()
114
+ };
115
+ touch(entry);
116
+ return snapshotFromEntry(entry);
117
+ },
118
+ async waitIfPaused() {
119
+ if (entry.controller.signal.aborted) {
120
+ throw new RunCanceledError();
121
+ }
122
+ if (!entry.pauseRequested) return;
123
+ setStatus(entry, RUN_STATUS_PAUSED);
124
+ while (entry.pauseRequested) {
125
+ const deferred = createDeferred();
126
+ entry.pauseWaiters.add(deferred);
127
+ try {
128
+ await deferred.promise;
129
+ } finally {
130
+ entry.pauseWaiters.delete(deferred);
131
+ }
132
+ if (entry.controller.signal.aborted) {
133
+ throw new RunCanceledError();
134
+ }
135
+ }
136
+ setStatus(entry, RUN_STATUS_RUNNING);
137
+ },
138
+ async sleep(ms) {
139
+ if (entry.controller.signal.aborted) {
140
+ throw new RunCanceledError();
141
+ }
142
+ await new Promise((resolve, reject) => {
143
+ const timer = setTimeout(resolve, ms);
144
+ const onAbort = () => {
145
+ clearTimeout(timer);
146
+ reject(new RunCanceledError());
147
+ };
148
+ entry.controller.signal.addEventListener("abort", onAbort, { once: true });
149
+ });
150
+ },
151
+ throwIfCanceled() {
152
+ if (entry.controller.signal.aborted) {
153
+ throw new RunCanceledError();
154
+ }
155
+ }
156
+ };
157
+ }
158
+
159
+ async function settle(entry, task) {
160
+ try {
161
+ const summary = await task(entry.controls);
162
+ if (entry.controller.signal.aborted || entry.cancelRequested) {
163
+ setStatus(entry, RUN_STATUS_CANCELED, {
164
+ completedAt: now(),
165
+ summary: summary || entry.run.summary
166
+ });
167
+ } else {
168
+ setStatus(entry, RUN_STATUS_COMPLETED, {
169
+ completedAt: now(),
170
+ summary: summary || entry.run.summary
171
+ });
172
+ }
173
+ } catch (error) {
174
+ if (error instanceof RunCanceledError || entry.controller.signal.aborted || entry.cancelRequested) {
175
+ setStatus(entry, RUN_STATUS_CANCELED, {
176
+ completedAt: now(),
177
+ error: null
178
+ });
179
+ return;
180
+ }
181
+ setStatus(entry, RUN_STATUS_FAILED, {
182
+ completedAt: now(),
183
+ error: {
184
+ name: error?.name || "Error",
185
+ message: error?.message || String(error)
186
+ }
187
+ });
188
+ }
189
+ }
190
+
191
+ function startRun({ name, context = {}, progress = {}, checkpoint = {}, task }) {
192
+ if (typeof task !== "function") {
193
+ throw new Error("startRun requires a task function");
194
+ }
195
+ const runId = createRunId(idPrefix);
196
+ const startedAt = now();
197
+ const entry = {
198
+ controller: new AbortController(),
199
+ pauseRequested: false,
200
+ cancelRequested: false,
201
+ pauseWaiters: new Set(),
202
+ run: {
203
+ runId,
204
+ name: name || runId,
205
+ status: RUN_STATUS_QUEUED,
206
+ phase: "queued",
207
+ progress,
208
+ context,
209
+ checkpoint,
210
+ startedAt,
211
+ updatedAt: startedAt,
212
+ completedAt: null,
213
+ error: null,
214
+ summary: null
215
+ }
216
+ };
217
+ entry.controls = createControls(entry);
218
+ runs.set(runId, entry);
219
+ setStatus(entry, RUN_STATUS_RUNNING, { phase: "running" });
220
+ entry.promise = settle(entry, task);
221
+ return snapshotFromEntry(entry);
222
+ }
223
+
224
+ function getRun(runId) {
225
+ return snapshotFromEntry(getEntry(runId));
226
+ }
227
+
228
+ function pauseRun(runId) {
229
+ const entry = getEntry(runId);
230
+ if (TERMINAL_STATUSES.has(entry.run.status)) return snapshotFromEntry(entry);
231
+ entry.pauseRequested = true;
232
+ if (entry.run.status === RUN_STATUS_RUNNING) {
233
+ touch(entry);
234
+ }
235
+ return snapshotFromEntry(entry);
236
+ }
237
+
238
+ function resumeRun(runId) {
239
+ const entry = getEntry(runId);
240
+ if (TERMINAL_STATUSES.has(entry.run.status)) return snapshotFromEntry(entry);
241
+ entry.pauseRequested = false;
242
+ for (const waiter of entry.pauseWaiters) {
243
+ waiter.resolve();
244
+ }
245
+ if (entry.run.status === RUN_STATUS_PAUSED) {
246
+ setStatus(entry, RUN_STATUS_RUNNING);
247
+ } else {
248
+ touch(entry);
249
+ }
250
+ return snapshotFromEntry(entry);
251
+ }
252
+
253
+ function cancelRun(runId) {
254
+ const entry = getEntry(runId);
255
+ if (TERMINAL_STATUSES.has(entry.run.status)) return snapshotFromEntry(entry);
256
+ entry.cancelRequested = true;
257
+ setStatus(entry, RUN_STATUS_CANCELING);
258
+ entry.controller.abort();
259
+ entry.pauseRequested = false;
260
+ for (const waiter of entry.pauseWaiters) {
261
+ waiter.resolve();
262
+ }
263
+ return snapshotFromEntry(entry);
264
+ }
265
+
266
+ async function waitForRun(runId, { timeoutMs = 10000 } = {}) {
267
+ const entry = getEntry(runId);
268
+ const timeout = new Promise((_, reject) => {
269
+ setTimeout(() => reject(new Error(`Timed out waiting for run ${runId}`)), timeoutMs);
270
+ });
271
+ await Promise.race([entry.promise, timeout]);
272
+ return snapshotFromEntry(entry);
273
+ }
274
+
275
+ return {
276
+ startRun,
277
+ getRun,
278
+ pauseRun,
279
+ resumeRun,
280
+ cancelRun,
281
+ waitForRun,
282
+ listRuns() {
283
+ return Array.from(runs.values()).map(snapshotFromEntry);
284
+ }
285
+ };
286
+ }