@youngjurry/pi-agents 0.7.1 → 0.7.2

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 (3) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/control.ts +35 -13
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.2 - 2026-09-02
4
+
5
+ - Keep execution slots reserved until an `AgentSession` is fully settled and safe to evict.
6
+ - Treat temporary resident saturation as queue backpressure instead of permanently failing waiting agents.
7
+ - Resume blocked FIFO scheduling on settlement without busy-looping or losing wake-up signals.
8
+ - Add regression coverage for the batch overflow race that previously produced `agent residency limit reached` errors.
9
+
3
10
  ## 0.7.1 - 2026-09-01
4
11
 
5
12
  - Order the `/agents` picker by latest task assignment, newest first, instead of alphabetically.
package/control.ts CHANGED
@@ -163,6 +163,7 @@ export class AgentControl {
163
163
  private modelRuntimePromise?: Promise<ModelRuntime>;
164
164
  private activeExecutionSlots = 0;
165
165
  private schedulerPromise?: Promise<void>;
166
+ private schedulerRerunRequested = false;
166
167
  private spawnOperationTail: Promise<void> = Promise.resolve();
167
168
  private disposed = false;
168
169
  private shuttingDown = false;
@@ -603,15 +604,22 @@ export class AgentControl {
603
604
  return loader;
604
605
  }
605
606
 
606
- private async evictForResidency(protectedPath?: string): Promise<void> {
607
+ private tryEvictForResidency(protectedPath?: string): boolean {
607
608
  const allResidents = [...this.agentsByPath.values()].filter((record) => record.loaded);
608
- if (allResidents.length < this.maxResidentSubagents) return;
609
+ if (allResidents.length < this.maxResidentSubagents) return true;
609
610
  const candidate = allResidents
610
611
  .filter((record) => record.path !== protectedPath)
611
612
  .filter((record) => record.status !== "running" && !record.holdsExecutionSlot && record.session?.isIdle !== false)
612
613
  .sort((left, right) => left.lastUsedAt - right.lastUsedAt)[0];
613
- if (!candidate) throw new Error(`agent residency limit reached (${this.maxResidentSubagents}); all resident agents are busy`);
614
+ if (!candidate) return false;
614
615
  this.unload(candidate);
616
+ return true;
617
+ }
618
+
619
+ private async evictForResidency(protectedPath?: string): Promise<void> {
620
+ if (!this.tryEvictForResidency(protectedPath)) {
621
+ throw new Error(`agent residency limit reached (${this.maxResidentSubagents}); all resident agents are busy`);
622
+ }
615
623
  }
616
624
 
617
625
  private unload(record: AgentRecord): void {
@@ -656,6 +664,13 @@ export class AgentControl {
656
664
  this.noteTurnEnd(record.path, lastAssistant?.role === "assistant" ? lastAssistant.stopReason : undefined);
657
665
  }
658
666
  }
667
+ if (event.type === "agent_settled") {
668
+ this.releaseExecutionSlot(record);
669
+ this.persistState();
670
+ // Let AgentSession finish emitting its settled event before an LRU eviction
671
+ // can dispose this now-idle session.
672
+ queueMicrotask(() => void this.scheduleQueued());
673
+ }
659
674
  this.changed();
660
675
  });
661
676
  }
@@ -686,10 +701,8 @@ export class AgentControl {
686
701
  if (answer.aborted) {
687
702
  record.status = "interrupted";
688
703
  record.statusMessage = "interrupted";
689
- this.releaseExecutionSlot(record);
690
704
  this.persistState();
691
705
  this.changed();
692
- void this.scheduleQueued();
693
706
  return;
694
707
  }
695
708
  if (answer.error) {
@@ -702,10 +715,8 @@ export class AgentControl {
702
715
  record.statusMessage = undefined;
703
716
  }
704
717
  this.writeAgentResult(record);
705
- this.releaseExecutionSlot(record);
706
718
  this.persistState();
707
719
  this.changed();
708
- void this.scheduleQueued();
709
720
  void this.deliver(record.path, record.parentPath, this.completionNotice(record), false, "AGENT_STATUS").catch(() => {});
710
721
  }
711
722
 
@@ -862,8 +873,11 @@ export class AgentControl {
862
873
  return records;
863
874
  }
864
875
 
865
- private async startQueued(record: AgentRecord): Promise<void> {
866
- if (record.status !== "queued") return;
876
+ private async startQueued(record: AgentRecord): Promise<boolean> {
877
+ if (record.status !== "queued") return true;
878
+ // A completed AgentSession emits agent_end just before it becomes idle. Treat a
879
+ // temporarily full resident set as backpressure, not as a permanent task error.
880
+ if (!record.session && !this.tryEvictForResidency(record.path)) return false;
867
881
  this.reserveExecutionSlot();
868
882
  record.holdsExecutionSlot = true;
869
883
  record.status = "pending_init";
@@ -877,7 +891,7 @@ export class AgentControl {
877
891
  record.statusMessage = "waiting for an execution slot";
878
892
  this.releaseExecutionSlot(record);
879
893
  this.unload(record);
880
- return;
894
+ return true;
881
895
  }
882
896
  const content = [...(record.queuedMail ?? []), record.queuedMessage].filter((item): item is string => Boolean(item)).join("\n\n");
883
897
  if ((record.queuedMail?.length ?? 0) > 0) this.mailboxPending.set(record.path, 0);
@@ -885,6 +899,7 @@ export class AgentControl {
885
899
  record.queuedMail = undefined;
886
900
  this.launch(record, content);
887
901
  this.persistState();
902
+ return true;
888
903
  } catch (error) {
889
904
  record.status = "errored";
890
905
  record.statusMessage = error instanceof Error ? error.message : String(error);
@@ -896,6 +911,7 @@ export class AgentControl {
896
911
  this.writeAgentResult(record);
897
912
  this.persistState();
898
913
  void this.deliver(record.path, record.parentPath, this.completionNotice(record), false, "AGENT_STATUS").catch(() => {});
914
+ return true;
899
915
  }
900
916
  }
901
917
 
@@ -903,20 +919,26 @@ export class AgentControl {
903
919
  while (!this.disposed && !this.shuttingDown && this.activeExecutionSlots < this.maxConcurrentSubagents) {
904
920
  const next = this.queuedRecords()[0];
905
921
  if (!next) break;
906
- await this.startQueued(next);
922
+ if (!(await this.startQueued(next))) break;
907
923
  }
908
924
  }
909
925
 
910
926
  private async scheduleQueued(): Promise<void> {
911
927
  if (this.disposed || this.shuttingDown) return;
912
- if (this.schedulerPromise) return this.schedulerPromise;
928
+ if (this.schedulerPromise) {
929
+ this.schedulerRerunRequested = true;
930
+ return this.schedulerPromise;
931
+ }
932
+ this.schedulerRerunRequested = false;
913
933
  const operation = this.runQueuedScheduler();
914
934
  this.schedulerPromise = operation;
915
935
  try {
916
936
  await operation;
917
937
  } finally {
938
+ const rerun = this.schedulerRerunRequested;
939
+ this.schedulerRerunRequested = false;
918
940
  if (this.schedulerPromise === operation) this.schedulerPromise = undefined;
919
- if (!this.disposed && !this.shuttingDown && this.activeExecutionSlots < this.maxConcurrentSubagents && this.queuedRecords().length > 0) {
941
+ if (rerun && !this.disposed && !this.shuttingDown) {
920
942
  queueMicrotask(() => void this.scheduleQueued());
921
943
  }
922
944
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youngjurry/pi-agents",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "Persistent in-process Codex-style multi-agent collaboration for Pi",
5
5
  "author": "youngjurry",
6
6
  "type": "module",