opencode-magi 0.5.0 → 0.6.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.
@@ -15,7 +15,6 @@ const DEFAULT_CLEAR_OPTIONS = {
15
15
  session: true,
16
16
  worktree: true,
17
17
  };
18
- const SYNC_RUN_TIMEOUT_MS = 600_000;
19
18
  function createRunId() {
20
19
  return `run-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
21
20
  }
@@ -152,6 +151,12 @@ function ciReportText(input) {
152
151
  const scopeInside = input.report.scopeInside.length;
153
152
  return `CI report for ${input.pr}: ${failed} failed, ${scopeInside} scope-in, ${rerun} rerun, ${recovered} recovered, ${unresolved} unresolved.`;
154
153
  }
154
+ function ciClassifierCompletedText(input) {
155
+ const summary = input.checks
156
+ .map((check) => `${check.name}: ${check.classification} - ${check.reason}`)
157
+ .join("; ");
158
+ return `**CI classifier ${input.reviewer}** completed for ${input.pr}: ${summary}`;
159
+ }
155
160
  function closeReconsiderationText(input) {
156
161
  if (input.to === "MERGE") {
157
162
  return `**Reviewer ${input.reviewer}** changed their close request to approval for ${input.pr}.`;
@@ -404,6 +409,8 @@ function extractQuestionRequest(properties) {
404
409
  export class MagiRunManager {
405
410
  input;
406
411
  active = new Map();
412
+ activePrRuns = 0;
413
+ activeTriageRuns = 0;
407
414
  countedToolParts = new Map();
408
415
  controllers = new Map();
409
416
  eventLastUpdates = new Map();
@@ -411,6 +418,8 @@ export class MagiRunManager {
411
418
  runPaths = new Map();
412
419
  outputDirs = new Set();
413
420
  sessionToRun = new Map();
421
+ prQueue = [];
422
+ triageQueue = [];
414
423
  constructor(input) {
415
424
  this.input = input;
416
425
  }
@@ -460,10 +469,13 @@ export class MagiRunManager {
460
469
  signal: controller.signal,
461
470
  });
462
471
  if (input.sync)
463
- return this.executeSync(state, controller, execute);
464
- void execute().catch(async (error) => {
465
- await this.failRun(runId, error);
472
+ return this.executeSync(state, controller, execute, input.timeoutMs);
473
+ this.prQueue.push({
474
+ execute,
475
+ repository: input.repository,
476
+ runId,
466
477
  });
478
+ this.drainPrQueue();
467
479
  return state;
468
480
  }
469
481
  async startMerge(input) {
@@ -521,10 +533,13 @@ export class MagiRunManager {
521
533
  signal: controller.signal,
522
534
  });
523
535
  if (input.sync)
524
- return this.executeSync(state, controller, execute);
525
- void execute().catch(async (error) => {
526
- await this.failRun(runId, error);
536
+ return this.executeSync(state, controller, execute, input.timeoutMs);
537
+ this.prQueue.push({
538
+ execute,
539
+ repository: input.repository,
540
+ runId,
527
541
  });
542
+ this.drainPrQueue();
528
543
  return state;
529
544
  }
530
545
  async startTriage(input) {
@@ -581,14 +596,64 @@ export class MagiRunManager {
581
596
  signal: controller.signal,
582
597
  });
583
598
  if (input.sync)
584
- return this.executeSync(state, controller, execute);
585
- void execute().catch(async (error) => {
586
- await this.failRun(runId, error);
599
+ return this.executeSync(state, controller, execute, input.timeoutMs);
600
+ this.triageQueue.push({
601
+ execute,
602
+ repository: input.repository,
603
+ runId,
587
604
  });
605
+ this.drainTriageQueue();
588
606
  return state;
589
607
  }
608
+ drainPrQueue() {
609
+ while (this.prQueue.length) {
610
+ const next = this.prQueue[0];
611
+ if (!next)
612
+ return;
613
+ if (this.activePrRuns >= next.repository.concurrency.runs)
614
+ return;
615
+ this.prQueue.shift();
616
+ const state = this.active.get(next.runId);
617
+ if (!state || state.status === "cancelled")
618
+ continue;
619
+ this.activePrRuns += 1;
620
+ void next
621
+ .execute()
622
+ .catch(async (error) => {
623
+ await this.failRun(next.runId, error);
624
+ })
625
+ .finally(() => {
626
+ this.activePrRuns -= 1;
627
+ this.drainPrQueue();
628
+ });
629
+ }
630
+ }
631
+ drainTriageQueue() {
632
+ while (this.triageQueue.length) {
633
+ const next = this.triageQueue[0];
634
+ if (!next)
635
+ return;
636
+ const limit = next.repository.triage?.concurrency.runs ?? 1;
637
+ if (this.activeTriageRuns >= limit)
638
+ return;
639
+ this.triageQueue.shift();
640
+ const state = this.active.get(next.runId);
641
+ if (!state || state.status === "cancelled")
642
+ continue;
643
+ this.activeTriageRuns += 1;
644
+ void next
645
+ .execute()
646
+ .catch(async (error) => {
647
+ await this.failRun(next.runId, error);
648
+ })
649
+ .finally(() => {
650
+ this.activeTriageRuns -= 1;
651
+ this.drainTriageQueue();
652
+ });
653
+ }
654
+ }
590
655
  async status(input = {}) {
591
- const timeoutMs = Math.min(input.timeoutMs ?? 60_000, 600_000);
656
+ const timeoutMs = input.timeoutMs;
592
657
  const startedAt = Date.now();
593
658
  while (input.block) {
594
659
  const states = await this.filteredStates(input);
@@ -596,7 +661,7 @@ export class MagiRunManager {
596
661
  hasAllRequestedPrStates(states, input.pr) &&
597
662
  states.every((state) => !isActiveStatus(state.status)))
598
663
  return states;
599
- if (Date.now() - startedAt >= timeoutMs)
664
+ if (timeoutMs != null && Date.now() - startedAt >= timeoutMs)
600
665
  return states;
601
666
  await new Promise((resolve) => setTimeout(resolve, 1_000));
602
667
  }
@@ -648,53 +713,7 @@ export class MagiRunManager {
648
713
  state.status = "cancelled";
649
714
  state.phase = "cancelled";
650
715
  state.completedAt = now();
651
- if (state.editor?.status === "pending" ||
652
- state.editor?.status === "running" ||
653
- state.editor?.status === "repairing") {
654
- state.editor.status = "cancelled";
655
- }
656
- if (state.editor?.sessionId) {
657
- await this.input.client.session
658
- .abort?.({ path: { id: state.editor.sessionId } })
659
- .catch(() => undefined);
660
- }
661
- if (state.triageCreator?.status === "pending" ||
662
- state.triageCreator?.status === "running" ||
663
- state.triageCreator?.status === "repairing" ||
664
- state.triageCreator?.status === "blocked") {
665
- state.triageCreator.status = "cancelled";
666
- }
667
- if (state.triageCreator?.sessionId) {
668
- await this.input.client.session
669
- .abort?.({ path: { id: state.triageCreator.sessionId } })
670
- .catch(() => undefined);
671
- }
672
- for (const reviewer of Object.values(state.reviewers)) {
673
- if (reviewer.status === "pending" ||
674
- reviewer.status === "running" ||
675
- reviewer.status === "repairing" ||
676
- reviewer.status === "blocked") {
677
- reviewer.status = "cancelled";
678
- }
679
- if (reviewer.sessionId) {
680
- await this.input.client.session
681
- .abort?.({ path: { id: reviewer.sessionId } })
682
- .catch(() => undefined);
683
- }
684
- }
685
- for (const classifier of Object.values(state.ciClassifiers ?? {})) {
686
- if (classifier.status === "pending" ||
687
- classifier.status === "running" ||
688
- classifier.status === "repairing" ||
689
- classifier.status === "blocked") {
690
- classifier.status = "cancelled";
691
- }
692
- if (classifier.sessionId) {
693
- await this.input.client.session
694
- .abort?.({ path: { id: classifier.sessionId } })
695
- .catch(() => undefined);
696
- }
697
- }
716
+ await this.finishActiveAgents(state, "cancelled");
698
717
  if (state.worktreePath) {
699
718
  await removeWorktree(this.input.exec, state.worktreePath).catch(() => undefined);
700
719
  }
@@ -1195,6 +1214,26 @@ export class MagiRunManager {
1195
1214
  ...Object.entries(state.reviewers),
1196
1215
  ];
1197
1216
  }
1217
+ isActiveAgent(agent) {
1218
+ return (agent.status === "pending" ||
1219
+ agent.status === "running" ||
1220
+ agent.status === "repairing" ||
1221
+ agent.status === "blocked");
1222
+ }
1223
+ async finishActiveAgents(state, status, error) {
1224
+ for (const [, agent] of this.agentEntries(state)) {
1225
+ if (this.isActiveAgent(agent)) {
1226
+ agent.status = status;
1227
+ if (error != null)
1228
+ agent.error = error;
1229
+ }
1230
+ if (agent.sessionId) {
1231
+ await this.input.client.session
1232
+ .abort?.({ path: { id: agent.sessionId } })
1233
+ .catch(() => undefined);
1234
+ }
1235
+ }
1236
+ }
1198
1237
  selectPendingAgent(state, kind, key, requestId) {
1199
1238
  const entries = key
1200
1239
  ? this.agentState(state, key)
@@ -1220,19 +1259,24 @@ export class MagiRunManager {
1220
1259
  hasBlockedAgents(state) {
1221
1260
  return this.agentEntries(state).some(([, agent]) => agent.status === "blocked");
1222
1261
  }
1223
- async executeSync(state, controller, execute) {
1262
+ async executeSync(state, controller, execute, timeoutMs) {
1224
1263
  let timeout;
1225
- const timeoutPromise = new Promise((resolve) => {
1226
- timeout = setTimeout(() => resolve("timeout"), SYNC_RUN_TIMEOUT_MS);
1227
- });
1264
+ const timeoutPromise = timeoutMs == null
1265
+ ? undefined
1266
+ : new Promise((resolve) => {
1267
+ timeout = setTimeout(() => resolve("timeout"), timeoutMs);
1268
+ });
1228
1269
  try {
1229
- const result = await Promise.race([
1230
- execute().then(() => "completed"),
1231
- timeoutPromise,
1232
- ]);
1270
+ const result = await (timeoutPromise
1271
+ ? Promise.race([
1272
+ execute().then(() => "completed"),
1273
+ timeoutPromise,
1274
+ ])
1275
+ : execute().then(() => "completed"));
1233
1276
  if (result === "timeout") {
1277
+ const timeoutSeconds = (timeoutMs ?? 0) / 1_000;
1234
1278
  controller.abort();
1235
- await this.failRun(state.runId, new Error("Magi sync run timed out after 600 seconds."));
1279
+ await this.failRun(state.runId, new Error(`Magi sync run timed out after ${timeoutSeconds} seconds.`));
1236
1280
  }
1237
1281
  }
1238
1282
  catch (error) {
@@ -1259,6 +1303,7 @@ export class MagiRunManager {
1259
1303
  dryRun: input.dryRun,
1260
1304
  exec: withGitHubApiRetry(this.input.exec, input.config.github?.apiRetryAttempts ?? 3),
1261
1305
  onProgress: (progress) => this.applyReviewProgress(input.runId, progress),
1306
+ parentSessionId: input.parentSessionId,
1262
1307
  pr: input.pr,
1263
1308
  repository: input.repository,
1264
1309
  runId: input.runId,
@@ -1314,6 +1359,7 @@ export class MagiRunManager {
1314
1359
  dryRun: input.dryRun,
1315
1360
  exec: withGitHubApiRetry(this.input.exec, input.config.github?.apiRetryAttempts ?? 3),
1316
1361
  onProgress: (progress) => this.applyMergeProgress(input.runId, progress),
1362
+ parentSessionId: input.parentSessionId,
1317
1363
  pr: input.pr,
1318
1364
  repository: input.repository,
1319
1365
  runId: input.runId,
@@ -1355,6 +1401,7 @@ export class MagiRunManager {
1355
1401
  exec: withGitHubApiRetry(this.input.exec, input.config.github?.apiRetryAttempts ?? 3),
1356
1402
  issue: input.issue,
1357
1403
  onProgress: (progress) => this.applyTriageProgress(input.runId, progress),
1404
+ parentSessionId: input.parentSessionId,
1358
1405
  repository: input.repository,
1359
1406
  runId: input.runId,
1360
1407
  signal: input.signal,
@@ -1395,6 +1442,7 @@ export class MagiRunManager {
1395
1442
  repository: input.repository,
1396
1443
  signal: input.signal,
1397
1444
  sync: input.sync,
1445
+ timeoutMs: input.timeoutMs,
1398
1446
  });
1399
1447
  if (input.sync)
1400
1448
  this.assertSuccessfulSyncFollowUp(followUp);
@@ -1408,6 +1456,7 @@ export class MagiRunManager {
1408
1456
  repository: input.repository,
1409
1457
  signal: input.signal,
1410
1458
  sync: input.sync,
1459
+ timeoutMs: input.timeoutMs,
1411
1460
  });
1412
1461
  if (input.sync)
1413
1462
  this.assertSuccessfulSyncFollowUp(followUp);
@@ -1451,57 +1500,69 @@ export class MagiRunManager {
1451
1500
  state.worktreePath = progress.worktreePath;
1452
1501
  state.worktreeBranch = progress.branch;
1453
1502
  }
1503
+ if (progress.type === "triage_session") {
1504
+ if (progress.options)
1505
+ this.input.setSessionOptions?.(progress.sessionId, progress.options);
1506
+ state.sessionIds = {
1507
+ ...state.sessionIds,
1508
+ [progress.key]: progress.sessionId,
1509
+ };
1510
+ this.sessionToRun.set(progress.sessionId, {
1511
+ agent: progress.agent,
1512
+ runId,
1513
+ });
1514
+ }
1454
1515
  if (progress.type === "triage_agent_started") {
1455
- const reviewer = state.reviewers[progress.reviewer];
1456
- if (reviewer)
1457
- reviewer.status = "running";
1516
+ const voter = state.reviewers[progress.voter];
1517
+ if (voter)
1518
+ voter.status = "running";
1458
1519
  }
1459
1520
  if (progress.type === "triage_agent_session") {
1460
- const reviewer = state.reviewers[progress.reviewer];
1461
- if (reviewer) {
1521
+ const voter = state.reviewers[progress.voter];
1522
+ if (voter) {
1462
1523
  if (progress.options)
1463
1524
  this.input.setSessionOptions?.(progress.sessionId, progress.options);
1464
- reviewer.sessionId = progress.sessionId;
1465
- reviewer.status = "running";
1466
- reviewer.lastUpdate = now();
1525
+ voter.sessionId = progress.sessionId;
1526
+ voter.status = "running";
1527
+ voter.lastUpdate = now();
1467
1528
  this.sessionToRun.set(progress.sessionId, {
1468
- agent: progress.reviewer,
1529
+ agent: progress.voter,
1469
1530
  runId,
1470
1531
  });
1471
1532
  }
1472
1533
  }
1473
1534
  if (progress.type === "triage_agent_repair") {
1474
- const reviewer = state.reviewers[progress.reviewer];
1475
- if (reviewer) {
1476
- reviewer.status = "repairing";
1477
- reviewer.repairAttempts += 1;
1478
- reviewer.lastUpdate = now();
1535
+ const voter = state.reviewers[progress.voter];
1536
+ if (voter) {
1537
+ voter.status = "repairing";
1538
+ voter.repairAttempts += 1;
1539
+ voter.lastUpdate = now();
1479
1540
  }
1480
1541
  }
1481
1542
  if (progress.type === "triage_agent_response") {
1482
- const reviewer = state.reviewers[progress.reviewer];
1483
- if (reviewer) {
1484
- reviewer.sessionId = progress.sessionId;
1485
- reviewer.lastUpdate = now();
1543
+ const voter = state.reviewers[progress.voter];
1544
+ if (voter) {
1545
+ voter.sessionId = progress.sessionId;
1546
+ voter.lastUpdate = now();
1486
1547
  }
1487
1548
  }
1488
1549
  if (progress.type === "triage_agent_completed") {
1489
- const reviewer = state.reviewers[progress.reviewer];
1490
- if (reviewer) {
1491
- reviewer.sessionId = progress.sessionId;
1492
- reviewer.status = "completed";
1493
- reviewer.verdict = progress.vote;
1494
- reviewer.rawPath = join(state.outputDir, `${progress.reviewer}.${progress.phase}.raw.txt`);
1495
- reviewer.parsedPath = join(state.outputDir, `${progress.reviewer}.${progress.phase}.json`);
1496
- reviewer.lastUpdate = now();
1550
+ const voter = state.reviewers[progress.voter];
1551
+ if (voter) {
1552
+ voter.sessionId = progress.sessionId;
1553
+ voter.status = "completed";
1554
+ voter.verdict = progress.vote;
1555
+ voter.rawPath = join(state.outputDir, `${progress.voter}.${progress.phase}.raw.txt`);
1556
+ voter.parsedPath = join(state.outputDir, `${progress.voter}.${progress.phase}.json`);
1557
+ voter.lastUpdate = now();
1497
1558
  }
1498
1559
  }
1499
1560
  if (progress.type === "triage_agent_failed") {
1500
- const reviewer = state.reviewers[progress.reviewer];
1501
- if (reviewer) {
1502
- reviewer.status = "failed";
1503
- reviewer.error = redactSecrets(progress.error);
1504
- reviewer.lastUpdate = now();
1561
+ const voter = state.reviewers[progress.voter];
1562
+ if (voter) {
1563
+ voter.status = "failed";
1564
+ voter.error = redactSecrets(progress.error);
1565
+ voter.lastUpdate = now();
1505
1566
  }
1506
1567
  }
1507
1568
  if (progress.type === "triage_creator_started") {
@@ -1555,16 +1616,16 @@ export class MagiRunManager {
1555
1616
  }));
1556
1617
  }
1557
1618
  if (progress.type === "triage_agent_started") {
1558
- await this.notify(state, `**Triage agent ${progress.reviewer}** started ${progress.phase} for ${issue}.`);
1619
+ await this.notify(state, `**Triage agent ${progress.voter}** started ${progress.phase} for ${issue}.`);
1559
1620
  }
1560
1621
  if (progress.type === "triage_agent_repair") {
1561
- await this.notify(state, `**Triage agent ${progress.reviewer}** started JSON regeneration for ${issue}.`);
1622
+ await this.notify(state, `**Triage agent ${progress.voter}** started JSON regeneration for ${issue}.`);
1562
1623
  }
1563
1624
  if (progress.type === "triage_agent_completed") {
1564
- await this.notify(state, `**Triage agent ${progress.reviewer}** completed ${progress.phase} for ${issue}: ${progress.vote}.`);
1625
+ await this.notify(state, `**Triage agent ${progress.voter}** completed ${progress.phase} for ${issue}: ${progress.vote}.`);
1565
1626
  }
1566
1627
  if (progress.type === "triage_agent_failed") {
1567
- await this.notify(state, `**Triage agent ${progress.reviewer}** failed ${progress.phase} for ${issue}: ${redactSecrets(progress.error)}`);
1628
+ await this.notify(state, `**Triage agent ${progress.voter}** failed ${progress.phase} for ${issue}: ${redactSecrets(progress.error)}`);
1568
1629
  }
1569
1630
  if (progress.type === "comment_posting") {
1570
1631
  await this.notify(state, `Posting triage comment for ${issue}.`);
@@ -1649,9 +1710,8 @@ export class MagiRunManager {
1649
1710
  if (progress.type === "ci_classifier_completed") {
1650
1711
  const classifier = state.ciClassifiers?.[progress.reviewer];
1651
1712
  if (classifier) {
1652
- classifier.classification = progress.classification;
1713
+ classifier.checks = progress.checks;
1653
1714
  classifier.rawPath = progress.rawPath;
1654
- classifier.reason = progress.reason;
1655
1715
  classifier.sessionId = progress.sessionId;
1656
1716
  classifier.status = "completed";
1657
1717
  classifier.lastUpdate = now();
@@ -1759,7 +1819,11 @@ export class MagiRunManager {
1759
1819
  await this.notify(state, `**CI classifier ${progress.reviewer}** started JSON regeneration for ${prMarkdownLink(state)}.`);
1760
1820
  }
1761
1821
  if (progress.type === "ci_classifier_completed") {
1762
- await this.notify(state, `**CI classifier ${progress.reviewer}** completed for ${prMarkdownLink(state)}: ${progress.classification} - ${progress.reason}`);
1822
+ await this.notify(state, ciClassifierCompletedText({
1823
+ checks: progress.checks,
1824
+ pr: prMarkdownLink(state),
1825
+ reviewer: progress.reviewer,
1826
+ }));
1763
1827
  }
1764
1828
  if (progress.type === "ci_classifier_failed") {
1765
1829
  await this.notify(state, `**CI classifier ${progress.reviewer}** failed for ${prMarkdownLink(state)}: ${redactSecrets(progress.error)}`);
@@ -1928,46 +1992,7 @@ export class MagiRunManager {
1928
1992
  state.phase = "failed";
1929
1993
  state.completedAt = now();
1930
1994
  state.error = errorMessage(error);
1931
- if (state.editor?.status === "pending" ||
1932
- state.editor?.status === "running" ||
1933
- state.editor?.status === "repairing" ||
1934
- state.editor?.status === "blocked") {
1935
- state.editor.status = "failed";
1936
- state.editor.error = state.error;
1937
- }
1938
- if (state.editor?.sessionId) {
1939
- await this.input.client.session
1940
- .abort?.({ path: { id: state.editor.sessionId } })
1941
- .catch(() => undefined);
1942
- }
1943
- for (const reviewer of Object.values(state.reviewers)) {
1944
- if (reviewer.status === "pending" ||
1945
- reviewer.status === "running" ||
1946
- reviewer.status === "repairing" ||
1947
- reviewer.status === "blocked") {
1948
- reviewer.status = "failed";
1949
- reviewer.error = state.error;
1950
- }
1951
- if (reviewer.sessionId) {
1952
- await this.input.client.session
1953
- .abort?.({ path: { id: reviewer.sessionId } })
1954
- .catch(() => undefined);
1955
- }
1956
- }
1957
- for (const classifier of Object.values(state.ciClassifiers ?? {})) {
1958
- if (classifier.status === "pending" ||
1959
- classifier.status === "running" ||
1960
- classifier.status === "repairing" ||
1961
- classifier.status === "blocked") {
1962
- classifier.status = "failed";
1963
- classifier.error = state.error;
1964
- }
1965
- if (classifier.sessionId) {
1966
- await this.input.client.session
1967
- .abort?.({ path: { id: classifier.sessionId } })
1968
- .catch(() => undefined);
1969
- }
1970
- }
1995
+ await this.finishActiveAgents(state, "failed", state.error);
1971
1996
  await this.persist(state);
1972
1997
  await this.notify(state, `Magi ${state.command} failed for ${runLabel(state)}: ${state.error}`, { reply: true });
1973
1998
  this.active.delete(runId);