@oh-my-pi/pi-agent-core 16.1.16 → 16.1.17
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/CHANGELOG.md +6 -0
- package/dist/types/utils/yield.d.ts +22 -3
- package/package.json +7 -7
- package/src/utils/yield.ts +52 -15
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.1.17] - 2026-06-24
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Hardened the agent-loop cooperative yield against backward wall-clock jumps. A stale future timestamp left in the shared yield gate (NTP step, or a fake-timer test mocking `Date.now`) could make `yieldIfDue()` gate forever and stop yielding to the event loop; the gate now treats a backward clock delta as due and re-anchors. The gate is exposed as an injectable `YieldGate` (with `yieldIfDue()` retained as the shared singleton) so it can be exercised without mocking process-global timers.
|
|
10
|
+
|
|
5
11
|
## [16.1.16] - 2026-06-23
|
|
6
12
|
|
|
7
13
|
### Added
|
|
@@ -29,9 +29,28 @@ export declare class EventLoopKeepalive {
|
|
|
29
29
|
[Symbol.dispose](): void;
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
33
|
-
* once every {@link
|
|
34
|
-
*
|
|
32
|
+
* Cooperative yield gate. Sleeps for at least {@link YieldGateOptions.sleepMs}
|
|
33
|
+
* but at most once every {@link YieldGateOptions.intervalMs}; hot-path callers
|
|
34
|
+
* invoke it freely and only the slow path actually sleeps.
|
|
35
|
+
*
|
|
36
|
+
* The clock and sleep are injectable so tests drive the gate logic without
|
|
37
|
+
* touching process-global `Date.now`/`scheduler.wait` — globals a concurrent
|
|
38
|
+
* test file can restore mid-run, which previously made the shared gate flake.
|
|
39
|
+
*/
|
|
40
|
+
export interface YieldGateOptions {
|
|
41
|
+
now?: () => number;
|
|
42
|
+
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
43
|
+
intervalMs?: number;
|
|
44
|
+
sleepMs?: number;
|
|
45
|
+
}
|
|
46
|
+
export declare class YieldGate {
|
|
47
|
+
#private;
|
|
48
|
+
constructor(opts?: YieldGateOptions);
|
|
49
|
+
yieldIfDue(signal?: AbortSignal): Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Yield to the Bun event loop, sleeping for at least 20 ms — but at most once
|
|
53
|
+
* every {@link YIELD_INTERVAL_MS} across all callers.
|
|
35
54
|
*/
|
|
36
55
|
export declare function yieldIfDue(): Promise<void>;
|
|
37
56
|
export declare class ExponentialYield {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-agent-core",
|
|
4
|
-
"version": "16.1.
|
|
4
|
+
"version": "16.1.17",
|
|
5
5
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -35,12 +35,12 @@
|
|
|
35
35
|
"fmt": "biome format --write ."
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@oh-my-pi/pi-ai": "16.1.
|
|
39
|
-
"@oh-my-pi/pi-catalog": "16.1.
|
|
40
|
-
"@oh-my-pi/pi-natives": "16.1.
|
|
41
|
-
"@oh-my-pi/pi-utils": "16.1.
|
|
42
|
-
"@oh-my-pi/pi-wire": "16.1.
|
|
43
|
-
"@oh-my-pi/snapcompact": "16.1.
|
|
38
|
+
"@oh-my-pi/pi-ai": "16.1.17",
|
|
39
|
+
"@oh-my-pi/pi-catalog": "16.1.17",
|
|
40
|
+
"@oh-my-pi/pi-natives": "16.1.17",
|
|
41
|
+
"@oh-my-pi/pi-utils": "16.1.17",
|
|
42
|
+
"@oh-my-pi/pi-wire": "16.1.17",
|
|
43
|
+
"@oh-my-pi/snapcompact": "16.1.17",
|
|
44
44
|
"@opentelemetry/api": "^1.9.1"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
package/src/utils/yield.ts
CHANGED
|
@@ -45,13 +45,6 @@ export class EventLoopKeepalive {
|
|
|
45
45
|
const YIELD_SLEEP_MS = 20;
|
|
46
46
|
const YIELD_INTERVAL_MS = 50;
|
|
47
47
|
|
|
48
|
-
/**
|
|
49
|
-
* Wall-clock timestamp of the last completed yield. Module-level so that
|
|
50
|
-
* tight loops sharing this helper collectively respect the gate, not just
|
|
51
|
-
* one caller at a time.
|
|
52
|
-
*/
|
|
53
|
-
let lastYieldAt = 0;
|
|
54
|
-
|
|
55
48
|
/**
|
|
56
49
|
* Sleep for at least `ms` milliseconds of wall-clock time.
|
|
57
50
|
* Retries the wait if it returns prematurely (which can happen when napi
|
|
@@ -76,15 +69,59 @@ async function sleepAtLeast(ms: number, signal?: AbortSignal): Promise<void> {
|
|
|
76
69
|
}
|
|
77
70
|
|
|
78
71
|
/**
|
|
79
|
-
*
|
|
80
|
-
* once every {@link
|
|
81
|
-
*
|
|
72
|
+
* Cooperative yield gate. Sleeps for at least {@link YieldGateOptions.sleepMs}
|
|
73
|
+
* but at most once every {@link YieldGateOptions.intervalMs}; hot-path callers
|
|
74
|
+
* invoke it freely and only the slow path actually sleeps.
|
|
75
|
+
*
|
|
76
|
+
* The clock and sleep are injectable so tests drive the gate logic without
|
|
77
|
+
* touching process-global `Date.now`/`scheduler.wait` — globals a concurrent
|
|
78
|
+
* test file can restore mid-run, which previously made the shared gate flake.
|
|
79
|
+
*/
|
|
80
|
+
export interface YieldGateOptions {
|
|
81
|
+
now?: () => number;
|
|
82
|
+
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
83
|
+
intervalMs?: number;
|
|
84
|
+
sleepMs?: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export class YieldGate {
|
|
88
|
+
#lastYieldAt = 0;
|
|
89
|
+
readonly #now: () => number;
|
|
90
|
+
readonly #sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
91
|
+
readonly #intervalMs: number;
|
|
92
|
+
readonly #sleepMs: number;
|
|
93
|
+
|
|
94
|
+
constructor(opts: YieldGateOptions = {}) {
|
|
95
|
+
this.#now = opts.now ?? (() => Date.now());
|
|
96
|
+
this.#sleep = opts.sleep ?? sleepAtLeast;
|
|
97
|
+
this.#intervalMs = opts.intervalMs ?? YIELD_INTERVAL_MS;
|
|
98
|
+
this.#sleepMs = opts.sleepMs ?? YIELD_SLEEP_MS;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async yieldIfDue(signal?: AbortSignal): Promise<void> {
|
|
102
|
+
const now = this.#now();
|
|
103
|
+
const elapsed = now - this.#lastYieldAt;
|
|
104
|
+
// `elapsed < 0` means the wall clock moved backward relative to the last
|
|
105
|
+
// yield (NTP step, fake-timer test, or a stale future timestamp left by
|
|
106
|
+
// another caller): treat it as due and re-anchor rather than gate forever.
|
|
107
|
+
if (elapsed >= 0 && elapsed < this.#intervalMs) return;
|
|
108
|
+
await this.#sleep(this.#sleepMs, signal);
|
|
109
|
+
this.#lastYieldAt = this.#now();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Process-wide gate shared by all hot-path callers so tight loops collectively
|
|
115
|
+
* respect the interval rather than each sleeping independently.
|
|
116
|
+
*/
|
|
117
|
+
const sharedYieldGate = new YieldGate();
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Yield to the Bun event loop, sleeping for at least 20 ms — but at most once
|
|
121
|
+
* every {@link YIELD_INTERVAL_MS} across all callers.
|
|
82
122
|
*/
|
|
83
|
-
export
|
|
84
|
-
|
|
85
|
-
if (now - lastYieldAt < YIELD_INTERVAL_MS) return;
|
|
86
|
-
await sleepAtLeast(YIELD_SLEEP_MS);
|
|
87
|
-
lastYieldAt = Date.now();
|
|
123
|
+
export function yieldIfDue(): Promise<void> {
|
|
124
|
+
return sharedYieldGate.yieldIfDue();
|
|
88
125
|
}
|
|
89
126
|
|
|
90
127
|
// ---------------------------------------------------------------------------
|