aegis-desktop 0.3.0 → 0.3.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.
- package/main.js +54 -16
- package/package.json +1 -1
- package/renderer/app.js +57 -14
package/main.js
CHANGED
|
@@ -106,15 +106,20 @@ function maskKey(key) {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
/**
|
|
109
|
-
* Classify a memory-endpoint failure: is this the
|
|
110
|
-
* offline? aegis1 answers HTTP 402 `free_session_limit_reached`
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
109
|
+
* Classify a memory-endpoint failure: is this the plan's sync quota, or is it
|
|
110
|
+
* just offline? aegis1 answers HTTP 402 `free_session_limit_reached` when a
|
|
111
|
+
* WRITE would exceed the plan's synced-token ceiling (free = 1M, pro = 10M).
|
|
112
|
+
* Since the quota became token-denominated, reads (search/pull) are always
|
|
113
|
+
* served over quota, so only pushes can answer 402 — a pull that 402s is an
|
|
114
|
+
* older server.
|
|
115
|
+
*
|
|
116
|
+
* The shared client attaches `err.status` / `err.data` (vendor/aegis.js
|
|
117
|
+
* parseResponse) — but `ipcRenderer.invoke` only carries the *message string*
|
|
118
|
+
* across the process boundary. A thrown cap therefore reaches the renderer as
|
|
119
|
+
* the bare text "free_session_limit_reached" with `status`/`data` stripped,
|
|
120
|
+
* which is why the upgrade UI in fetchMemory() never fired. So: detect it here,
|
|
121
|
+
* in main, where the fields still exist, and hand the renderer a plain resolved
|
|
122
|
+
* payload.
|
|
118
123
|
*
|
|
119
124
|
* Returns null for anything that is not a cap (offline, no key, 500, …).
|
|
120
125
|
*/
|
|
@@ -123,10 +128,19 @@ function upgradeInfo(err) {
|
|
|
123
128
|
const data = (err && err.data) || {};
|
|
124
129
|
const code = typeof data.error === 'string' ? data.error : '';
|
|
125
130
|
if (status !== 402 && code !== 'free_session_limit_reached') return null;
|
|
131
|
+
// Quota numbers are TOKENS now (`tokensUsed` / `tokenLimit`). The legacy
|
|
132
|
+
// session-named keys are read only as a fallback so this build still shows a
|
|
133
|
+
// number against a server that predates the rename.
|
|
134
|
+
const used = data.tokensUsed != null
|
|
135
|
+
? data.tokensUsed
|
|
136
|
+
: (data.sessionsUsed != null ? data.sessionsUsed : null);
|
|
137
|
+
const limit = data.tokenLimit != null
|
|
138
|
+
? data.tokenLimit
|
|
139
|
+
: (data.freeSessionLimit != null ? data.freeSessionLimit : null);
|
|
126
140
|
return {
|
|
127
141
|
url: data.upgradeUrl || 'https://aegiscloud.org/subscribe',
|
|
128
|
-
used
|
|
129
|
-
limit
|
|
142
|
+
used,
|
|
143
|
+
limit,
|
|
130
144
|
code: code || 'free_session_limit_reached',
|
|
131
145
|
};
|
|
132
146
|
}
|
|
@@ -164,14 +178,22 @@ async function saveMemoryWithQueue(aegis, dir, entry) {
|
|
|
164
178
|
}
|
|
165
179
|
|
|
166
180
|
/**
|
|
167
|
-
* Read side of the same normalisation.
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
181
|
+
* Read side of the same normalisation. Since the sync quota became
|
|
182
|
+
* token-denominated, aegis1 no longer refuses reads over quota — it serves
|
|
183
|
+
* `memorySearch` / `memoryList` and reports `tokensUsed` / `tokenLimit` in the
|
|
184
|
+
* payload. So an exhausted account arrives as a *success*, and the notice has
|
|
185
|
+
* to be derived from the fields rather than from a thrown 402. The 402 branch
|
|
186
|
+
* stays for an older server that still caps reads.
|
|
187
|
+
*
|
|
188
|
+
* Both paths resolve the same `upgrade` shape so the renderer keeps one branch.
|
|
171
189
|
*/
|
|
172
190
|
async function normalizeMemoryRead(promise) {
|
|
173
191
|
try {
|
|
174
|
-
|
|
192
|
+
const data = await promise;
|
|
193
|
+
const quota = quotaFromPayload(data);
|
|
194
|
+
// Entries are deliberately preserved: an over-quota account still owns its
|
|
195
|
+
// memory and must see it. Only the notice is added.
|
|
196
|
+
return quota ? Object.assign({}, data, { upgrade: quota }) : data;
|
|
175
197
|
} catch (err) {
|
|
176
198
|
const upgrade = upgradeInfo(err);
|
|
177
199
|
if (!upgrade) throw err;
|
|
@@ -179,6 +201,22 @@ async function normalizeMemoryRead(promise) {
|
|
|
179
201
|
}
|
|
180
202
|
}
|
|
181
203
|
|
|
204
|
+
/** Over-quota notice derived from a successful read payload, or null.
|
|
205
|
+
* `tokensUsed`/`tokenLimit` come from aegis1 `_token_quota_fields`. */
|
|
206
|
+
function quotaFromPayload(data) {
|
|
207
|
+
if (!data || typeof data !== 'object') return null;
|
|
208
|
+
const used = data.tokensUsed != null ? data.tokensUsed : null;
|
|
209
|
+
const limit = data.tokenLimit != null ? data.tokenLimit : null;
|
|
210
|
+
if (used == null || limit == null || limit <= 0) return null;
|
|
211
|
+
if (used < limit) return null;
|
|
212
|
+
return {
|
|
213
|
+
url: 'https://aegiscloud.org/subscribe',
|
|
214
|
+
used,
|
|
215
|
+
limit,
|
|
216
|
+
code: 'sync_quota_reached',
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
182
220
|
/**
|
|
183
221
|
* `aegis:memoryImport` — scan this machine for other AI tools' memory and,
|
|
184
222
|
* when confirmed, push it into AEGIS cloud memory.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aegis-desktop",
|
|
3
3
|
"productName": "AEGIS Desktop",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.1",
|
|
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/renderer/app.js
CHANGED
|
@@ -160,6 +160,13 @@ const RESERVED_PROVIDERS = new Set(['__aegis', 'aegis']);
|
|
|
160
160
|
|
|
161
161
|
let pendingEl = null;
|
|
162
162
|
let pendingSessionId = null;
|
|
163
|
+
// The active thread's session id + prior turns. Both used to reset only on
|
|
164
|
+
// "New Chat" / opening a different session — NOT on every send() — so the
|
|
165
|
+
// model actually sees what was said earlier in the same open conversation
|
|
166
|
+
// instead of starting from a blank slate on every message (the root cause
|
|
167
|
+
// of "the model doesn't remember anything I just said").
|
|
168
|
+
let currentSessionId = null;
|
|
169
|
+
let threadMessages = [];
|
|
163
170
|
let classOptions = [];
|
|
164
171
|
let modelMeta = new Map(); // model id -> raw model object from listModels() (P2 §6.3 ceiling)
|
|
165
172
|
|
|
@@ -444,13 +451,17 @@ async function fetchMemory(query, limit) {
|
|
|
444
451
|
// `{ entries: [], upgrade }` instead. Checking the resolved payload first
|
|
445
452
|
// is what makes the upgrade UI reachable at all — the catch below only
|
|
446
453
|
// covers a non-IPC caller that still throws the raw client error.
|
|
454
|
+
// An over-quota account arrives as a SUCCESS with `upgrade` attached (reads
|
|
455
|
+
// are served over quota — main.js quotaFromPayload), so the entries must
|
|
456
|
+
// render alongside the notice. Returning `{entries: []}` here is only right
|
|
457
|
+
// for the 402 path, where there is genuinely nothing to show.
|
|
458
|
+
const raw = data && (data.entries || data.results);
|
|
459
|
+
const entries = Array.isArray(raw) ? raw.map(normalizeMemoryEntry) : [];
|
|
447
460
|
if (data && data.upgrade) {
|
|
448
|
-
return { entries
|
|
461
|
+
return { entries, error: '', upgrade: data.upgrade };
|
|
449
462
|
}
|
|
450
463
|
// `entries` is the real key. Keep the `results` fallback only so an older
|
|
451
464
|
// backend that still sends it degrades to a working list, not an empty one.
|
|
452
|
-
const raw = data && (data.entries || data.results);
|
|
453
|
-
const entries = Array.isArray(raw) ? raw.map(normalizeMemoryEntry) : [];
|
|
454
465
|
return { entries, error: '', upgrade: null };
|
|
455
466
|
} catch (err) {
|
|
456
467
|
const status = err && err.status;
|
|
@@ -461,8 +472,8 @@ async function fetchMemory(query, limit) {
|
|
|
461
472
|
error: '',
|
|
462
473
|
upgrade: {
|
|
463
474
|
url: (err.data && err.data.upgradeUrl) || 'https://aegiscloud.org/subscribe',
|
|
464
|
-
used: err.data && err.data.sessionsUsed,
|
|
465
|
-
limit: err.data && err.data.freeSessionLimit,
|
|
475
|
+
used: err.data && (err.data.tokensUsed != null ? err.data.tokensUsed : err.data.sessionsUsed),
|
|
476
|
+
limit: err.data && (err.data.tokenLimit != null ? err.data.tokenLimit : err.data.freeSessionLimit),
|
|
466
477
|
},
|
|
467
478
|
};
|
|
468
479
|
}
|
|
@@ -479,12 +490,24 @@ async function fetchMemory(query, limit) {
|
|
|
479
490
|
* inspector's block. The hint elements are bare <p>s, so the subscribe link
|
|
480
491
|
* has to be a real child node — a plain text assignment would wipe it, and an
|
|
481
492
|
* href left in text is not clickable. */
|
|
493
|
+
/** Human token count for the sync quota: 10000000 -> "10M", 42800 -> "43k".
|
|
494
|
+
* The quota is denominated in tokens (aegis1 FREE_SYNC_TOKENS/PRO_SYNC_TOKENS),
|
|
495
|
+
* which for stored prose is about one per character — rendering the raw
|
|
496
|
+
* integer ("10000000") tells a user nothing at a glance. */
|
|
497
|
+
function formatTokens(n) {
|
|
498
|
+
if (n == null || isNaN(n)) return '?';
|
|
499
|
+
const v = Number(n);
|
|
500
|
+
if (v >= 1e6) return `${(v / 1e6).toFixed(v % 1e6 === 0 ? 0 : 1)}M`;
|
|
501
|
+
if (v >= 1e4) return `${Math.round(v / 1e3)}k`;
|
|
502
|
+
return v.toLocaleString('en-US');
|
|
503
|
+
}
|
|
504
|
+
|
|
482
505
|
function capNotice(el, upgrade, prefix, cta) {
|
|
483
506
|
if (!el) return;
|
|
484
|
-
const used = upgrade.used != null ? upgrade.used : '?';
|
|
485
|
-
const cap = upgrade.limit != null ? upgrade.limit : '?';
|
|
507
|
+
const used = upgrade.used != null ? formatTokens(upgrade.used) : '?';
|
|
508
|
+
const cap = upgrade.limit != null ? formatTokens(upgrade.limit) : '?';
|
|
486
509
|
el.textContent =
|
|
487
|
-
`${prefix || '
|
|
510
|
+
`${prefix || 'sync limit reached'} — ${used} of ${cap} tokens synced. ` +
|
|
488
511
|
'Nothing was lost. ';
|
|
489
512
|
const a = document.createElement('a');
|
|
490
513
|
a.href = upgrade.url || 'https://aegiscloud.org/subscribe';
|
|
@@ -683,11 +706,11 @@ function renderMemoryOverlay() {
|
|
|
683
706
|
const h = document.createElement('strong');
|
|
684
707
|
h.textContent = 'Cloud memory is paused on the free plan';
|
|
685
708
|
const p = document.createElement('span');
|
|
686
|
-
const used = memoryView.upgrade.used != null ? memoryView.upgrade.used : '?';
|
|
687
|
-
const cap = memoryView.upgrade.limit != null ? memoryView.upgrade.limit : '?';
|
|
709
|
+
const used = memoryView.upgrade.used != null ? formatTokens(memoryView.upgrade.used) : '?';
|
|
710
|
+
const cap = memoryView.upgrade.limit != null ? formatTokens(memoryView.upgrade.limit) : '?';
|
|
688
711
|
p.textContent =
|
|
689
|
-
`This account has used ${used} of ${cap}
|
|
690
|
-
'
|
|
712
|
+
`This account has used ${used} of ${cap} tokens of synced conversation. ` +
|
|
713
|
+
'Saved memory is still readable — only new saves are paused until there is room again.';
|
|
691
714
|
const a = document.createElement('a');
|
|
692
715
|
a.href = memoryView.upgrade.url;
|
|
693
716
|
a.target = '_blank';
|
|
@@ -1703,6 +1726,13 @@ function openSession(id) {
|
|
|
1703
1726
|
: 'user';
|
|
1704
1727
|
addMessage(role, m.content || m.text || '', undefined, role === 'assistant' ? s.id : undefined);
|
|
1705
1728
|
}
|
|
1729
|
+
// Resuming a past conversation must resume its context too, not just
|
|
1730
|
+
// its on-screen transcript — continuing it as sessionId reuses the same
|
|
1731
|
+
// id and threadMessages carries the prior turns into the next send().
|
|
1732
|
+
currentSessionId = s.id;
|
|
1733
|
+
threadMessages = msgs
|
|
1734
|
+
.filter((m) => m.role === 'user' || m.role === 'assistant')
|
|
1735
|
+
.map((m) => ({ role: m.role, content: m.content || m.text || '' }));
|
|
1706
1736
|
els.sessionsHint.textContent = `opened ${s.id.slice(0, 8)}…`;
|
|
1707
1737
|
})
|
|
1708
1738
|
.catch((err) => {
|
|
@@ -1717,6 +1747,8 @@ function newChat() {
|
|
|
1717
1747
|
els.sessionsHint.textContent = '';
|
|
1718
1748
|
pendingEl = null;
|
|
1719
1749
|
pendingSessionId = null;
|
|
1750
|
+
currentSessionId = null;
|
|
1751
|
+
threadMessages = [];
|
|
1720
1752
|
flowCount = 0;
|
|
1721
1753
|
}
|
|
1722
1754
|
|
|
@@ -1750,9 +1782,19 @@ async function send() {
|
|
|
1750
1782
|
// clamping this feeds.
|
|
1751
1783
|
const effort = autonomous ? els.autonomousEffort.value : undefined;
|
|
1752
1784
|
const workers = autonomous ? parseInt(els.autonomousWorkers.value, 10) || undefined : undefined;
|
|
1753
|
-
|
|
1785
|
+
// Reuse the open thread's session id (minted once, on its first message)
|
|
1786
|
+
// instead of a fresh one per send — a new id every turn is what made both
|
|
1787
|
+
// the local `messages` history below and the cloud class's server-side
|
|
1788
|
+
// session memory reset on every single message.
|
|
1789
|
+
if (!currentSessionId) currentSessionId = newSessionId();
|
|
1790
|
+
const sessionId = currentSessionId;
|
|
1754
1791
|
pendingSessionId = sessionId;
|
|
1755
1792
|
|
|
1793
|
+
// Snapshot prior turns for the model — the new prompt travels separately
|
|
1794
|
+
// as `prompt` and providers.js appends it after `messages` on the wire.
|
|
1795
|
+
const historyForModel = threadMessages.slice();
|
|
1796
|
+
threadMessages.push({ role: 'user', content: prompt });
|
|
1797
|
+
|
|
1756
1798
|
setBusy(true, { cancellable: true });
|
|
1757
1799
|
|
|
1758
1800
|
// Persist the user turn locally (best-effort — never blocks chat).
|
|
@@ -1789,7 +1831,7 @@ async function send() {
|
|
|
1789
1831
|
// All four classes route through the model: surface (the main process
|
|
1790
1832
|
// decides transport — cloud client, ollama, or a direct provider).
|
|
1791
1833
|
const data = await models.chat(
|
|
1792
|
-
{ class: cls, prompt, model, maxTokens, sessionId, autonomous, effort, workers },
|
|
1834
|
+
{ class: cls, prompt, model, maxTokens, sessionId, autonomous, effort, workers, messages: historyForModel },
|
|
1793
1835
|
onDelta
|
|
1794
1836
|
);
|
|
1795
1837
|
|
|
@@ -1798,6 +1840,7 @@ async function send() {
|
|
|
1798
1840
|
(choice.message && choice.message.content) ||
|
|
1799
1841
|
streamedText ||
|
|
1800
1842
|
'(empty response)';
|
|
1843
|
+
threadMessages.push({ role: 'assistant', content: text });
|
|
1801
1844
|
const bits = [];
|
|
1802
1845
|
if (data && data.model) bits.push(`model: ${data.model}`);
|
|
1803
1846
|
else if (model) bits.push(`model: ${model}`);
|