@harness-engineering/orchestrator 0.2.8 → 0.2.10

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/index.d.mts CHANGED
@@ -94,7 +94,14 @@ interface OrchestratorState {
94
94
  running: Map<string, RunningEntry>;
95
95
  claimed: Set<string>;
96
96
  retryAttempts: Map<string, RetryEntry>;
97
- completed: Set<string>;
97
+ /**
98
+ * Tracks completed issue IDs mapped to the epoch-ms timestamp when the
99
+ * completion was recorded. The timestamp enables a grace-period so that
100
+ * issues manually re-activated in the roadmap can be re-dispatched, while
101
+ * still guarding against duplicate dispatch on the tick immediately after
102
+ * completion (when the tracker write-back may not have persisted yet).
103
+ */
104
+ completed: Map<string, number>;
98
105
  tokenTotals: TokenTotals;
99
106
  rateLimits: RateLimitSnapshot$1;
100
107
  /** Running count of claim rejections (another orchestrator won the race). */
package/dist/index.d.ts CHANGED
@@ -94,7 +94,14 @@ interface OrchestratorState {
94
94
  running: Map<string, RunningEntry>;
95
95
  claimed: Set<string>;
96
96
  retryAttempts: Map<string, RetryEntry>;
97
- completed: Set<string>;
97
+ /**
98
+ * Tracks completed issue IDs mapped to the epoch-ms timestamp when the
99
+ * completion was recorded. The timestamp enables a grace-period so that
100
+ * issues manually re-activated in the roadmap can be re-dispatched, while
101
+ * still guarding against duplicate dispatch on the tick immediately after
102
+ * completion (when the tracker write-back may not have persisted yet).
103
+ */
104
+ completed: Map<string, number>;
98
105
  tokenTotals: TokenTotals;
99
106
  rateLimits: RateLimitSnapshot$1;
100
107
  /** Running count of claim rejections (another orchestrator won the race). */
package/dist/index.js CHANGED
@@ -492,7 +492,7 @@ function cloneState(state) {
492
492
  running: new Map(state.running),
493
493
  claimed: new Set(state.claimed),
494
494
  retryAttempts: new Map(state.retryAttempts),
495
- completed: new Set(state.completed),
495
+ completed: new Map(state.completed),
496
496
  tokenTotals: { ...state.tokenTotals },
497
497
  rateLimits: { ...state.rateLimits }
498
498
  };
@@ -591,13 +591,39 @@ function resolveBackend(action, hasLocalBackend) {
591
591
  }
592
592
  function pruneCompleted(next) {
593
593
  if (next.completed.size <= COMPLETED_PRUNE_THRESHOLD) return;
594
- for (const id of next.completed) {
594
+ for (const [id] of next.completed) {
595
595
  const hasPending = next.retryAttempts.has(id) || next.running.has(id) || next.claimed.has(id);
596
596
  if (!hasPending) {
597
597
  next.completed.delete(id);
598
598
  }
599
599
  }
600
600
  }
601
+ var COMPLETED_GRACE_MULTIPLIER = 2;
602
+ function reconcileCompletedAndClaimed(next, candidates, nowMs, effects) {
603
+ const gracePeriodMs = next.pollIntervalMs * COMPLETED_GRACE_MULTIPLIER;
604
+ const candidateIds = new Set(candidates.map((c) => c.id));
605
+ for (const [id, completedAtMs] of next.completed) {
606
+ if (candidateIds.has(id) && nowMs - completedAtMs > gracePeriodMs) {
607
+ next.completed.delete(id);
608
+ effects.push({
609
+ type: "emitLog",
610
+ level: "info",
611
+ message: `Released completed lock for ${id}: reappeared as active candidate after grace period`
612
+ });
613
+ }
614
+ }
615
+ for (const id of next.claimed) {
616
+ if (next.running.has(id) || next.retryAttempts.has(id)) continue;
617
+ if (!candidateIds.has(id)) {
618
+ next.claimed.delete(id);
619
+ effects.push({
620
+ type: "emitLog",
621
+ level: "info",
622
+ message: `Released orphaned claim for ${id}: no longer in active candidates`
623
+ });
624
+ }
625
+ }
626
+ }
601
627
  function handleTick(state, event, config) {
602
628
  const { candidates, runningStates, nowMs } = event;
603
629
  const next = cloneState(state);
@@ -610,6 +636,7 @@ function handleTick(state, event, config) {
610
636
  );
611
637
  effects.push(...reconcileEffects);
612
638
  applyReconcileEffects(next, reconcileEffects);
639
+ reconcileCompletedAndClaimed(next, candidates, nowMs, effects);
613
640
  const eligible = selectCandidates(
614
641
  candidates,
615
642
  next,
@@ -671,7 +698,7 @@ function handleWorkerExit(state, issueId, reason, error, attempt, config) {
671
698
  next.running.delete(issueId);
672
699
  const nowMs = Date.now();
673
700
  if (reason === "normal") {
674
- next.completed.add(issueId);
701
+ next.completed.set(issueId, nowMs);
675
702
  next.claimed.delete(issueId);
676
703
  effects.push({
677
704
  type: "cleanWorkspace",
@@ -948,7 +975,7 @@ function createEmptyState(config) {
948
975
  running: /* @__PURE__ */ new Map(),
949
976
  claimed: /* @__PURE__ */ new Set(),
950
977
  retryAttempts: /* @__PURE__ */ new Map(),
951
- completed: /* @__PURE__ */ new Set(),
978
+ completed: /* @__PURE__ */ new Map(),
952
979
  tokenTotals: {
953
980
  inputTokens: 0,
954
981
  outputTokens: 0,
@@ -1414,7 +1441,7 @@ var WorkflowLoader = class {
1414
1441
  if (parts.length < 3) {
1415
1442
  return (0, import_types3.Err)(
1416
1443
  new Error(
1417
- `Invalid WORKFLOW.md format at ${filePath}. Expected frontmatter surrounded by '---'.`
1444
+ `Invalid harness.orchestrator.md format at ${filePath}. Expected frontmatter surrounded by '---'.`
1418
1445
  )
1419
1446
  );
1420
1447
  }
@@ -1701,11 +1728,6 @@ var WorkspaceManager = class {
1701
1728
  const workspacePath = path5.resolve(this.resolvePath(identifier));
1702
1729
  try {
1703
1730
  await fs7.access(path5.join(workspacePath, ".git"));
1704
- return (0, import_types6.Ok)(workspacePath);
1705
- } catch {
1706
- }
1707
- try {
1708
- await fs7.access(workspacePath);
1709
1731
  const repoRoot2 = await this.getRepoRoot();
1710
1732
  try {
1711
1733
  await this.git(["worktree", "remove", "--force", workspacePath], repoRoot2);
@@ -1713,6 +1735,16 @@ var WorkspaceManager = class {
1713
1735
  await fs7.rm(workspacePath, { recursive: true, force: true });
1714
1736
  }
1715
1737
  } catch {
1738
+ try {
1739
+ await fs7.access(workspacePath);
1740
+ const repoRoot2 = await this.getRepoRoot();
1741
+ try {
1742
+ await this.git(["worktree", "remove", "--force", workspacePath], repoRoot2);
1743
+ } catch {
1744
+ await fs7.rm(workspacePath, { recursive: true, force: true });
1745
+ }
1746
+ } catch {
1747
+ }
1716
1748
  }
1717
1749
  const repoRoot = await this.getRepoRoot();
1718
1750
  await this.tryFetch(repoRoot);
@@ -6678,7 +6710,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
6678
6710
  claimed: new Set(this.state.claimed),
6679
6711
  running: new Map(this.state.running),
6680
6712
  retryAttempts: new Map(this.state.retryAttempts),
6681
- completed: new Set(this.state.completed),
6713
+ completed: new Map(this.state.completed),
6682
6714
  recentRequestTimestamps: [...this.state.recentRequestTimestamps],
6683
6715
  recentInputTokens: [...this.state.recentInputTokens],
6684
6716
  recentOutputTokens: [...this.state.recentOutputTokens],
@@ -6786,7 +6818,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
6786
6818
  running: Array.from(this.state.running.entries()),
6787
6819
  retryAttempts: Array.from(this.state.retryAttempts.entries()),
6788
6820
  claimed: Array.from(this.state.claimed),
6789
- completed: Array.from(this.state.completed),
6821
+ completed: Array.from(this.state.completed.keys()),
6790
6822
  tokenTotals: { ...this.state.tokenTotals, secondsRunning },
6791
6823
  maxConcurrentAgents: this.state.maxConcurrentAgents,
6792
6824
  globalCooldownUntilMs: this.state.globalCooldownUntilMs,
package/dist/index.mjs CHANGED
@@ -420,7 +420,7 @@ function cloneState(state) {
420
420
  running: new Map(state.running),
421
421
  claimed: new Set(state.claimed),
422
422
  retryAttempts: new Map(state.retryAttempts),
423
- completed: new Set(state.completed),
423
+ completed: new Map(state.completed),
424
424
  tokenTotals: { ...state.tokenTotals },
425
425
  rateLimits: { ...state.rateLimits }
426
426
  };
@@ -519,13 +519,39 @@ function resolveBackend(action, hasLocalBackend) {
519
519
  }
520
520
  function pruneCompleted(next) {
521
521
  if (next.completed.size <= COMPLETED_PRUNE_THRESHOLD) return;
522
- for (const id of next.completed) {
522
+ for (const [id] of next.completed) {
523
523
  const hasPending = next.retryAttempts.has(id) || next.running.has(id) || next.claimed.has(id);
524
524
  if (!hasPending) {
525
525
  next.completed.delete(id);
526
526
  }
527
527
  }
528
528
  }
529
+ var COMPLETED_GRACE_MULTIPLIER = 2;
530
+ function reconcileCompletedAndClaimed(next, candidates, nowMs, effects) {
531
+ const gracePeriodMs = next.pollIntervalMs * COMPLETED_GRACE_MULTIPLIER;
532
+ const candidateIds = new Set(candidates.map((c) => c.id));
533
+ for (const [id, completedAtMs] of next.completed) {
534
+ if (candidateIds.has(id) && nowMs - completedAtMs > gracePeriodMs) {
535
+ next.completed.delete(id);
536
+ effects.push({
537
+ type: "emitLog",
538
+ level: "info",
539
+ message: `Released completed lock for ${id}: reappeared as active candidate after grace period`
540
+ });
541
+ }
542
+ }
543
+ for (const id of next.claimed) {
544
+ if (next.running.has(id) || next.retryAttempts.has(id)) continue;
545
+ if (!candidateIds.has(id)) {
546
+ next.claimed.delete(id);
547
+ effects.push({
548
+ type: "emitLog",
549
+ level: "info",
550
+ message: `Released orphaned claim for ${id}: no longer in active candidates`
551
+ });
552
+ }
553
+ }
554
+ }
529
555
  function handleTick(state, event, config) {
530
556
  const { candidates, runningStates, nowMs } = event;
531
557
  const next = cloneState(state);
@@ -538,6 +564,7 @@ function handleTick(state, event, config) {
538
564
  );
539
565
  effects.push(...reconcileEffects);
540
566
  applyReconcileEffects(next, reconcileEffects);
567
+ reconcileCompletedAndClaimed(next, candidates, nowMs, effects);
541
568
  const eligible = selectCandidates(
542
569
  candidates,
543
570
  next,
@@ -599,7 +626,7 @@ function handleWorkerExit(state, issueId, reason, error, attempt, config) {
599
626
  next.running.delete(issueId);
600
627
  const nowMs = Date.now();
601
628
  if (reason === "normal") {
602
- next.completed.add(issueId);
629
+ next.completed.set(issueId, nowMs);
603
630
  next.claimed.delete(issueId);
604
631
  effects.push({
605
632
  type: "cleanWorkspace",
@@ -876,7 +903,7 @@ function createEmptyState(config) {
876
903
  running: /* @__PURE__ */ new Map(),
877
904
  claimed: /* @__PURE__ */ new Set(),
878
905
  retryAttempts: /* @__PURE__ */ new Map(),
879
- completed: /* @__PURE__ */ new Set(),
906
+ completed: /* @__PURE__ */ new Map(),
880
907
  tokenTotals: {
881
908
  inputTokens: 0,
882
909
  outputTokens: 0,
@@ -1342,7 +1369,7 @@ var WorkflowLoader = class {
1342
1369
  if (parts.length < 3) {
1343
1370
  return Err2(
1344
1371
  new Error(
1345
- `Invalid WORKFLOW.md format at ${filePath}. Expected frontmatter surrounded by '---'.`
1372
+ `Invalid harness.orchestrator.md format at ${filePath}. Expected frontmatter surrounded by '---'.`
1346
1373
  )
1347
1374
  );
1348
1375
  }
@@ -1632,11 +1659,6 @@ var WorkspaceManager = class {
1632
1659
  const workspacePath = path5.resolve(this.resolvePath(identifier));
1633
1660
  try {
1634
1661
  await fs7.access(path5.join(workspacePath, ".git"));
1635
- return Ok6(workspacePath);
1636
- } catch {
1637
- }
1638
- try {
1639
- await fs7.access(workspacePath);
1640
1662
  const repoRoot2 = await this.getRepoRoot();
1641
1663
  try {
1642
1664
  await this.git(["worktree", "remove", "--force", workspacePath], repoRoot2);
@@ -1644,6 +1666,16 @@ var WorkspaceManager = class {
1644
1666
  await fs7.rm(workspacePath, { recursive: true, force: true });
1645
1667
  }
1646
1668
  } catch {
1669
+ try {
1670
+ await fs7.access(workspacePath);
1671
+ const repoRoot2 = await this.getRepoRoot();
1672
+ try {
1673
+ await this.git(["worktree", "remove", "--force", workspacePath], repoRoot2);
1674
+ } catch {
1675
+ await fs7.rm(workspacePath, { recursive: true, force: true });
1676
+ }
1677
+ } catch {
1678
+ }
1647
1679
  }
1648
1680
  const repoRoot = await this.getRepoRoot();
1649
1681
  await this.tryFetch(repoRoot);
@@ -6647,7 +6679,7 @@ var Orchestrator = class extends EventEmitter {
6647
6679
  claimed: new Set(this.state.claimed),
6648
6680
  running: new Map(this.state.running),
6649
6681
  retryAttempts: new Map(this.state.retryAttempts),
6650
- completed: new Set(this.state.completed),
6682
+ completed: new Map(this.state.completed),
6651
6683
  recentRequestTimestamps: [...this.state.recentRequestTimestamps],
6652
6684
  recentInputTokens: [...this.state.recentInputTokens],
6653
6685
  recentOutputTokens: [...this.state.recentOutputTokens],
@@ -6755,7 +6787,7 @@ var Orchestrator = class extends EventEmitter {
6755
6787
  running: Array.from(this.state.running.entries()),
6756
6788
  retryAttempts: Array.from(this.state.retryAttempts.entries()),
6757
6789
  claimed: Array.from(this.state.claimed),
6758
- completed: Array.from(this.state.completed),
6790
+ completed: Array.from(this.state.completed.keys()),
6759
6791
  tokenTotals: { ...this.state.tokenTotals, secondsRunning },
6760
6792
  maxConcurrentAgents: this.state.maxConcurrentAgents,
6761
6793
  globalCooldownUntilMs: this.state.globalCooldownUntilMs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-engineering/orchestrator",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "Orchestrator daemon for dispatching coding agents to issues",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",