aegis-desktop 0.4.2 → 0.4.3
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/lib/local/engine.js +60 -7
- package/package.json +1 -1
- package/vendor/aegis.js +52 -7
package/lib/local/engine.js
CHANGED
|
@@ -96,8 +96,17 @@ const EFFORT_TOKEN_BUDGET = { low: 8192, medium: 16384, high: 32768 };
|
|
|
96
96
|
* that is a multi-thousand-token reasoning pass per worker — easily past a
|
|
97
97
|
* minute. Timing out there aborts a perfectly healthy autonomous turn
|
|
98
98
|
* mid-flight, after the server has already run and billed every worker.
|
|
99
|
+
*
|
|
100
|
+
* 15 minutes deliberately outlasts the server's OWN ceiling for that window
|
|
101
|
+
* (aegis1 services/pool_brain.py: NEXUS_BRAIN_WORKER_TIMEOUT, default 600s),
|
|
102
|
+
* because aborting first leaves the server running and billing a fan-out
|
|
103
|
+
* nobody will ever see. Raising that env var past ~14 minutes means raising
|
|
104
|
+
* this constant too; the shared client applies the same budget from the
|
|
105
|
+
* server's X-AEGIS-Brain response header (see client/aegis.js idleBudgetFor),
|
|
106
|
+
* which is what covers a fan-out the caller did not flag — test/
|
|
107
|
+
* autonomous-mode.test.mjs pins both halves.
|
|
99
108
|
*/
|
|
100
|
-
const AUTONOMOUS_IDLE_TIMEOUT_MS =
|
|
109
|
+
const AUTONOMOUS_IDLE_TIMEOUT_MS = 15 * 60_000;
|
|
101
110
|
|
|
102
111
|
/**
|
|
103
112
|
* Only ever raises a too-low budget for a DeepSeek reasoning model — never
|
|
@@ -513,6 +522,12 @@ function createLocalEngine({ aegis, settings, ollama, providers, tools, promptBu
|
|
|
513
522
|
/** One transport round for the chosen class. */
|
|
514
523
|
async function dispatch(cls, opts) {
|
|
515
524
|
if (cls === 'aegis') {
|
|
525
|
+
// `undefined` = "leave the model id's own default alone" (a real user
|
|
526
|
+
// turn on the pooled class: the selected Nexus id is what decides, so
|
|
527
|
+
// today's behaviour is unchanged). `false` = an explicit single provider
|
|
528
|
+
// call, for a pass that is a continuation rather than a new
|
|
529
|
+
// investigation. `true` = the autonomous fan-out.
|
|
530
|
+
const brainFlag = opts.singlePass ? false : opts.autonomous ? true : undefined;
|
|
516
531
|
return aegis.chatCompletion({
|
|
517
532
|
prompt: opts.prompt,
|
|
518
533
|
system: opts.system,
|
|
@@ -542,13 +557,30 @@ function createLocalEngine({ aegis, settings, ollama, providers, tools, promptBu
|
|
|
542
557
|
extra: {
|
|
543
558
|
aegis_memory: true,
|
|
544
559
|
session: opts.sessionId,
|
|
545
|
-
|
|
546
|
-
//
|
|
560
|
+
// The fan-out is opt-in per dispatch. `brain` is sent EXPLICITLY
|
|
561
|
+
// whenever this dispatch is not the autonomous one, because the
|
|
562
|
+
// model id this class sends (``nexus-brain``) enables the pooled
|
|
563
|
+
// brain on its own: without the flag a continuation pass — the
|
|
564
|
+
// doubled-budget retry, or the "write up what you already found"
|
|
565
|
+
// re-dispatch — silently re-ran the whole workers+1 fan-out for a
|
|
566
|
+
// pass whose documented cost is a single request. aegis1
|
|
567
|
+
// services/pool_brain.py parse_brain_request honours the opt-out.
|
|
568
|
+
...(brainFlag === undefined ? {} : { brain: brainFlag }),
|
|
569
|
+
// An opted-out pass is also told WHICH band to run on. A single call
|
|
570
|
+
// on a brain model id infers its band from the id and lands on
|
|
571
|
+
// "fast" (the cheapest id in the pool); "brain" is the band the
|
|
572
|
+
// workers themselves run on (cheapest model that can think). Same
|
|
573
|
+
// model as the fan-out, one sample instead of four — otherwise
|
|
574
|
+
// dropping the fan-out would have quietly changed the model too.
|
|
575
|
+
...(brainFlag === false ? { mode: 'brain' } : {}),
|
|
576
|
+
// Only meaningful (and only sent) alongside a running fan-out — aegis1
|
|
547
577
|
// services/pool_brain.py parse_brain_request reads `effort`/
|
|
548
578
|
// `workers` straight off the body and clamps them itself
|
|
549
579
|
// (EFFORT_LEVELS / MAX_WORKERS), so no client-side validation here.
|
|
550
|
-
|
|
551
|
-
|
|
580
|
+
// Keyed on the effective brain flag, not on `autonomous`: an opted-out
|
|
581
|
+
// single pass carries no fan-out tuning it cannot use.
|
|
582
|
+
...(brainFlag === true && opts.effort ? { effort: opts.effort } : {}),
|
|
583
|
+
...(brainFlag === true && opts.workers ? { workers: opts.workers } : {}),
|
|
552
584
|
// The pool forwards `tools` to the provider and returns tool_calls
|
|
553
585
|
// (aegis1 app.py:7765 → provider, pool_brain synthesis keeps them).
|
|
554
586
|
...(opts.tools.length ? { tools: opts.tools } : {}),
|
|
@@ -763,7 +795,16 @@ function createLocalEngine({ aegis, settings, ollama, providers, tools, promptBu
|
|
|
763
795
|
// would stream it a second time onto the same bubble.
|
|
764
796
|
if (!truncationRetried && !assistantText(res) && isTruncated(res)) {
|
|
765
797
|
truncationRetried = true;
|
|
766
|
-
|
|
798
|
+
// singlePass: this retry buys *budget*, not a second investigation.
|
|
799
|
+
// The fan-out's workers would re-run the whole task from scratch for
|
|
800
|
+
// it — 3 extra reasoning passes + a synthesis — which is the opposite
|
|
801
|
+
// of what "one doubled request" means (and what the note above
|
|
802
|
+
// promises). The pass itself is unchanged apart from that.
|
|
803
|
+
res = await dispatch(cls, {
|
|
804
|
+
...opts,
|
|
805
|
+
singlePass: true,
|
|
806
|
+
maxTokens: doubledBudget(opts.maxTokens),
|
|
807
|
+
});
|
|
767
808
|
addUsage(res);
|
|
768
809
|
}
|
|
769
810
|
|
|
@@ -778,7 +819,19 @@ function createLocalEngine({ aegis, settings, ollama, providers, tools, promptBu
|
|
|
778
819
|
synthesisDone = true;
|
|
779
820
|
foldPromptIntoHistory();
|
|
780
821
|
history.push({ role: 'user', content: EMPTY_TURN_NUDGE });
|
|
781
|
-
|
|
822
|
+
// singlePass: same reasoning as the truncation retry above, and it is
|
|
823
|
+
// the same comment's literal promise ("force the summary out of the
|
|
824
|
+
// context it already holds"). Escalating a write-up back into the
|
|
825
|
+
// worker fan-out asked three fresh workers to redo an investigation
|
|
826
|
+
// whose findings are already in `history`, at 4x the cost, to produce
|
|
827
|
+
// a paragraph the model had all the material for.
|
|
828
|
+
res = await dispatch(cls, {
|
|
829
|
+
...opts,
|
|
830
|
+
singlePass: true,
|
|
831
|
+
messages: history,
|
|
832
|
+
prompt: '',
|
|
833
|
+
tools: [],
|
|
834
|
+
});
|
|
782
835
|
addUsage(res);
|
|
783
836
|
if (!assistantText(res)) {
|
|
784
837
|
throw emptyTurnError({
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aegis-desktop",
|
|
3
3
|
"productName": "AEGIS Desktop",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.3",
|
|
5
5
|
"description": "Thin Electron host for AEGIS — a local chat UI over the shared client/aegis.js transport. Ships transport + UI only; engine logic stays server-side.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "AEGIS Code",
|
package/vendor/aegis.js
CHANGED
|
@@ -466,12 +466,15 @@ function createClient(opts = {}) {
|
|
|
466
466
|
// close), reader.read() below waits forever and the whole desktop host
|
|
467
467
|
// hangs with no way out but force-quit. Cap the gap between chunks —
|
|
468
468
|
// not the whole response — so a slow-but-alive generation is untouched.
|
|
469
|
-
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
//
|
|
473
|
-
|
|
474
|
-
|
|
469
|
+
//
|
|
470
|
+
// The budget is per *response*, not per call site: a pooled brain call
|
|
471
|
+
// announces its worker fan-out in the X-AEGIS-Brain response header, and a
|
|
472
|
+
// fan-out is legitimately silent until its first worker returns. Keying the
|
|
473
|
+
// longer budget on a request the caller remembered to flag left the
|
|
474
|
+
// default Nexus turn (brain model id, checkbox off) dying at 60s — with
|
|
475
|
+
// the server already past its own fan-out deadline and every worker
|
|
476
|
+
// billed. See idleBudgetFor().
|
|
477
|
+
const idleMs = idleBudgetFor(res, idleTimeoutMs);
|
|
475
478
|
async function readWithIdleTimeout() {
|
|
476
479
|
let timer;
|
|
477
480
|
const timeout = new Promise((_, reject) => {
|
|
@@ -752,7 +755,49 @@ function createClient(opts = {}) {
|
|
|
752
755
|
};
|
|
753
756
|
}
|
|
754
757
|
|
|
755
|
-
|
|
758
|
+
/** Gap between SSE chunks that counts as "the stream is dead", in ms. */
|
|
759
|
+
const SSE_IDLE_TIMEOUT_MS = 60_000;
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Gap allowed while a pooled brain call runs its worker fan-out, in ms.
|
|
763
|
+
*
|
|
764
|
+
* The fan-out yields a header chunk and then says nothing until its FIRST
|
|
765
|
+
* worker pass returns — and each worker is a full reasoning-model call at
|
|
766
|
+
* roughly 1/(workers+1) of the effort budget, so the silent window is minutes,
|
|
767
|
+
* not seconds. The server bounds that window itself
|
|
768
|
+
* (``services/pool_brain.py`` ``NEXUS_BRAIN_WORKER_TIMEOUT``, default 600s) and
|
|
769
|
+
* announces it per-response with the ``X-AEGIS-Brain`` header, so this budget
|
|
770
|
+
* has to outlast the *server's* deadline: aborting first kills a healthy turn
|
|
771
|
+
* the server is still running and billing.
|
|
772
|
+
*
|
|
773
|
+
* Raising the server's env var above ~14 minutes requires raising this too.
|
|
774
|
+
* desktop/lib/local/engine.js mirrors the same value for the explicit
|
|
775
|
+
* "work autonomously" path (AUTONOMOUS_IDLE_TIMEOUT_MS); the desktop test
|
|
776
|
+
* test/autonomous-mode.test.mjs fails if either drops below the server default.
|
|
777
|
+
*/
|
|
778
|
+
const BRAIN_IDLE_TIMEOUT_MS = 15 * 60_000;
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Idle budget for one SSE response: the caller's override when it asked for
|
|
782
|
+
* one, widened to the fan-out budget when the server says this response *is* a
|
|
783
|
+
* fan-out. The header is the authoritative signal — it is set by the same code
|
|
784
|
+
* that runs the fan-out, so a renamed brain id or a caller that forgot its
|
|
785
|
+
* flag cannot desynchronise the two. A response with no header (an older
|
|
786
|
+
* server, or a single-pass call) keeps the caller's budget or the 60s default.
|
|
787
|
+
*/
|
|
788
|
+
function idleBudgetFor(res, requestedMs) {
|
|
789
|
+
const base = Number(requestedMs) > 0 ? Number(requestedMs) : SSE_IDLE_TIMEOUT_MS;
|
|
790
|
+
let header = '';
|
|
791
|
+
try {
|
|
792
|
+
const get = res && res.headers && typeof res.headers.get === 'function' ? res.headers.get.bind(res.headers) : null;
|
|
793
|
+
header = (get && get('X-AEGIS-Brain')) || '';
|
|
794
|
+
} catch {
|
|
795
|
+
header = ''; // an exotic fetch shim without headers: keep the caller's budget
|
|
796
|
+
}
|
|
797
|
+
return header && String(header).trim() ? Math.max(base, BRAIN_IDLE_TIMEOUT_MS) : base;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const api = { createClient, envVar, randomUUID, DEFAULT_API_BASE, CLIENT_VERSION, idleBudgetFor, SSE_IDLE_TIMEOUT_MS, BRAIN_IDLE_TIMEOUT_MS };
|
|
756
801
|
|
|
757
802
|
// Node / Electron (CommonJS): the MCP plugin and desktop shell require() this.
|
|
758
803
|
if (typeof module !== 'undefined' && module.exports) {
|