feat(FN-978): add diagnostic logging, semaphore resilience, and executor tests

- Add structured diagnostic logging to executor, stuck-task-detector, and pi.ts with subsystem prefixes
- Add defensive guards to AgentSemaphore (limit minimum 1, invalid limit handling)
- Add comprehensive integration tests for agent execution flow (executor.test.ts)
- Add unit tests for semaphore resilience (concurrency.test.ts) and stuck-task-detector (stuck-task-detector.test.ts)
- Fix TypeScript errors in test task objects and duplicate execution test
- Document engine diagnostic logging points in AGENTS.md
This commit is contained in:
gsxdsm
2026-04-05 13:37:21 -07:00
parent 64dbbdd5ba
commit 856ceebe88
9 changed files with 586 additions and 7 deletions

View File

@@ -61,14 +61,20 @@ export class AgentSemaphore {
}
/** Number of slots available for immediate acquisition. May be 0 or negative
* if the limit was reduced below the current active count. */
* if the limit was reduced below the current active count.
* Returns 0 when the limit is not a valid positive number (defensive guard). */
get availableCount(): number {
return Math.max(0, this._getLimit() - this._active);
const limit = this._getLimit();
if (!Number.isFinite(limit) || limit <= 0) return 0;
return Math.max(0, limit - this._active);
}
/** Current concurrency limit. */
/** Current concurrency limit.
* Returns a minimum of 1 to prevent indefinite blocking. */
get limit(): number {
return this._getLimit();
const limit = this._getLimit();
if (!Number.isFinite(limit) || limit <= 0) return 1;
return limit;
}
/**
@@ -83,7 +89,8 @@ export class AgentSemaphore {
* agents and {@link PRIORITY_EXECUTE} (`1`) for execution agents.
*/
acquire(priority: number = 0): Promise<void> {
if (this._active < this._getLimit()) {
const limit = this.limit; // Uses the guarded getter (returns min 1)
if (this._active < limit) {
this._active++;
return Promise.resolve();
}
@@ -131,7 +138,8 @@ export class AgentSemaphore {
* priority, the one that was enqueued first (FIFO) is chosen.
*/
private _drain(): void {
while (this._waiters.length > 0 && this._active < this._getLimit()) {
const limit = this.limit; // Uses the guarded getter (returns min 1)
while (this._waiters.length > 0 && this._active < limit) {
const idx = this._highestPriorityIndex();
const [waiter] = this._waiters.splice(idx, 1);
waiter.resolve();