@groupchatai/claude-runner 0.3.1 → 0.4.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 (2) hide show
  1. package/dist/index.js +88 -20
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -445,8 +445,10 @@ function runShellCommand(cmd, args, cwd) {
445
445
  });
446
446
  }
447
447
  var sessionCache = /* @__PURE__ */ new Map();
448
+ var runCounter = 0;
448
449
  async function processRun(client, run, config) {
449
- const runTag = ` ${C.pid}[${run.id.slice(-8)}]${C.reset}`;
450
+ const runNum = ++runCounter;
451
+ const runTag = ` ${C.pid}[${runNum}]${C.reset}`;
450
452
  const log = (msg) => console.log(`${runTag} ${msg}`);
451
453
  const logGreen = (msg) => console.log(`${runTag} ${C.green}${msg}${C.reset}`);
452
454
  const detail = await client.getRunDetail(run.id);
@@ -638,23 +640,89 @@ function parseArgs() {
638
640
  async function sleep(ms) {
639
641
  return new Promise((resolve) => setTimeout(resolve, ms));
640
642
  }
641
- function handlePendingRuns(runs, activeRuns, client, config) {
643
+ var TaskScheduler = class {
644
+ slots = /* @__PURE__ */ new Map();
645
+ processing = /* @__PURE__ */ new Set();
646
+ get activeTaskCount() {
647
+ return this.slots.size;
648
+ }
649
+ isRunActive(runId) {
650
+ return this.processing.has(runId);
651
+ }
652
+ hasActiveTask(taskId) {
653
+ return this.slots.has(taskId);
654
+ }
655
+ tryStart(run, maxConcurrent) {
656
+ if (this.processing.has(run.id)) return "at_limit";
657
+ const existingSlot = this.slots.get(run.taskId);
658
+ if (existingSlot) {
659
+ if (!existingSlot.queue.some((q) => q.id === run.id)) {
660
+ existingSlot.queue.push(run);
661
+ }
662
+ return "queued";
663
+ }
664
+ if (this.slots.size >= maxConcurrent) {
665
+ return "at_limit";
666
+ }
667
+ this.slots.set(run.taskId, { activeRunId: run.id, queue: [] });
668
+ this.processing.add(run.id);
669
+ return "start";
670
+ }
671
+ complete(run) {
672
+ this.processing.delete(run.id);
673
+ const slot = this.slots.get(run.taskId);
674
+ if (!slot) return void 0;
675
+ const next = slot.queue.shift();
676
+ if (next) {
677
+ slot.activeRunId = next.id;
678
+ this.processing.add(next.id);
679
+ return next;
680
+ }
681
+ this.slots.delete(run.taskId);
682
+ return void 0;
683
+ }
684
+ isEmpty() {
685
+ return this.slots.size === 0;
686
+ }
687
+ };
688
+ function handlePendingRuns(runs, scheduler, client, config) {
642
689
  if (runs.length > 0 && config.verbose) {
643
690
  console.log(`\u{1F4EC} ${runs.length} pending run(s)`);
644
691
  }
645
692
  for (const run of runs) {
646
- if (activeRuns.has(run.id)) continue;
647
- if (activeRuns.size >= config.maxConcurrent) {
693
+ const result = scheduler.tryStart(run, config.maxConcurrent);
694
+ if (result === "at_limit") {
648
695
  if (config.verbose) {
649
- console.log(`\u23F8 At concurrency limit (${config.maxConcurrent}), waiting\u2026`);
696
+ console.log(`\u23F8 At concurrency limit (${config.maxConcurrent} tasks), waiting\u2026`);
650
697
  }
651
698
  break;
652
699
  }
653
- activeRuns.add(run.id);
654
- processRun(client, run, config).catch((err) => console.error(`Unhandled error processing run ${run.id}:`, err)).finally(() => activeRuns.delete(run.id));
700
+ if (result === "queued") {
701
+ if (config.verbose) {
702
+ console.log(` ${C.dim}Queued follow-up for active task${C.reset}`);
703
+ }
704
+ continue;
705
+ }
706
+ processRunWithDrain(client, run, scheduler, config).catch(
707
+ (err) => console.error(`Unhandled error processing run ${run.id}:`, err)
708
+ );
709
+ }
710
+ }
711
+ async function processRunWithDrain(client, run, scheduler, config) {
712
+ let current = run;
713
+ while (current) {
714
+ try {
715
+ await processRun(client, current, config);
716
+ } catch (err) {
717
+ console.error(`Unhandled error processing run ${current.id}:`, err);
718
+ }
719
+ current = scheduler.complete(current);
720
+ if (current) {
721
+ console.log(` \u23ED Processing queued follow-up\u2026`);
722
+ }
655
723
  }
656
724
  }
657
- async function runWithWebSocket(client, config, activeRuns) {
725
+ async function runWithWebSocket(client, config, scheduler) {
658
726
  let ConvexClient;
659
727
  let anyApi;
660
728
  try {
@@ -663,7 +731,7 @@ async function runWithWebSocket(client, config, activeRuns) {
663
731
  } catch {
664
732
  console.warn("\u26A0 convex package not found \u2014 falling back to HTTP polling.");
665
733
  console.warn(" Install convex for WebSocket mode: npm i convex\n");
666
- await runWithPolling(client, config, activeRuns);
734
+ await runWithPolling(client, config, scheduler);
667
735
  return;
668
736
  }
669
737
  const convex = new ConvexClient(config.convexUrl);
@@ -673,7 +741,7 @@ async function runWithWebSocket(client, config, activeRuns) {
673
741
  { token: config.token },
674
742
  (runs) => {
675
743
  if (!runs || runs.length === 0) return;
676
- handlePendingRuns(runs, activeRuns, client, config);
744
+ handlePendingRuns(runs, scheduler, client, config);
677
745
  }
678
746
  );
679
747
  await new Promise((resolve) => {
@@ -686,7 +754,7 @@ async function runWithWebSocket(client, config, activeRuns) {
686
754
  process.on("SIGTERM", shutdown);
687
755
  });
688
756
  }
689
- async function runWithPolling(client, config, activeRuns) {
757
+ async function runWithPolling(client, config, scheduler) {
690
758
  console.log(`\u{1F4E1} Polling every ${config.pollInterval}ms \u2014 listening for tasks\u2026
691
759
  `);
692
760
  let running = true;
@@ -699,17 +767,17 @@ async function runWithPolling(client, config, activeRuns) {
699
767
  if (config.once) {
700
768
  try {
701
769
  const pending = await client.listPendingRuns();
702
- handlePendingRuns(pending, activeRuns, client, config);
770
+ handlePendingRuns(pending, scheduler, client, config);
703
771
  } catch (err) {
704
772
  console.error(`Poll error: ${err instanceof Error ? err.message : err}`);
705
773
  }
706
- while (activeRuns.size > 0) await sleep(1e3);
774
+ while (!scheduler.isEmpty()) await sleep(1e3);
707
775
  return;
708
776
  }
709
777
  while (running) {
710
778
  try {
711
779
  const pending = await client.listPendingRuns();
712
- handlePendingRuns(pending, activeRuns, client, config);
780
+ handlePendingRuns(pending, scheduler, client, config);
713
781
  } catch (err) {
714
782
  const msg = err instanceof Error ? err.message : String(err);
715
783
  if (config.verbose || !msg.includes("fetch")) {
@@ -738,15 +806,15 @@ async function main() {
738
806
  if (config.model) console.log(` Model: ${config.model}`);
739
807
  if (config.dryRun) console.log(` Mode: DRY RUN`);
740
808
  console.log();
741
- const activeRuns = /* @__PURE__ */ new Set();
809
+ const scheduler = new TaskScheduler();
742
810
  if (config.poll || config.once) {
743
- await runWithPolling(client, config, activeRuns);
811
+ await runWithPolling(client, config, scheduler);
744
812
  } else {
745
- await runWithWebSocket(client, config, activeRuns);
813
+ await runWithWebSocket(client, config, scheduler);
746
814
  }
747
- if (activeRuns.size > 0) {
748
- console.log(`\u23F3 Waiting for ${activeRuns.size} in-flight run(s)\u2026`);
749
- while (activeRuns.size > 0) {
815
+ if (!scheduler.isEmpty()) {
816
+ console.log(`\u23F3 Waiting for ${scheduler.activeTaskCount} in-flight task(s)\u2026`);
817
+ while (!scheduler.isEmpty()) {
750
818
  await sleep(1e3);
751
819
  }
752
820
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groupchatai/claude-runner",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Run GroupChat AI agent tasks locally with Claude Code",
5
5
  "type": "module",
6
6
  "bin": {