pi-harness-runtime 1.1.459 → 1.1.460
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 +38 -0
- package/harness/pending-countdown.ts +348 -0
- package/index.ts +216 -28
- package/package.json +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/logging/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/relay/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/tooling-preference/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/two-phase-review/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/write-review/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,44 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [1.1.460](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v1.1.448...v1.1.460) (2026-10-28)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **delegation:** a child is only a child when it really ran ([ac4e678](https://github.com/ManotLuijiu/pi-harness-runtime/commit/ac4e6782d7d4bc4941e458425c8a9f38a51fc9d4))
|
|
11
|
+
* **delegation:** the scope of a request is measured before it is judged ([684b6a5](https://github.com/ManotLuijiu/pi-harness-runtime/commit/684b6a55184f63d3be24c48dcecfb0b13410dc15))
|
|
12
|
+
* **honcho:** the memory service speaks the native MCP of Pi ([f9f740c](https://github.com/ManotLuijiu/pi-harness-runtime/commit/f9f740c12869bae4c4380afbe8c76e12388bb744))
|
|
13
|
+
* **jev:** the pending question carries the ask and the answer ([a792133](https://github.com/ManotLuijiu/pi-harness-runtime/commit/a792133e517fb8c12d3363e98942f3af30270f27))
|
|
14
|
+
* **jev:** the pending question counts down to the decision ([d0cc171](https://github.com/ManotLuijiu/pi-harness-runtime/commit/d0cc171223986b2d32c670b6c1c74712fe0a4ad8))
|
|
15
|
+
* **keys:** the key files are repaired when their permissions are unsafe ([e9c2c26](https://github.com/ManotLuijiu/pi-harness-runtime/commit/e9c2c26b287665b56ed749de269737e8403bb8af))
|
|
16
|
+
* **merge:** a manual run can leave the merge to the pull request event ([b3d1950](https://github.com/ManotLuijiu/pi-harness-runtime/commit/b3d1950b5941c78ccd56951acdafd1d4afbe1bff))
|
|
17
|
+
* **phase:** the agent owns the todo list and the harness says so ([c489370](https://github.com/ManotLuijiu/pi-harness-runtime/commit/c489370bd7926aece8cb1a77aa20ca2d2672f1fb))
|
|
18
|
+
* **review:** the review starts on its own when the implementation is done ([04f51f5](https://github.com/ManotLuijiu/pi-harness-runtime/commit/04f51f599cd3222b6c74dd8fadb1373bd72d9bd7))
|
|
19
|
+
* **status:** the status line names the phase, the progress and the children ([0167bf3](https://github.com/ManotLuijiu/pi-harness-runtime/commit/0167bf396a0b987c1d44f3b869351976a951dd65))
|
|
20
|
+
* **test:** the task waits for the final tier of the validation plan ([fa147db](https://github.com/ManotLuijiu/pi-harness-runtime/commit/fa147dbf7d50d56e6ea48c6d8fbb4fcb190caf8a))
|
|
21
|
+
* **validation:** a large job starts with a measured pilot ([425964b](https://github.com/ManotLuijiu/pi-harness-runtime/commit/425964b04a3d4fa02adfabed47ff62c5ec2b9c28))
|
|
22
|
+
* **validation:** the size of a job is a fact, not a phrase ([8aa6c11](https://github.com/ManotLuijiu/pi-harness-runtime/commit/8aa6c119565d0f8412dc8aeafa6b295d47d3f093))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Bug Fixes
|
|
26
|
+
|
|
27
|
+
* **ci:** every run of a required check counts, and the worst one decides ([da9e0da](https://github.com/ManotLuijiu/pi-harness-runtime/commit/da9e0da6b0ca29cb320764fd5fc1b8a8dee8b1e0))
|
|
28
|
+
* **ci:** the newest attempt of a run decides, not the attempt that failed ([9d7adcc](https://github.com/ManotLuijiu/pi-harness-runtime/commit/9d7adcc31cbacc517c3c1a9bcd3956be94c908ef))
|
|
29
|
+
* **hooks:** a tag is checked through the commit behind it ([c823e8e](https://github.com/ManotLuijiu/pi-harness-runtime/commit/c823e8e1f6973c62b5449a97c101759a4d5cbba5))
|
|
30
|
+
* **jev:** the alternatives of a question are parsed without a quadratic pattern ([63016a3](https://github.com/ManotLuijiu/pi-harness-runtime/commit/63016a3f0488acb801990dd46770c00d382b5858))
|
|
31
|
+
* **keys:** the key directory is audited as a tree ([081cc75](https://github.com/ManotLuijiu/pi-harness-runtime/commit/081cc75ac8ceff36b0d619c3f76371dc08459d3c))
|
|
32
|
+
* **merge:** the boolean input is read as a boolean ([971b716](https://github.com/ManotLuijiu/pi-harness-runtime/commit/971b7169a43db382dbdeb6bb9bab02ab56e0351c))
|
|
33
|
+
* **merge:** the manual job needs the right of the merge ([7ba685e](https://github.com/ManotLuijiu/pi-harness-runtime/commit/7ba685ec43d815f90aa35b742579e87e76c69209))
|
|
34
|
+
* **merge:** the merge of the pull request is the GraphQL mutation ([972bab4](https://github.com/ManotLuijiu/pi-harness-runtime/commit/972bab4b0f7faf1b0047f3bb767ef6db3de4b2fa))
|
|
35
|
+
* **merge:** the workflow merges the pull request the gates cleared ([d7639ec](https://github.com/ManotLuijiu/pi-harness-runtime/commit/d7639ec9b83723510acb4a5c4e23d5d6167c3af6))
|
|
36
|
+
* **release:** a manual release keeps the version of its manifest ([122e661](https://github.com/ManotLuijiu/pi-harness-runtime/commit/122e661ea48cb8aff14c71dbd0221a4cf0ba4c8e))
|
|
37
|
+
* **release:** the alignment names the reason of a failure ([5814dd0](https://github.com/ManotLuijiu/pi-harness-runtime/commit/5814dd0b780f5839cf8011ed0cbbbb31e7eb0418))
|
|
38
|
+
* **release:** the channels of the package name the release ([a7a273f](https://github.com/ManotLuijiu/pi-harness-runtime/commit/a7a273ffe8398e2b5b07ef1c321777de41792c6e))
|
|
39
|
+
* **release:** the release publishes and leaves the channels alone ([5871746](https://github.com/ManotLuijiu/pi-harness-runtime/commit/5871746589c1281d855dbeb591b02419fa43f22e))
|
|
40
|
+
* **release:** the release pull request names the version of its branch ([a846e83](https://github.com/ManotLuijiu/pi-harness-runtime/commit/a846e83e6de6e529f4bca05e869db0b5eebb37c5))
|
|
41
|
+
* **release:** the release scans the tree of the commit it publishes ([7d47a0b](https://github.com/ManotLuijiu/pi-harness-runtime/commit/7d47a0b5406480a1e1d3a52623a76aabd1e2d0ba))
|
|
42
|
+
|
|
5
43
|
### [1.1.459](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v1.1.448...v1.1.459) (2026-10-28)
|
|
6
44
|
|
|
7
45
|
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live countdown of a pending question.
|
|
3
|
+
*
|
|
4
|
+
* The harness asks a human a question and waits. The wait has a deadline,
|
|
5
|
+
* and the countdown shows the remaining time in the status row of the TUI
|
|
6
|
+
* and starts the decision of the judge exactly at that deadline.
|
|
7
|
+
*
|
|
8
|
+
* The module owns the two policies of the wait:
|
|
9
|
+
*
|
|
10
|
+
* - the first countdown counts down to the deadline of the decision, which
|
|
11
|
+
* is derived from the moment the wait started,
|
|
12
|
+
* - the recheck counts down to the next attempt, after the judge asked to
|
|
13
|
+
* wait longer.
|
|
14
|
+
*
|
|
15
|
+
* The caller cannot pass a deadline of its own: the displayed deadline and
|
|
16
|
+
* the deadline the judge uses therefore cannot drift apart.
|
|
17
|
+
*
|
|
18
|
+
* The countdown is bound to one request. Every start replaces the previous
|
|
19
|
+
* binding and every stop releases the timer, so there is never more than
|
|
20
|
+
* one countdown and never a timer without a request.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** What the countdown counts to. */
|
|
24
|
+
export type CountdownMode = "initial" | "recheck";
|
|
25
|
+
|
|
26
|
+
/** The state of the countdown that is drawn in the status row. */
|
|
27
|
+
export interface CountdownView {
|
|
28
|
+
/** The request the countdown belongs to. */
|
|
29
|
+
requestId: string;
|
|
30
|
+
/** The milliseconds that are left. Never negative. */
|
|
31
|
+
remainingMs: number;
|
|
32
|
+
/** The remaining time as "MM:SS". */
|
|
33
|
+
remaining: string;
|
|
34
|
+
/** What the countdown counts to. */
|
|
35
|
+
mode: CountdownMode;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The dependencies of a countdown. */
|
|
39
|
+
export interface PendingCountdownDeps {
|
|
40
|
+
/** Draw the countdown in the status row. */
|
|
41
|
+
render: (view: CountdownView) => void;
|
|
42
|
+
/** The deadline was reached and the decision is due. */
|
|
43
|
+
onDeadline: (requestId: string) => void;
|
|
44
|
+
/**
|
|
45
|
+
* Is the request still the pending one?
|
|
46
|
+
*
|
|
47
|
+
* A tick of a question that was answered or replaced in the meantime
|
|
48
|
+
* must not redraw its status and must not trigger a decision.
|
|
49
|
+
*/
|
|
50
|
+
isPending: (requestId: string) => boolean;
|
|
51
|
+
/** A failing callback of the countdown is reported here. */
|
|
52
|
+
onError?: (error: unknown) => void;
|
|
53
|
+
/** The wait is over: the status row is cleared. */
|
|
54
|
+
onStop?: (requestId: string | null) => void;
|
|
55
|
+
/** The minutes the judge waits for a human answer. */
|
|
56
|
+
waitMinutes: number;
|
|
57
|
+
/** The milliseconds until the next attempt after the judge asked to wait. */
|
|
58
|
+
recheckIntervalMs: number;
|
|
59
|
+
/** The clock (the tests drive it). */
|
|
60
|
+
now?: () => number;
|
|
61
|
+
/** The timer that drives the ticks (the tests drive it). */
|
|
62
|
+
setInterval?: (callback: () => void, ms: number) => unknown;
|
|
63
|
+
/** The timer that stops the ticks (the tests drive it). */
|
|
64
|
+
clearInterval?: (handle: unknown) => void;
|
|
65
|
+
/** The tick interval in milliseconds. */
|
|
66
|
+
tickMs?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The milliseconds between two ticks of the display. */
|
|
70
|
+
const COUNTDOWN_TICK_MS = 1_000;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The smallest tick interval.
|
|
74
|
+
*
|
|
75
|
+
* A shorter interval would repaint the status row faster than a reader can
|
|
76
|
+
* follow it and would waste the event loop of the harness.
|
|
77
|
+
*/
|
|
78
|
+
const MIN_TICK_MS = 100;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The deadline of the decision.
|
|
82
|
+
*
|
|
83
|
+
* The deadline is derived from the moment the wait started, so the
|
|
84
|
+
* countdown and the judge share one timestamp.
|
|
85
|
+
*/
|
|
86
|
+
export function decisionDeadlineAt(startedAt: number, waitMinutes: number): number {
|
|
87
|
+
const minutes = Number.isFinite(waitMinutes) ? Math.max(0, waitMinutes) : 0;
|
|
88
|
+
const started = Number.isFinite(startedAt) ? startedAt : 0;
|
|
89
|
+
return started + minutes * 60_000;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The remaining time as "MM:SS".
|
|
94
|
+
*
|
|
95
|
+
* A time that is not a number is shown as an empty countdown: a status row
|
|
96
|
+
* must never claim a time that does not exist.
|
|
97
|
+
*/
|
|
98
|
+
export function formatRemainingTime(remainingMs: number): string {
|
|
99
|
+
const safeMs = Number.isFinite(remainingMs) ? Math.max(0, remainingMs) : 0;
|
|
100
|
+
const totalSeconds = Math.floor(safeMs / 1_000);
|
|
101
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
102
|
+
const seconds = totalSeconds % 60;
|
|
103
|
+
return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The binding of a running countdown. */
|
|
107
|
+
interface CountdownBinding {
|
|
108
|
+
requestId: string;
|
|
109
|
+
deadlineAt: number;
|
|
110
|
+
mode: CountdownMode;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The countdown of one pending question.
|
|
115
|
+
*
|
|
116
|
+
* The countdown renders at once: the status row must show the remaining
|
|
117
|
+
* time without waiting for the first tick.
|
|
118
|
+
*/
|
|
119
|
+
export class PendingQuestionCountdown {
|
|
120
|
+
private readonly deps: PendingCountdownDeps;
|
|
121
|
+
private readonly now: () => number;
|
|
122
|
+
private readonly startTimer: (callback: () => void, ms: number) => unknown;
|
|
123
|
+
private readonly stopTimer: (handle: unknown) => void;
|
|
124
|
+
private readonly tickMs: number;
|
|
125
|
+
private handle: unknown = null;
|
|
126
|
+
private binding: CountdownBinding | null = null;
|
|
127
|
+
/**
|
|
128
|
+
* Bumped on every start and every stop.
|
|
129
|
+
*
|
|
130
|
+
* A tick of an older generation belongs to a binding that is gone: it is
|
|
131
|
+
* ignored instead of redrawing a status or triggering a decision.
|
|
132
|
+
*/
|
|
133
|
+
private generation = 0;
|
|
134
|
+
/**
|
|
135
|
+
* True while the countdown owns the wait.
|
|
136
|
+
*
|
|
137
|
+
* The countdown stays engaged after its deadline fired, until the
|
|
138
|
+
* decision path stops it or replaces it. The fallback poll uses this to
|
|
139
|
+
* stay out of the way of the accurate timer.
|
|
140
|
+
*/
|
|
141
|
+
private engaged = false;
|
|
142
|
+
/**
|
|
143
|
+
* True when the deadline fired and the decision did not start yet.
|
|
144
|
+
*
|
|
145
|
+
* The countdown owns the wait in that state, but it has no timer: the
|
|
146
|
+
* caller arms it again when the decision did not start.
|
|
147
|
+
*/
|
|
148
|
+
private deadlineFired = false;
|
|
149
|
+
|
|
150
|
+
constructor(deps: PendingCountdownDeps) {
|
|
151
|
+
this.deps = deps;
|
|
152
|
+
this.now = deps.now ?? (() => Date.now());
|
|
153
|
+
this.startTimer =
|
|
154
|
+
deps.setInterval ??
|
|
155
|
+
((callback, ms) => setInterval(callback, ms) as unknown);
|
|
156
|
+
this.stopTimer =
|
|
157
|
+
deps.clearInterval ??
|
|
158
|
+
((handle) => clearInterval(handle as NodeJS.Timeout));
|
|
159
|
+
this.tickMs = Math.max(MIN_TICK_MS, deps.tickMs ?? COUNTDOWN_TICK_MS);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Does the countdown own the current wait? */
|
|
163
|
+
get active(): boolean {
|
|
164
|
+
return this.engaged;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Did the deadline fire and is the decision still due? */
|
|
168
|
+
get fired(): boolean {
|
|
169
|
+
return this.deadlineFired;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The request the countdown belongs to. */
|
|
173
|
+
get requestId(): string | null {
|
|
174
|
+
return this.binding?.requestId ?? null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** What the countdown counts to. */
|
|
178
|
+
get mode(): CountdownMode | null {
|
|
179
|
+
return this.binding?.mode ?? null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Start the countdown of a request, or replace the running one.
|
|
184
|
+
*
|
|
185
|
+
* The deadline of the decision is derived from the moment the wait
|
|
186
|
+
* started: the caller passes that timestamp, the countdown computes the
|
|
187
|
+
* deadline. The countdown and the judge therefore share one timestamp.
|
|
188
|
+
*/
|
|
189
|
+
start(requestId: string, startedAt: number): void {
|
|
190
|
+
this.begin(requestId, decisionDeadlineAt(startedAt, this.deps.waitMinutes), "initial");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Start the countdown of the next attempt.
|
|
195
|
+
*
|
|
196
|
+
* The judge asked to wait longer: the countdown counts down to the next
|
|
197
|
+
* attempt instead of to the deadline of the decision.
|
|
198
|
+
*/
|
|
199
|
+
startRecheck(requestId: string): void {
|
|
200
|
+
this.begin(requestId, this.now() + this.deps.recheckIntervalMs, "recheck");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Stop the countdown and release its timer.
|
|
205
|
+
*
|
|
206
|
+
* Stopping is idempotent: a countdown that is not running can be stopped
|
|
207
|
+
* as often as the cleanup paths need. The status row is cleared with it,
|
|
208
|
+
* so a dead countdown never leaves a row that claims a decision is due.
|
|
209
|
+
*/
|
|
210
|
+
stop(): void {
|
|
211
|
+
const requestId = this.binding?.requestId ?? null;
|
|
212
|
+
this.engaged = false;
|
|
213
|
+
this.deadlineFired = false;
|
|
214
|
+
this.generation += 1;
|
|
215
|
+
this.clearTimer();
|
|
216
|
+
this.binding = null;
|
|
217
|
+
this.safeCall(() => this.deps.onStop?.(requestId));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Bind the countdown to a request and start its timer. */
|
|
221
|
+
private begin(requestId: string, deadlineAt: number, mode: CountdownMode): void {
|
|
222
|
+
this.clearTimer();
|
|
223
|
+
this.generation += 1;
|
|
224
|
+
const generation = this.generation;
|
|
225
|
+
this.binding = { requestId, deadlineAt, mode };
|
|
226
|
+
this.engaged = true;
|
|
227
|
+
this.deadlineFired = false;
|
|
228
|
+
try {
|
|
229
|
+
this.handle = this.startTimer(() => this.tick(generation), this.tickMs);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
// The timer could not be created: the countdown owns nothing, so
|
|
232
|
+
// it must not claim the wait. The row of the countdown is
|
|
233
|
+
// cleared with it, so no row claims a decision that is not due.
|
|
234
|
+
this.engaged = false;
|
|
235
|
+
this.binding = null;
|
|
236
|
+
this.safeCall(() => this.deps.onStop?.(requestId));
|
|
237
|
+
this.report(error);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
this.tick(generation);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* One tick of the countdown.
|
|
245
|
+
*
|
|
246
|
+
* The tick is a no-op when it belongs to a binding that is gone, or
|
|
247
|
+
* when the request it counts for is no longer the pending one.
|
|
248
|
+
*/
|
|
249
|
+
private tick(generation: number): void {
|
|
250
|
+
if (!this.engaged || generation !== this.generation) return;
|
|
251
|
+
const binding = this.binding;
|
|
252
|
+
if (!binding) return;
|
|
253
|
+
|
|
254
|
+
let remainingMs: number;
|
|
255
|
+
try {
|
|
256
|
+
if (!this.deps.isPending(binding.requestId)) {
|
|
257
|
+
// The question was answered or replaced: the countdown of
|
|
258
|
+
// that question is over.
|
|
259
|
+
this.stop();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
// The callback may have stopped the countdown: the binding of
|
|
263
|
+
// this tick is gone and must not be drawn any more.
|
|
264
|
+
if (!this.engaged || generation !== this.generation) return;
|
|
265
|
+
remainingMs = Math.max(0, binding.deadlineAt - this.now());
|
|
266
|
+
} catch (error) {
|
|
267
|
+
// A failing callback must never escape the timer: the countdown
|
|
268
|
+
// stops and the failure is reported.
|
|
269
|
+
this.stop();
|
|
270
|
+
this.report(error);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (remainingMs <= 0) {
|
|
275
|
+
// The deadline is reached. The timer stops, the countdown stays
|
|
276
|
+
// engaged until the decision path stops it or replaces it, so
|
|
277
|
+
// the fallback poll does not start a second decision.
|
|
278
|
+
this.clearTimer();
|
|
279
|
+
this.deadlineFired = true;
|
|
280
|
+
// The countdown reaches its zero before the decision starts.
|
|
281
|
+
this.safeRender({
|
|
282
|
+
requestId: binding.requestId,
|
|
283
|
+
remainingMs: 0,
|
|
284
|
+
remaining: formatRemainingTime(0),
|
|
285
|
+
mode: binding.mode,
|
|
286
|
+
});
|
|
287
|
+
// A failing decision handler must not leave a countdown that owns
|
|
288
|
+
// the wait without a timer: the countdown stops, so the fallback
|
|
289
|
+
// poll can take over.
|
|
290
|
+
try {
|
|
291
|
+
this.deps.onDeadline(binding.requestId);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
this.stop();
|
|
294
|
+
this.report(error);
|
|
295
|
+
}
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
this.safeRender({
|
|
300
|
+
requestId: binding.requestId,
|
|
301
|
+
remainingMs,
|
|
302
|
+
remaining: formatRemainingTime(remainingMs),
|
|
303
|
+
mode: binding.mode,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Draw the countdown, and report a failing status row. */
|
|
308
|
+
private safeRender(view: CountdownView): void {
|
|
309
|
+
try {
|
|
310
|
+
this.deps.render(view);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
this.stop();
|
|
313
|
+
this.report(error);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Run a callback of the countdown without letting it escape. */
|
|
318
|
+
private safeCall(action: () => void): void {
|
|
319
|
+
try {
|
|
320
|
+
action();
|
|
321
|
+
} catch (error) {
|
|
322
|
+
this.report(error);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Report a failure without letting the report escape. */
|
|
327
|
+
private report(error: unknown): void {
|
|
328
|
+
try {
|
|
329
|
+
this.deps.onError?.(error);
|
|
330
|
+
} catch {
|
|
331
|
+
// The report of a failure must not become a new failure.
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Release the timer of the countdown. */
|
|
336
|
+
private clearTimer(): void {
|
|
337
|
+
if (this.handle === null) return;
|
|
338
|
+
const handle = this.handle;
|
|
339
|
+
this.handle = null;
|
|
340
|
+
try {
|
|
341
|
+
this.stopTimer(handle);
|
|
342
|
+
} catch (error) {
|
|
343
|
+
// The timer is already released from the point of view of the
|
|
344
|
+
// countdown: the failure is reported, nothing is left behind.
|
|
345
|
+
this.report(error);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
package/index.ts
CHANGED
|
@@ -42,6 +42,10 @@ import {
|
|
|
42
42
|
relayShouldBeReported,
|
|
43
43
|
} from "./harness/startup-report.ts";
|
|
44
44
|
import { localSkillsReport, skillsReport } from "./harness/skills-startup.ts";
|
|
45
|
+
import {
|
|
46
|
+
PendingQuestionCountdown,
|
|
47
|
+
type CountdownView,
|
|
48
|
+
} from "./harness/pending-countdown.ts";
|
|
45
49
|
import { isChannelDisabled, setChannelDisabled } from "./harness/notification-settings.ts";
|
|
46
50
|
// The Honcho contract lives with the Honcho integration. A type-only import
|
|
47
51
|
// is erased at runtime, so the startup keeps loading the module lazily.
|
|
@@ -292,6 +296,9 @@ const sharedState = {
|
|
|
292
296
|
validationLedger: null as unknown,
|
|
293
297
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
294
298
|
updateAutoContinueStatus: null as any,
|
|
299
|
+
// The live countdown of the pending question. The channels stop it when
|
|
300
|
+
// a human answer arrives.
|
|
301
|
+
pendingCountdown: null as PendingQuestionCountdown | null,
|
|
295
302
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
296
303
|
pqController: null as any,
|
|
297
304
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -335,6 +342,8 @@ function applyChannelApproval(
|
|
|
335
342
|
// cleared after a successful resolution.
|
|
336
343
|
sharedState.waitingForUserSince = null;
|
|
337
344
|
sharedState.updateAutoContinueStatus?.("IDLE");
|
|
345
|
+
// The countdown of the answered question is over with it.
|
|
346
|
+
sharedState.pendingCountdown?.stop();
|
|
338
347
|
|
|
339
348
|
if (result.resolution === "yes") {
|
|
340
349
|
(sharedState.todoContinuationController as any)?.resume?.();
|
|
@@ -932,6 +941,12 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
932
941
|
let lastUserMessage: Date | null = null;
|
|
933
942
|
let autoContinueTimer: NodeJS.Timeout | null = null;
|
|
934
943
|
let _inferenceInProgress = false; // Track for overlap prevention
|
|
944
|
+
// The timing of the wait. The countdown in the status row and the
|
|
945
|
+
// decision of the judge use the same constants, so the displayed
|
|
946
|
+
// deadline and the decision can never drift apart.
|
|
947
|
+
const AUTO_CONTINUE_INTERVAL_MS = 60 * 1000; // Fallback poll: every minute
|
|
948
|
+
const MIN_WAIT_MINUTES = 5; // Minimum wait before auto-continue
|
|
949
|
+
const MAX_WAIT_MINUTES = 30; // After this the work pauses instead of waiting
|
|
935
950
|
// A continuation was dispatched and its turn has not settled yet.
|
|
936
951
|
// The gate owns that state: the status stays "RUN" until the turn
|
|
937
952
|
// settles, and no timer may clear it in the meantime.
|
|
@@ -1023,6 +1038,15 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
1023
1038
|
}
|
|
1024
1039
|
});
|
|
1025
1040
|
let _sessionId = Date.now().toString(36); // Track session for cancellation
|
|
1041
|
+
// The session of the Pi runtime. A countdown belongs to the session
|
|
1042
|
+
// that asked the question.
|
|
1043
|
+
let _piSessionId = "";
|
|
1044
|
+
// The session the running countdown belongs to. A countdown of a
|
|
1045
|
+
// session that is gone must not draw its status any more.
|
|
1046
|
+
let _countdownSessionId = "";
|
|
1047
|
+
// The request the row of the footer belongs to. A countdown that
|
|
1048
|
+
// stops must not clear the row of a newer question.
|
|
1049
|
+
let _countdownRowRequestId: string | null = null;
|
|
1026
1050
|
let _lastNotifiedQuestion = ""; // Track last question to prevent repeat notifications
|
|
1027
1051
|
let _completedNotifiedFor: string | null = null; // Track completion notification deduplication
|
|
1028
1052
|
|
|
@@ -1048,36 +1072,164 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
1048
1072
|
ERROR: "Auto-continue unavailable",
|
|
1049
1073
|
} as const;
|
|
1050
1074
|
|
|
1075
|
+
// The live countdown of the pending question.
|
|
1076
|
+
//
|
|
1077
|
+
// It owns the deadline of the wait: it shows the remaining time in
|
|
1078
|
+
// the status row once per second and starts the decision of the
|
|
1079
|
+
// judge exactly at the deadline. The fallback poll only takes over
|
|
1080
|
+
// when there is no countdown, so a decision is never started twice.
|
|
1081
|
+
const pendingCountdown = new PendingQuestionCountdown({
|
|
1082
|
+
// The two policies of the wait belong to the countdown, so the
|
|
1083
|
+
// displayed deadline and the deadline of the judge cannot drift
|
|
1084
|
+
// apart.
|
|
1085
|
+
waitMinutes: MIN_WAIT_MINUTES,
|
|
1086
|
+
recheckIntervalMs: AUTO_CONTINUE_INTERVAL_MS,
|
|
1087
|
+
render: (view: CountdownView) => {
|
|
1088
|
+
_countdownRowRequestId = view.requestId;
|
|
1089
|
+
updateAutoContinueStatus(
|
|
1090
|
+
"WAITING",
|
|
1091
|
+
view.requestId.slice(0, 8),
|
|
1092
|
+
view.mode === "recheck"
|
|
1093
|
+
? `Jev will recheck in ${view.remaining}`
|
|
1094
|
+
: `Jev decision in ${view.remaining}`,
|
|
1095
|
+
);
|
|
1096
|
+
},
|
|
1097
|
+
onDeadline: (requestId: string) => {
|
|
1098
|
+
logToFile("[auto-continue]", "The decision deadline was reached:", requestId);
|
|
1099
|
+
checkAndAutoContinue("countdown")
|
|
1100
|
+
.then(() => {
|
|
1101
|
+
// The decision did not start - another one was still in
|
|
1102
|
+
// flight, or the wait was gone before it ran. The
|
|
1103
|
+
// deadline is not lost: the countdown is armed again
|
|
1104
|
+
// and the decision follows.
|
|
1105
|
+
if (pendingCountdown.fired && pendingCountdown.requestId === requestId) {
|
|
1106
|
+
pendingCountdown.startRecheck(requestId);
|
|
1107
|
+
}
|
|
1108
|
+
})
|
|
1109
|
+
.catch((error) => {
|
|
1110
|
+
logToFile("[auto-continue]", "The decision failed:", oneLineError(error));
|
|
1111
|
+
// The decision did not run: the countdown releases the
|
|
1112
|
+
// wait, so the fallback poll can take over again.
|
|
1113
|
+
pendingCountdown.stop();
|
|
1114
|
+
});
|
|
1115
|
+
},
|
|
1116
|
+
isPending: (requestId: string) => {
|
|
1117
|
+
// The wait must still belong to the request: a late tick of
|
|
1118
|
+
// an answered or replaced question must not draw its status
|
|
1119
|
+
// and must not trigger a decision.
|
|
1120
|
+
if (!waitingForUserSince) return false;
|
|
1121
|
+
if (_countdownSessionId !== _piSessionId) return false;
|
|
1122
|
+
const current = sharedState.pqController?.getCurrentQuestion?.();
|
|
1123
|
+
if (!current) return false;
|
|
1124
|
+
return (
|
|
1125
|
+
current.requestId === requestId &&
|
|
1126
|
+
(current.state === "pending" || current.state === "evaluating")
|
|
1127
|
+
);
|
|
1128
|
+
},
|
|
1129
|
+
onStop: (requestId: string | null) => {
|
|
1130
|
+
// The countdown is over: the row is cleared with it, so no
|
|
1131
|
+
// row claims a decision that is not due any more. Only the row
|
|
1132
|
+
// of that countdown is cleared: a row of another state - the
|
|
1133
|
+
// judge continues, for example - belongs to that state, and
|
|
1134
|
+
// the row of a newer question belongs to that question.
|
|
1135
|
+
if (_countdownRowRequestId && requestId && _countdownRowRequestId !== requestId) {
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
_countdownRowRequestId = null;
|
|
1139
|
+
if (_currentAutoContinueState.startsWith("[WAIT]")) {
|
|
1140
|
+
_currentAutoContinueState = "";
|
|
1141
|
+
_setAutoContinueStatus?.(undefined);
|
|
1142
|
+
}
|
|
1143
|
+
},
|
|
1144
|
+
onError: (error: unknown) => {
|
|
1145
|
+
logToFile("[auto-continue]", "The countdown failed:", oneLineError(error));
|
|
1146
|
+
},
|
|
1147
|
+
});
|
|
1148
|
+
sharedState.pendingCountdown = pendingCountdown;
|
|
1149
|
+
|
|
1051
1150
|
// Wire shared state for callback handlers
|
|
1052
1151
|
sharedState.waitingForUserSince = waitingForUserSince;
|
|
1053
1152
|
sharedState.updateAutoContinueStatus = updateAutoContinueStatus;
|
|
1054
1153
|
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1154
|
+
/**
|
|
1155
|
+
* Start the countdown of a pending question.
|
|
1156
|
+
*
|
|
1157
|
+
* The deadline of the decision is derived from the wait that is
|
|
1158
|
+
* already running, so the displayed deadline and the decision of
|
|
1159
|
+
* the judge can never drift apart.
|
|
1160
|
+
*/
|
|
1161
|
+
function startPendingCountdown(requestId: string): void {
|
|
1162
|
+
_countdownSessionId = _piSessionId;
|
|
1163
|
+
const startedAt = waitingForUserSince?.getTime() ?? Date.now();
|
|
1164
|
+
pendingCountdown.start(requestId, startedAt);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* Start the countdown of the next attempt.
|
|
1169
|
+
*
|
|
1170
|
+
* The judge asked to wait longer: the countdown counts down to the
|
|
1171
|
+
* next attempt instead of to the deadline of the decision.
|
|
1172
|
+
*/
|
|
1173
|
+
function startRecheckCountdown(requestId: string): void {
|
|
1174
|
+
_countdownSessionId = _piSessionId;
|
|
1175
|
+
pendingCountdown.startRecheck(requestId);
|
|
1176
|
+
}
|
|
1058
1177
|
|
|
1178
|
+
// Update auto-continue status in TUI footer
|
|
1179
|
+
function updateAutoContinueStatus(
|
|
1180
|
+
state: keyof typeof AC_STATE_MESSAGES,
|
|
1181
|
+
requestId?: string,
|
|
1182
|
+
detail?: string,
|
|
1183
|
+
): void {
|
|
1059
1184
|
const message = AC_STATE_MESSAGES[state];
|
|
1060
1185
|
if (state === "IDLE") {
|
|
1061
|
-
//
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
// Only update if state changed (prevent flicker)
|
|
1070
|
-
if (statusText !== _currentAutoContinueState) {
|
|
1071
|
-
_setAutoContinueStatus(statusText);
|
|
1072
|
-
_currentAutoContinueState = statusText;
|
|
1186
|
+
// Only the row is cleared here: the countdown belongs to the
|
|
1187
|
+
// wait, not to the row. A settled continuation must not stop
|
|
1188
|
+
// the countdown of a question that the agent asked in the
|
|
1189
|
+
// same turn, and it must not blank its row either.
|
|
1190
|
+
if (!pendingCountdown.active) {
|
|
1191
|
+
_currentAutoContinueState = "";
|
|
1192
|
+
_setAutoContinueStatus?.(undefined);
|
|
1073
1193
|
}
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const parts = [`[${AC_STATE[state]}] ${message}`];
|
|
1198
|
+
if (requestId) parts.push(requestId);
|
|
1199
|
+
if (detail) parts.push(detail);
|
|
1200
|
+
const statusText = parts.join(" | ");
|
|
1201
|
+
|
|
1202
|
+
// Only update if state changed (prevent flicker). The state is
|
|
1203
|
+
// kept even without a UI binding: a session that starts without
|
|
1204
|
+
// a TUI shows the current state as soon as the UI is there.
|
|
1205
|
+
if (statusText !== _currentAutoContinueState) {
|
|
1206
|
+
_currentAutoContinueState = statusText;
|
|
1207
|
+
_setAutoContinueStatus?.(statusText);
|
|
1074
1208
|
}
|
|
1075
1209
|
}
|
|
1076
1210
|
|
|
1077
1211
|
// Capture UI context from session_start
|
|
1078
1212
|
pi.on("session_start", (_event, ctx) => {
|
|
1213
|
+
// The session of the runtime: the countdown of a question belongs
|
|
1214
|
+
// to the session that asked it.
|
|
1215
|
+
const sessionId = (
|
|
1216
|
+
ctx as { sessionManager?: { getSessionId?: () => string | undefined } }
|
|
1217
|
+
).sessionManager?.getSessionId?.();
|
|
1218
|
+
if (typeof sessionId === "string" && sessionId) {
|
|
1219
|
+
if (_piSessionId && _piSessionId !== sessionId) {
|
|
1220
|
+
// The session changed: the countdown of the old session
|
|
1221
|
+
// is over with it.
|
|
1222
|
+
pendingCountdown.stop();
|
|
1223
|
+
}
|
|
1224
|
+
_piSessionId = sessionId;
|
|
1225
|
+
}
|
|
1079
1226
|
if (ctx.hasUI && ctx.ui) {
|
|
1080
1227
|
_setAutoContinueStatus = ctx.ui.setStatus.bind(ctx.ui, "harness-auto-continue");
|
|
1228
|
+
// A session without a TUI collected the state already: it is
|
|
1229
|
+
// shown as soon as the UI is there.
|
|
1230
|
+
if (_currentAutoContinueState) {
|
|
1231
|
+
_setAutoContinueStatus(_currentAutoContinueState);
|
|
1232
|
+
}
|
|
1081
1233
|
}
|
|
1082
1234
|
|
|
1083
1235
|
});
|
|
@@ -1090,6 +1242,9 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
1090
1242
|
autoContinueTimer = null;
|
|
1091
1243
|
}
|
|
1092
1244
|
|
|
1245
|
+
// The countdown of the pending question is over with the session.
|
|
1246
|
+
pendingCountdown.stop();
|
|
1247
|
+
|
|
1093
1248
|
// Clear UI binding and status
|
|
1094
1249
|
if (_setAutoContinueStatus) {
|
|
1095
1250
|
_setAutoContinueStatus(undefined);
|
|
@@ -1148,6 +1303,8 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
1148
1303
|
waitingForUserSince = null;
|
|
1149
1304
|
sharedState.waitingForUserSince = null;
|
|
1150
1305
|
_lastNotifiedQuestion = "";
|
|
1306
|
+
// The question is answered: the countdown of the wait is over.
|
|
1307
|
+
pendingCountdown.stop();
|
|
1151
1308
|
updateAutoContinueStatus("IDLE");
|
|
1152
1309
|
// Dispatch answer to Pi
|
|
1153
1310
|
const message = answer === "yes" ? "yes" : "not yet";
|
|
@@ -1224,8 +1381,10 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
1224
1381
|
// Use question.requestId as the jobId for callback matching
|
|
1225
1382
|
const questionJobId = question.requestId;
|
|
1226
1383
|
|
|
1227
|
-
//
|
|
1228
|
-
|
|
1384
|
+
// The status row counts down to the decision of the judge.
|
|
1385
|
+
// The countdown is bound to this request, so a newer
|
|
1386
|
+
// question replaces it.
|
|
1387
|
+
startPendingCountdown(questionJobId);
|
|
1229
1388
|
|
|
1230
1389
|
// Get task context for additional info
|
|
1231
1390
|
const taskContext = await getTaskContext().catch(() => ({ total: 0, completed: 0, remaining: [] as string[] }));
|
|
@@ -1388,6 +1547,9 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
1388
1547
|
if (waitingForUserSince) {
|
|
1389
1548
|
waitingForUserSince = null;
|
|
1390
1549
|
_lastNotifiedQuestion = "";
|
|
1550
|
+
// The user answered in Pi: the countdown of the wait is
|
|
1551
|
+
// over with it.
|
|
1552
|
+
pendingCountdown.stop();
|
|
1391
1553
|
updateAutoContinueStatus("IDLE");
|
|
1392
1554
|
// Cancel pending question on user response
|
|
1393
1555
|
// (User answered via Pi instead of button)
|
|
@@ -1397,18 +1559,22 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
1397
1559
|
});
|
|
1398
1560
|
|
|
1399
1561
|
// Periodic check for auto-continue decision
|
|
1400
|
-
const AUTO_CONTINUE_INTERVAL_MS = 60 * 1000; // Check every 1 minute
|
|
1401
|
-
const MIN_WAIT_MINUTES = 5; // Minimum wait before auto-continue
|
|
1402
|
-
const MAX_WAIT_MINUTES = 30; // After this the work pauses instead of waiting
|
|
1403
1562
|
|
|
1404
|
-
const checkAndAutoContinue = async () => {
|
|
1563
|
+
const checkAndAutoContinue = async (source: "countdown" | "poll" = "poll") => {
|
|
1405
1564
|
// Prevent overlap if inference is already in progress
|
|
1406
1565
|
if (_inferenceInProgress) {
|
|
1566
|
+
// A deadline that was blocked is armed again by the caller of
|
|
1567
|
+
// the countdown, so the decision is not lost.
|
|
1407
1568
|
return;
|
|
1408
1569
|
}
|
|
1409
1570
|
|
|
1410
1571
|
if (!waitingForUserSince) return;
|
|
1411
1572
|
|
|
1573
|
+
// The countdown owns the wait while it runs: the fallback poll
|
|
1574
|
+
// only takes over when there is no countdown, so the decision is
|
|
1575
|
+
// never started twice.
|
|
1576
|
+
if (source === "poll" && pendingCountdown.active) return;
|
|
1577
|
+
|
|
1412
1578
|
// Capture session ID at check time for post-inference validation
|
|
1413
1579
|
const checkSessionId = _sessionId;
|
|
1414
1580
|
// Use same hash algorithm as detection
|
|
@@ -1470,6 +1636,9 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
1470
1636
|
if (taskContext.total === 0 && !hasPendingQuestion) {
|
|
1471
1637
|
logToFile("[auto-continue]", "No tasks and no pending questions, skipping auto-continue check");
|
|
1472
1638
|
_inferenceInProgress = false;
|
|
1639
|
+
// The countdown is over: the fallback poll takes over the
|
|
1640
|
+
// next attempt.
|
|
1641
|
+
pendingCountdown.stop();
|
|
1473
1642
|
return;
|
|
1474
1643
|
}
|
|
1475
1644
|
|
|
@@ -1624,15 +1793,20 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
1624
1793
|
if (waitBelongsToRequest(questionContext?.requestId)) {
|
|
1625
1794
|
waitingForUserSince = null;
|
|
1626
1795
|
sharedState.waitingForUserSince = null;
|
|
1796
|
+
// The request expired: the countdown of the wait
|
|
1797
|
+
// is over with it.
|
|
1798
|
+
pendingCountdown.stop();
|
|
1627
1799
|
updateAutoContinueStatus("IDLE");
|
|
1628
1800
|
}
|
|
1629
1801
|
} else {
|
|
1630
|
-
// The request stays open: the
|
|
1631
|
-
//
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
questionContext
|
|
1635
|
-
|
|
1802
|
+
// The request stays open: the countdown counts down to
|
|
1803
|
+
// the next check, so the retry does not wait for the
|
|
1804
|
+
// fallback poll.
|
|
1805
|
+
if (questionContext?.requestId) {
|
|
1806
|
+
startRecheckCountdown(questionContext.requestId);
|
|
1807
|
+
} else {
|
|
1808
|
+
updateAutoContinueStatus("WAITING");
|
|
1809
|
+
}
|
|
1636
1810
|
}
|
|
1637
1811
|
} else {
|
|
1638
1812
|
// Close the question with the Jev decision. The buttons of
|
|
@@ -1669,6 +1843,9 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
1669
1843
|
waitingForUserSince = null;
|
|
1670
1844
|
sharedState.waitingForUserSince = null;
|
|
1671
1845
|
_lastNotifiedQuestion = "";
|
|
1846
|
+
// The stale decision is discarded: the countdown
|
|
1847
|
+
// of the wait is over with it.
|
|
1848
|
+
pendingCountdown.stop();
|
|
1672
1849
|
updateAutoContinueStatus("IDLE");
|
|
1673
1850
|
} else {
|
|
1674
1851
|
logToFile(
|
|
@@ -1692,6 +1869,9 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
1692
1869
|
waitingForUserSince = null;
|
|
1693
1870
|
sharedState.waitingForUserSince = null;
|
|
1694
1871
|
_lastNotifiedQuestion = "";
|
|
1872
|
+
// The work pauses for a human: the countdown of
|
|
1873
|
+
// the wait is over with it.
|
|
1874
|
+
pendingCountdown.stop();
|
|
1695
1875
|
updateAutoContinueStatus("IDLE");
|
|
1696
1876
|
}
|
|
1697
1877
|
return;
|
|
@@ -1725,6 +1905,8 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
1725
1905
|
waitingForUserSince = null;
|
|
1726
1906
|
sharedState.waitingForUserSince = null;
|
|
1727
1907
|
_lastNotifiedQuestion = "";
|
|
1908
|
+
// The question is closed: the countdown of the wait is over.
|
|
1909
|
+
pendingCountdown.stop();
|
|
1728
1910
|
|
|
1729
1911
|
// The status stays "RUN" until the turn that the steer
|
|
1730
1912
|
// started has settled. A timer would clear it while the
|
|
@@ -1735,6 +1917,9 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
1735
1917
|
_inferenceInProgress = false;
|
|
1736
1918
|
} catch {
|
|
1737
1919
|
_inferenceInProgress = false;
|
|
1920
|
+
// The countdown is over: the fallback poll takes over the next
|
|
1921
|
+
// attempt.
|
|
1922
|
+
pendingCountdown.stop();
|
|
1738
1923
|
updateAutoContinueStatus("ERROR");
|
|
1739
1924
|
addHarnessMessage("Jev", "auto-continue evaluation failed", "err", [
|
|
1740
1925
|
"How to fix: Check logs at /tmp/pi-harness.log for API errors",
|
|
@@ -2108,7 +2293,10 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
2108
2293
|
});
|
|
2109
2294
|
|
|
2110
2295
|
// Start periodic check
|
|
2111
|
-
autoContinueTimer = setInterval(
|
|
2296
|
+
autoContinueTimer = setInterval(
|
|
2297
|
+
() => checkAndAutoContinue("poll"),
|
|
2298
|
+
AUTO_CONTINUE_INTERVAL_MS,
|
|
2299
|
+
);
|
|
2112
2300
|
if (jevKeyMissing) {
|
|
2113
2301
|
// Without a provider the health check cannot run. The missing-key
|
|
2114
2302
|
// message (with its fix commands) stays the visible one.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-harness/two-phase-review",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.460",
|
|
4
4
|
"description": "pi-harness-runtime — two-phase implementation and review workflow (queue review signals, gate the phase, scope the diff)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|