specrails-desktop 2.30.0 → 2.30.1
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.
|
@@ -61,7 +61,7 @@ spawnInteractive = isInteractiveJobsEnabled() // SPECRAILS_INTERACTIV
|
|
|
61
61
|
| | `'finalize'` | `'auto'` |
|
|
62
62
|
|---|---|---|
|
|
63
63
|
| Who gets it | freestyle/Freestyle QueueManager jobs (claude) | every other interactive job + ALL loop ai-steps |
|
|
64
|
-
| End of session | explicit human **Finalize** only (SIGTERM → 2s → SIGKILL) | **quiescence**: a turn `result` arrived, nothing queued, no write in flight |
|
|
64
|
+
| End of session | explicit human **Finalize** only (SIGTERM → 2s → SIGKILL) | **quiescence**: a turn `result` arrived, nothing queued, no write in flight — torn down GRACEFULLY: stdin EOF (the CLI exits itself, flushing its session transcript so the next loop step can `--resume`), SIGTERM only after a grace window (`quiescentEofGraceMs`, default 5s) or when stdin is already gone |
|
|
65
65
|
| Idle between turns | by design (awaiting the human) | only transiently (microtask window) |
|
|
66
66
|
| Wedge detector | never armed | the queue's zombie-timeout budget, reset on any raw child output; silence for the whole budget → fold in-flight turn, settle `crashed` |
|
|
67
67
|
| Composer action | **Finalize Job** | **Wrap up now** (QueueManager) / **Settle this step** (loop step) |
|
package/package.json
CHANGED
|
@@ -119,6 +119,10 @@ class InteractiveJobSession {
|
|
|
119
119
|
_onSettle;
|
|
120
120
|
_settleMode;
|
|
121
121
|
_zombieTimeoutMs;
|
|
122
|
+
_quiescentEofGraceMs;
|
|
123
|
+
/** Armed by the quiescent graceful teardown: SIGTERM escalation if the child
|
|
124
|
+
* ignores the stdin EOF. Cleared on close/settle/dispose. */
|
|
125
|
+
_eofTimer = null;
|
|
122
126
|
_nextEventSeq;
|
|
123
127
|
_persistTurnUsage;
|
|
124
128
|
_persistTurnActivity;
|
|
@@ -194,6 +198,7 @@ class InteractiveJobSession {
|
|
|
194
198
|
this._onSettle = deps.onSettle;
|
|
195
199
|
this._settleMode = deps.settleMode ?? 'finalize';
|
|
196
200
|
this._zombieTimeoutMs = deps.zombieTimeoutMs ?? 0;
|
|
201
|
+
this._quiescentEofGraceMs = deps.quiescentEofGraceMs ?? 5000;
|
|
197
202
|
this._nextEventSeq = deps.nextEventSeq ?? null;
|
|
198
203
|
this._persistTurnUsage = deps.persistTurnUsage ?? null;
|
|
199
204
|
this._persistTurnActivity = deps.persistTurnActivity ?? null;
|
|
@@ -229,6 +234,7 @@ class InteractiveJobSession {
|
|
|
229
234
|
child.on('close', (code) => {
|
|
230
235
|
this._childClosed = true;
|
|
231
236
|
this._clearKillTimer();
|
|
237
|
+
this._clearEofTimer();
|
|
232
238
|
this._handleClose(code);
|
|
233
239
|
});
|
|
234
240
|
// A buffered write can fail asynchronously (typically EPIPE after the child
|
|
@@ -320,6 +326,39 @@ class InteractiveJobSession {
|
|
|
320
326
|
this._clearZombieTimer();
|
|
321
327
|
this._terminateChildTree('finalized');
|
|
322
328
|
}
|
|
329
|
+
/** Quiescent auto-settle teardown, GRACEFUL: EOF the child's stdin so the
|
|
330
|
+
* stream-json CLI exits by itself — it flushes its session transcript on the
|
|
331
|
+
* way out, keeping the session `--resume`-able by the NEXT loop step. The
|
|
332
|
+
* previous immediate SIGTERM raced that flush: the next step's resume found
|
|
333
|
+
* no conversation, got an instant empty synthetic result, and settled
|
|
334
|
+
* zero-work in 0.0s. SIGTERM remains as escalation after the grace window,
|
|
335
|
+
* and as the direct fallback when stdin is already gone. */
|
|
336
|
+
_finalizeQuiescent() {
|
|
337
|
+
if (this._finalizing || this._settled)
|
|
338
|
+
return;
|
|
339
|
+
this._finalizing = true;
|
|
340
|
+
this._clearZombieTimer();
|
|
341
|
+
const child = this._child;
|
|
342
|
+
const stdin = child?.stdin;
|
|
343
|
+
if (!child || this._childClosed || !stdin || stdin.destroyed || this._stdinFailed) {
|
|
344
|
+
this._terminateChildTree('finalized');
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
stdin.end();
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
this._terminateChildTree('finalized');
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
this._eofTimer = setTimeout(() => {
|
|
355
|
+
this._eofTimer = null;
|
|
356
|
+
if (this._childClosed || this._settled || this._disposed)
|
|
357
|
+
return;
|
|
358
|
+
this._terminateChildTree('finalized');
|
|
359
|
+
}, this._quiescentEofGraceMs);
|
|
360
|
+
this._eofTimer.unref?.();
|
|
361
|
+
}
|
|
323
362
|
/** Programmatic teardown that SETTLES 'crashed' after folding any in-flight
|
|
324
363
|
* turn (loop step timeout / run cancel). Unlike dispose(), the onSettle
|
|
325
364
|
* callback fires — an owner AWAITING the settle (the loop engine) is
|
|
@@ -342,6 +381,7 @@ class InteractiveJobSession {
|
|
|
342
381
|
return;
|
|
343
382
|
this._disposed = true;
|
|
344
383
|
this._clearZombieTimer();
|
|
384
|
+
this._clearEofTimer();
|
|
345
385
|
this._closeReaders();
|
|
346
386
|
this._terminateChildTree(null);
|
|
347
387
|
this._child = null;
|
|
@@ -603,7 +643,7 @@ class InteractiveJobSession {
|
|
|
603
643
|
return;
|
|
604
644
|
if (this._streaming || this._pending.length > 0)
|
|
605
645
|
return;
|
|
606
|
-
this.
|
|
646
|
+
this._finalizeQuiescent();
|
|
607
647
|
});
|
|
608
648
|
}
|
|
609
649
|
}
|
|
@@ -785,6 +825,7 @@ class InteractiveJobSession {
|
|
|
785
825
|
this._streaming = false;
|
|
786
826
|
this._awaitingResult = false;
|
|
787
827
|
this._clearKillTimer();
|
|
828
|
+
this._clearEofTimer();
|
|
788
829
|
this._clearZombieTimer();
|
|
789
830
|
this._closeReaders();
|
|
790
831
|
// The child is gone — any prompts still queued (turn died without a `result`,
|
|
@@ -888,6 +929,12 @@ class InteractiveJobSession {
|
|
|
888
929
|
this._killTimer = null;
|
|
889
930
|
}
|
|
890
931
|
}
|
|
932
|
+
_clearEofTimer() {
|
|
933
|
+
if (this._eofTimer !== null) {
|
|
934
|
+
clearTimeout(this._eofTimer);
|
|
935
|
+
this._eofTimer = null;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
891
938
|
_closeReaders() {
|
|
892
939
|
try {
|
|
893
940
|
this._stdoutReader?.close();
|