specrails-desktop 2.30.0 → 2.30.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.
- package/client/dist/assets/{ActivityFeedPage-AQgpr9qG.js → ActivityFeedPage-9DZOvIcY.js} +1 -1
- package/client/dist/assets/{AgentBrowserCapture-I7a0usG7.js → AgentBrowserCapture-DqyHbzk7.js} +1 -1
- package/client/dist/assets/{AgentModeCodePane-BqOVg15n.js → AgentModeCodePane-BAYyFRmY.js} +2 -2
- package/client/dist/assets/{AgentModeJobsPane-4SG6FF27.js → AgentModeJobsPane-CTIHMUKN.js} +1 -1
- package/client/dist/assets/{AgentsPage-C3FgYlEi.js → AgentsPage-DuyJuaFs.js} +1 -1
- package/client/dist/assets/{AnalyticsPage-BcMNByW5.js → AnalyticsPage-BY7gtCx6.js} +1 -1
- package/client/dist/assets/{CodePage-CgdDkXp-.js → CodePage-vyQr40Vl.js} +1 -1
- package/client/dist/assets/{DesktopAnalyticsPage-DPMcS88n.js → DesktopAnalyticsPage-CkXUFj3F.js} +1 -1
- package/client/dist/assets/{DocsDialog-BC7574Zi.js → DocsDialog-PhdCJNVj.js} +1 -1
- package/client/dist/assets/{DocsPage-Dd9yilUg.js → DocsPage-BY-wDthx.js} +1 -1
- package/client/dist/assets/{InteractiveJobComposer-CqC_0VQL.js → InteractiveJobComposer-C9I0A4KR.js} +1 -1
- package/client/dist/assets/{JobDetailModal-BFNXOhk1.js → JobDetailModal-I_RoQJV4.js} +1 -1
- package/client/dist/assets/{JobDetailPage-DM-6zYpA.js → JobDetailPage-CU4FEnUC.js} +1 -1
- package/client/dist/assets/{JobsPage-1dkjPk1k.js → JobsPage-DcllM4Ip.js} +1 -1
- package/client/dist/assets/{LoopBuilderPage-BPeqz6cG.js → LoopBuilderPage-DGm_iuff.js} +1 -1
- package/client/dist/assets/{LoopPreviewModal-EdkyYaGN.js → LoopPreviewModal-BC-TeFdD.js} +1 -1
- package/client/dist/assets/{LoopsPage-nZfgiaK8.js → LoopsPage-SqglnXlp.js} +1 -1
- package/client/dist/assets/{PluginsPage-CwFfFBzW.js → PluginsPage-B89S8o1a.js} +1 -1
- package/client/dist/assets/{ProjectSettingsDialog-DHsxLFMT.js → ProjectSettingsDialog-nEs-ENrE.js} +1 -1
- package/client/dist/assets/{TemplatePreviewModal-wcI3nGNr.js → TemplatePreviewModal-D4qW-J9B.js} +1 -1
- package/client/dist/assets/{TicketDetailModal-7EloKP1C.js → TicketDetailModal-CI47Dq9E.js} +2 -2
- package/client/dist/assets/{dist-js-zvt5ap3o.js → dist-js-Dj72W6qo.js} +1 -1
- package/client/dist/assets/{dist-js-Bt2zW2YG.js → dist-js-jwoXo0YD.js} +1 -1
- package/client/dist/assets/{index-xkzK3E5i.js → index-F9r8mWP_.js} +5 -5
- package/client/dist/index.html +2 -2
- package/docs/internals/interactive-jobs.md +1 -1
- package/package.json +1 -1
- package/server/dist/core-update-manager.js +10 -3
- package/server/dist/framework-manager.js +22 -8
- package/server/dist/interactive-job-session.js +48 -1
package/client/dist/index.html
CHANGED
|
@@ -204,7 +204,7 @@
|
|
|
204
204
|
.specrails-splash__mark .pill { opacity: 1; }
|
|
205
205
|
}
|
|
206
206
|
</style>
|
|
207
|
-
<script type="module" crossorigin src="/assets/index-
|
|
207
|
+
<script type="module" crossorigin src="/assets/index-F9r8mWP_.js"></script>
|
|
208
208
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-CNC7AqOf.js">
|
|
209
209
|
<link rel="modulepreload" crossorigin href="/assets/jsx-runtime-zu2_FqZY.js">
|
|
210
210
|
<link rel="modulepreload" crossorigin href="/assets/react-dom-DiWzBDP9.js">
|
|
@@ -276,7 +276,7 @@
|
|
|
276
276
|
<link rel="modulepreload" crossorigin href="/assets/getTimezoneOffsetInMilliseconds-qrcBaLW1.js">
|
|
277
277
|
<link rel="modulepreload" crossorigin href="/assets/formatDistanceToNow-ByKli7Q_.js">
|
|
278
278
|
<link rel="modulepreload" crossorigin href="/assets/spending-DMnCXy84.js">
|
|
279
|
-
<link rel="modulepreload" crossorigin href="/assets/TicketDetailModal-
|
|
279
|
+
<link rel="modulepreload" crossorigin href="/assets/TicketDetailModal-CI47Dq9E.js">
|
|
280
280
|
<link rel="stylesheet" crossorigin href="/assets/index-Dh9i4CnN.css">
|
|
281
281
|
</head>
|
|
282
282
|
<body>
|
|
@@ -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
|
@@ -136,9 +136,16 @@ class CoreUpdateManager {
|
|
|
136
136
|
if (mat.errors.length > 0) {
|
|
137
137
|
throw new Error(`framework materialize failed: ${mat.errors.map((e) => `${e.provider}: ${e.message}`).join('; ')}`);
|
|
138
138
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
// A materialize that reports no errors but ALSO no materialized providers
|
|
140
|
+
// wrote nothing — swapping `current` at a nonexistent version dir would
|
|
141
|
+
// fail downstream with a misleading "swap-current failed". Surface the
|
|
142
|
+
// real condition instead (mirrors FrameworkManager.versionCheck's guard).
|
|
143
|
+
if (mat.providers.length === 0) {
|
|
144
|
+
throw new Error(`framework materialize completed without materializing any provider (requested: ${providers.join(', ') || 'none'})`);
|
|
145
|
+
}
|
|
146
|
+
const swapped = fm.swapCurrentDetailed(installed);
|
|
147
|
+
if (!swapped.ok) {
|
|
148
|
+
throw new Error(`framework swap-current failed${swapped.detail ? `: ${swapped.detail}` : ''}`);
|
|
142
149
|
}
|
|
143
150
|
this.latestVersion = installed;
|
|
144
151
|
this.lastCheckedAt = Date.now();
|
|
@@ -204,16 +204,30 @@ class FrameworkManager {
|
|
|
204
204
|
* `swap-current` only moves the version pointer (provider-invariant).
|
|
205
205
|
*/
|
|
206
206
|
swapCurrent(version, _provider = 'claude') {
|
|
207
|
+
return this.swapCurrentDetailed(version).ok;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Like `swapCurrent`, but on failure carries WHY it failed (`detail`) — the
|
|
211
|
+
* subprocess stderr/exit or the missing-CLI/verify-mismatch case — so callers
|
|
212
|
+
* (the core update channel) can surface an actionable error instead of the
|
|
213
|
+
* bare "swap-current failed".
|
|
214
|
+
*/
|
|
215
|
+
swapCurrentDetailed(version) {
|
|
207
216
|
const cli = this.resolveCli();
|
|
208
217
|
if (!cli)
|
|
209
|
-
return false;
|
|
218
|
+
return { ok: false, detail: 'no usable core CLI to run swap-current with' };
|
|
210
219
|
if (readCurrentFrameworkVersion(this.home) === version)
|
|
211
|
-
return true;
|
|
220
|
+
return { ok: true, detail: null };
|
|
212
221
|
const fwDir = frameworkRoot(this.home);
|
|
213
222
|
const res = (0, child_process_1.spawnSync)(nodeInterpreter(), [cli, 'swap-current', '--framework-dir', fwDir, '--version', version], { env: this.childEnv(), encoding: 'utf-8', timeout: 120_000 });
|
|
214
|
-
if (res.error || (res.status ?? 1) !== 0)
|
|
215
|
-
|
|
216
|
-
|
|
223
|
+
if (res.error || (res.status ?? 1) !== 0) {
|
|
224
|
+
const detail = (res.error?.message ?? `${res.stderr || res.stdout || ''}`.trim()) || `exit ${res.status ?? 'unknown'}`;
|
|
225
|
+
return { ok: false, detail };
|
|
226
|
+
}
|
|
227
|
+
if (readCurrentFrameworkVersion(this.home) !== version) {
|
|
228
|
+
return { ok: false, detail: `swap-current exited 0 but current still resolves to ${readCurrentFrameworkVersion(this.home) ?? 'nothing'}` };
|
|
229
|
+
}
|
|
230
|
+
return { ok: true, detail: null };
|
|
217
231
|
}
|
|
218
232
|
/**
|
|
219
233
|
* Compare the bundled core version with `framework/current`. When they differ
|
|
@@ -266,8 +280,8 @@ class FrameworkManager {
|
|
|
266
280
|
// No errors yet nothing materialized (empty/duplicate-only provider list).
|
|
267
281
|
return { swapped: false, version: current };
|
|
268
282
|
}
|
|
269
|
-
const
|
|
270
|
-
if (ok) {
|
|
283
|
+
const swap = this.swapCurrentDetailed(bundled);
|
|
284
|
+
if (swap.ok) {
|
|
271
285
|
try {
|
|
272
286
|
this.broadcast?.({ type: 'framework.updated', version: bundled });
|
|
273
287
|
}
|
|
@@ -281,7 +295,7 @@ class FrameworkManager {
|
|
|
281
295
|
this.broadcast?.({
|
|
282
296
|
type: 'framework.update_failed',
|
|
283
297
|
version: bundled,
|
|
284
|
-
errors: [{ provider: providers[0] ?? 'claude', message:
|
|
298
|
+
errors: [{ provider: providers[0] ?? 'claude', message: `swap-current failed${swap.detail ? `: ${swap.detail}` : ''}` }],
|
|
285
299
|
});
|
|
286
300
|
}
|
|
287
301
|
catch {
|
|
@@ -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();
|