aegis-desktop 0.8.5 → 0.8.6
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/README.md +15 -25
- package/docs/demo.gif +0 -0
- package/docs/marketing-assets/01-model-class-picker.png +0 -0
- package/docs/marketing-assets/02-answer-complete.png +0 -0
- package/docs/marketing-assets/03-tool-approval-diff.png +0 -0
- package/docs/screenshot.png +0 -0
- package/lib/avatar/companion.js +2 -2
- package/lib/avatar/cosmetics.js +2 -2
- package/lib/avatar/events.js +2 -2
- package/lib/avatar/holder-turn.js +6 -7
- package/lib/avatar/holders.js +6 -7
- package/lib/avatar/identity.js +7 -7
- package/lib/avatar/level.js +1 -1
- package/lib/avatar/mirror.js +6 -7
- package/lib/avatar/packs/README.md +1 -1
- package/lib/avatar/persona.js +5 -5
- package/lib/avatar/profile.js +13 -14
- package/lib/avatar/register.js +2 -2
- package/lib/avatar/store.js +6 -6
- package/lib/avatar/transfer.js +1 -2
- package/lib/avatar/turn.js +4 -4
- package/lib/local/agents.js +1 -1
- package/lib/local/autonomous.js +1 -1
- package/lib/local/engine.js +9 -9
- package/lib/local/git-scope.js +1 -1
- package/lib/local/prompt.js +3 -3
- package/lib/local/queue.js +2 -2
- package/lib/local/shell.js +1 -1
- package/lib/local/tools.js +2 -2
- package/lib/local/turn-guard.js +1 -1
- package/lib/local/worktree-lock.js +1 -1
- package/lib/settings.js +3 -3
- package/main.js +4 -4
- package/package.json +4 -8
- package/renderer/app.js +4 -4
- package/renderer/avatar/assemble.js +1 -1
- package/renderer/avatar/avatar.css +2 -2
- package/renderer/avatar/avatar.js +5 -5
- package/renderer/avatar/cards.js +1 -2
- package/renderer/avatar/hud.js +1 -2
- package/renderer/avatar/machine.js +1 -3
- package/renderer/avatar/pane.js +3 -3
- package/renderer/avatar/parts.js +4 -4
- package/renderer/budget.js +2 -2
- package/renderer/index.html +1 -1
- package/renderer/transcript-view.js +1 -1
- package/renderer/usage.js +6 -6
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AEGIS Desktop
|
|
2
2
|
|
|
3
|
-

|
|
4
4
|
|
|
5
5
|
A standalone Electron chat app over the [AEGIS](https://aegiscloud.org) API,
|
|
6
6
|
with an **agentic tool loop**: the model can read, write, and edit files,
|
|
@@ -9,9 +9,8 @@ delegate whole sub-tasks to subagents. It does **not** require Claude Code.
|
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
repo the release workflow publishes binaries to — or install from npm:
|
|
12
|
+
Prebuilt binaries (AppImage / MSI+NSIS / dmg) ship with each release, and the
|
|
13
|
+
package is on npm:
|
|
15
14
|
|
|
16
15
|
```bash
|
|
17
16
|
npm install -g aegis-desktop # requires Node 18+
|
|
@@ -114,16 +113,16 @@ stub and refuse to publish an asset that fails their assertions.
|
|
|
114
113
|
|
|
115
114
|
**The model-class picker** — all three routes, switchable mid-conversation:
|
|
116
115
|
|
|
117
|
-

|
|
118
117
|
|
|
119
118
|
**A completed answer** in the transcript:
|
|
120
119
|
|
|
121
|
-

|
|
122
121
|
|
|
123
122
|
**The tool-call approval card** with a proposed edit — what blocks `exec`,
|
|
124
123
|
`writeFile` and `editFile` until you allow, allow for the session, or deny:
|
|
125
124
|
|
|
126
|
-

|
|
127
126
|
|
|
128
127
|
## Tools available to the model
|
|
129
128
|
|
|
@@ -156,7 +155,7 @@ aegiscode autonomous add "fix the flaky retry test" --cwd ~/repo --commit
|
|
|
156
155
|
aegiscode autonomous list
|
|
157
156
|
aegiscode autonomous run # drain one task, then stop
|
|
158
157
|
aegiscode autonomous proceed --max 3 # drain up to three
|
|
159
|
-
aegiscode autonomous reconcile --auto # queue the next unfinished
|
|
158
|
+
aegiscode autonomous reconcile --auto # queue the next unfinished phase of your plan file, then drain it
|
|
160
159
|
aegiscode autonomous retry <id> # put a finished task back
|
|
161
160
|
aegiscode autonomous clear --all # empty the queue
|
|
162
161
|
```
|
|
@@ -278,17 +277,12 @@ The app registers an `aegis://` protocol handler:
|
|
|
278
277
|
|
|
279
278
|
## Run from source
|
|
280
279
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
npm install
|
|
285
|
-
npm start
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
Or, from the monorepo checkout, run this directory directly:
|
|
280
|
+
A source checkout is available to contributors with repository access only — the
|
|
281
|
+
desktop host is not a public repository. From the checkout, run this directory
|
|
282
|
+
directly:
|
|
289
283
|
|
|
290
284
|
```bash
|
|
291
|
-
cd
|
|
285
|
+
cd desktop
|
|
292
286
|
npm install
|
|
293
287
|
npm start
|
|
294
288
|
```
|
|
@@ -328,11 +322,7 @@ transport over the same AEGIS backend — see the repo root for that fuller
|
|
|
328
322
|
architecture picture, and [cli/README.md](../cli/README.md) for the terminal
|
|
329
323
|
host over the same engine.
|
|
330
324
|
|
|
331
|
-
**
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
cloned and built on its own. Edits land here and are synced out; nothing is
|
|
336
|
-
authored there. The npm package
|
|
337
|
-
[`aegis-desktop`](https://www.npmjs.com/package/aegis-desktop) is built from
|
|
338
|
-
that standalone repo.
|
|
325
|
+
**One source of truth.** This directory is it. The npm package
|
|
326
|
+
[`aegis-desktop`](https://www.npmjs.com/package/aegis-desktop) is built from a
|
|
327
|
+
`git subtree split` of it — same code, published without the rest of the
|
|
328
|
+
monorepo. Edits land here and are synced out; nothing is authored elsewhere.
|
package/docs/demo.gif
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/avatar/companion.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* companion.js — the four companion behaviours
|
|
4
|
+
* companion.js — the four companion behaviours.
|
|
5
5
|
*
|
|
6
6
|
* The behaviours are the morning brief, the idea log, the session reflection
|
|
7
7
|
* and in-session recall. What they have in common is not their content, it is
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
* checkable: nothing here may be a shell, a network call or anything that sends
|
|
43
43
|
* data off the device, and `carveOuts()` is the assertion that says so.
|
|
44
44
|
*
|
|
45
|
-
* Pure: no `fs`, no Electron, no globals.
|
|
45
|
+
* Pure: no `fs`, no Electron, no globals.
|
|
46
46
|
*/
|
|
47
47
|
|
|
48
48
|
const level = require('./level.js');
|
package/lib/avatar/cosmetics.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* cosmetics.js — the cosmetics pack format and its validator
|
|
4
|
+
* cosmetics.js — the cosmetics pack format and its validator.
|
|
5
5
|
*
|
|
6
6
|
* WHY A FORMAT AT ALL. Spec §3 says the avatar is *data, not art*: a persona is
|
|
7
7
|
* one JSON record and a look is a layered SVG assembled from a part manifest, so
|
|
@@ -196,7 +196,7 @@ function validatePack(raw) {
|
|
|
196
196
|
for (const where of forbidden) {
|
|
197
197
|
errors.push(
|
|
198
198
|
`cosmetics packs may not carry \`${where}\` — a pack sells LOOKS only, never XP, ` +
|
|
199
|
-
'levels, recall breadth, approval friction or personalization
|
|
199
|
+
'levels, recall breadth, approval friction or personalization',
|
|
200
200
|
);
|
|
201
201
|
}
|
|
202
202
|
|
package/lib/avatar/events.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* that throws on an unrecognised signal turns every future feature into a
|
|
18
18
|
* crash in the avatar, so the default transition is "stay put".
|
|
19
19
|
*
|
|
20
|
-
* The states are
|
|
20
|
+
* The states are wired to signals that
|
|
21
21
|
* already exist: turn start/stream/end, `tool.*` from the tool host, the
|
|
22
22
|
* approval card, queue progress, and sync status. No new event bus.
|
|
23
23
|
*
|
|
@@ -175,7 +175,7 @@ const TABLE = Object.freeze({
|
|
|
175
175
|
|
|
176
176
|
/**
|
|
177
177
|
* Expression id per state. The renderer maps these to the persona's expression
|
|
178
|
-
* pack; keeping the mapping here means
|
|
178
|
+
* pack; keeping the mapping here means the renderer needs no state knowledge.
|
|
179
179
|
*/
|
|
180
180
|
const EXPRESSIONS = Object.freeze({
|
|
181
181
|
idle: 'neutral',
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* holder-turn.js — the seam where a holder reaches turn assembly
|
|
5
|
-
* docs/avatar-identity-plan.md §3, §4, §7).
|
|
4
|
+
* holder-turn.js — the seam where a holder reaches turn assembly.
|
|
6
5
|
*
|
|
7
6
|
* `register.js` turns a persona into a bounded tone fragment, `profile.js` folds
|
|
8
7
|
* a holder's memory into evidence-gated facets, and `holders.js` owns the
|
|
9
8
|
* directory and the isolation filter. This file is the only place they meet, and
|
|
10
|
-
* it is where the one claim
|
|
9
|
+
* it is where the one claim this file is really about gets decided:
|
|
11
10
|
*
|
|
12
11
|
* **Holder A's assembled prompt cannot contain holder B's text.**
|
|
13
12
|
*
|
|
@@ -22,10 +21,10 @@
|
|
|
22
21
|
* - *The recall block.* The same filter runs on the recall rows, so an entry
|
|
23
22
|
* that arrives after the wrong cache still cannot be quoted into A's turn.
|
|
24
23
|
*
|
|
25
|
-
* The
|
|
24
|
+
* The test drives both through `assemble()` — the function main.js
|
|
26
25
|
* actually calls — and then through `engine.js` with this file's prompt builder
|
|
27
26
|
* injected, grepping the system prompt a scripted transport received. That is
|
|
28
|
-
* the
|
|
27
|
+
* the lesson (never assert on a harness leg that was re-pointed) turned
|
|
29
28
|
* on privacy: the interesting failure is a filter applied after the wrong cache,
|
|
30
29
|
* and only the real path can show it.
|
|
31
30
|
*
|
|
@@ -157,7 +156,7 @@ function profileFragment(fold, opts = {}) {
|
|
|
157
156
|
* rows. Exported because main.js renders it into the memory pane as well, and
|
|
158
157
|
* one implementation is one thing to get right.
|
|
159
158
|
*
|
|
160
|
-
* `opts.header` exists for the one caller that has no holder:
|
|
159
|
+
* `opts.header` exists for the one caller that has no holder: the account-level turn
|
|
161
160
|
* assembly recalls the account's memory before any holder exists, and captioning
|
|
162
161
|
* that block "this holder's memory" would be a lie told in the prompt. The
|
|
163
162
|
* default stays the holder header, so every holder-scoped caller is unchanged.
|
|
@@ -253,7 +252,7 @@ function assemble(input = {}) {
|
|
|
253
252
|
entryIds: scoped.map((row) => String(row && row.id)).filter(Boolean),
|
|
254
253
|
recallIds: recall.entryIds,
|
|
255
254
|
// Rows the recall block could not fit. Facet drops have their own field, so
|
|
256
|
-
// neither the usage accounting nor the
|
|
255
|
+
// neither the usage accounting nor the assertions have to guess
|
|
257
256
|
// which kind of "dropped" this is.
|
|
258
257
|
dropped: recall.dropped,
|
|
259
258
|
neutralizations: profile.neutralizations + recall.neutralizations + registerFragment.neutralizations,
|
package/lib/avatar/holders.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* holders.js — who holds the memory
|
|
5
|
-
* §3, §4, §5).
|
|
4
|
+
* holders.js — who holds the memory.
|
|
6
5
|
*
|
|
7
|
-
* `identity.js`
|
|
8
|
-
* refuses to merge two people on a fingerprint match. `profile.js`
|
|
6
|
+
* `identity.js` decides *which* holder a set of signals points at and
|
|
7
|
+
* refuses to merge two people on a fingerprint match. `profile.js`
|
|
9
8
|
* folds one holder's entries into bounded, evidence-gated facets. Neither of
|
|
10
9
|
* them owns a directory. This file is that owner — the layout, the registry, the
|
|
11
10
|
* switch, the conflict decision and the "why does it know this" surface — and it
|
|
@@ -32,7 +31,7 @@
|
|
|
32
31
|
* is the client-side filter §3 calls "the assertion that the fix happened":
|
|
33
32
|
* the server-side scope is the fix, and this is what proves it ran. It is
|
|
34
33
|
* used by the fold *and* by the recall block, so a leak needs to defeat one
|
|
35
|
-
* function rather than two call sites — and the
|
|
34
|
+
* function rather than two call sites — and the test drives two
|
|
36
35
|
* holders through the real turn assembly and greps the prompt.
|
|
37
36
|
*
|
|
38
37
|
* 4. **Every claim is traceable and deletable.** `provenance()` renders a
|
|
@@ -63,7 +62,7 @@ const PROFILE_FILE = 'profile.json';
|
|
|
63
62
|
const FORGOTTEN_FILE = 'forgotten.json';
|
|
64
63
|
|
|
65
64
|
/**
|
|
66
|
-
* The one thing a switch may leave behind. A list, not a sentence: the
|
|
65
|
+
* The one thing a switch may leave behind. A list, not a sentence: the
|
|
67
66
|
* test iterates it, so adding a survivor to the code without adding it here
|
|
68
67
|
* makes the test fail rather than the promise quietly rot.
|
|
69
68
|
*/
|
|
@@ -445,7 +444,7 @@ function applyConflict(registry, resolution, choice = {}) {
|
|
|
445
444
|
* particular and therefore to the holder asking, which is what makes a legacy
|
|
446
445
|
* unstamped ledger readable instead of invisible.
|
|
447
446
|
*
|
|
448
|
-
* `clientFilter:false` exists only so the
|
|
447
|
+
* `clientFilter:false` exists only so the isolation test can prove the
|
|
449
448
|
* filter is load-bearing by removing it and watching the leg go red. It is not
|
|
450
449
|
* reachable from any IPC handler (holder-ipc.js never passes it).
|
|
451
450
|
*/
|
package/lib/avatar/identity.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* identity.js — whose memory it is.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Memory is the value; value is meaningless
|
|
7
7
|
* unattributed. This module answers exactly one question — *which holder is at
|
|
8
8
|
* the keyboard* — from signals the caller already has, and it answers with a
|
|
9
9
|
* **proposal plus provenance**, never with a merge.
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
* 4. **Resolution is pure and never writes.** `holders.json` is validated,
|
|
39
39
|
* repaired and copied; the input registry is never mutated, and the fields
|
|
40
40
|
* `proposal`/`conflict`/`needsMint` exist precisely so main writes the file
|
|
41
|
-
* with the user's confirmation
|
|
41
|
+
* with the user's confirmation instead of this module guessing.
|
|
42
42
|
*
|
|
43
43
|
* Pure: no fs, no Electron, no crypto import — the caller supplies `hash` so
|
|
44
44
|
* this file is testable in plain Node exactly like `lib/sync/*`.
|
|
@@ -149,10 +149,10 @@ function looksLikeRawKey(value) {
|
|
|
149
149
|
*
|
|
150
150
|
* Deterministic on purpose — the same machine seed produces the same id on a
|
|
151
151
|
* reinstall, which is what keeps "the ledger survives a reinstall"
|
|
152
|
-
*
|
|
152
|
+
* true for an install that never had a cloud key.
|
|
153
153
|
*
|
|
154
154
|
* @returns {string|null} `null` when there is nothing trustworthy to mint from;
|
|
155
|
-
* the caller then falls back to random bytes
|
|
155
|
+
* the caller then falls back to random bytes.
|
|
156
156
|
*/
|
|
157
157
|
function mintHolderId(hash, seed) {
|
|
158
158
|
if (typeof hash !== 'function') return null;
|
|
@@ -424,7 +424,7 @@ function setActive(current, id) {
|
|
|
424
424
|
return validate(base);
|
|
425
425
|
}
|
|
426
426
|
|
|
427
|
-
/**
|
|
427
|
+
/** «forget»: drop one row, and re-point `active` if it was that row. */
|
|
428
428
|
function removeHolder(current, id) {
|
|
429
429
|
const base = validate(current).registry;
|
|
430
430
|
const wanted = holderIdShape(id);
|
|
@@ -475,7 +475,7 @@ const CONFIDENCE = Object.freeze({
|
|
|
475
475
|
* `conflict.a` is what you would be switching away from (the registry's
|
|
476
476
|
* active holder, or the losing signal's holder) and `conflict.b` is the
|
|
477
477
|
* proposal. Returned so main can show a "who is at the keyboard?" card
|
|
478
|
-
*
|
|
478
|
+
* instead of switching silently.
|
|
479
479
|
*/
|
|
480
480
|
function resolve(signals, registry) {
|
|
481
481
|
const raw = signals && typeof signals === 'object' ? signals : {};
|
|
@@ -519,7 +519,7 @@ function resolve(signals, registry) {
|
|
|
519
519
|
// A fingerprint that matches no registry row proves nothing about who this
|
|
520
520
|
// is, so it can never *select* a holder. It can only be proposed for
|
|
521
521
|
// binding onto the holder that WAS resolved, which main writes only after
|
|
522
|
-
// the user confirms
|
|
522
|
+
// the user confirms — this module never writes anything. Built
|
|
523
523
|
// here rather than in the last rung so every rung reports it identically:
|
|
524
524
|
// an explicit persona import with a freshly rotated key is the same
|
|
525
525
|
// question as a fresh install with one.
|
package/lib/avatar/level.js
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* they too cannot widen write/shell/network approval.
|
|
32
32
|
*
|
|
33
33
|
* Pure: no fs, no Electron, no process access except the `env` the caller
|
|
34
|
-
* passes.
|
|
34
|
+
* passes.
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
const { TIERS, tierFor, tierIndex } = require('./tiers.js');
|
package/lib/avatar/mirror.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* mirror.js — the per-holder cloud mirror
|
|
5
|
-
* docs/avatar-identity-plan.md §6, extending docs/avatar-plan.md §1.5).
|
|
4
|
+
* mirror.js — the per-holder cloud mirror.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Earlier work put the ledger on disk, made a profile foldable from it and
|
|
7
|
+
* gave every row a holder. All three are local. This file is the half
|
|
9
8
|
* that leaves the machine, and it exists to make four claims true — each of
|
|
10
9
|
* which is a test in `desktop/test/avatar-mirror.test.mjs`, not a paragraph:
|
|
11
10
|
*
|
|
@@ -22,8 +21,8 @@
|
|
|
22
21
|
* `reconcile()` unions two sides by a content-addressed row id, deduplicates
|
|
23
22
|
* a row both machines already have, and sorts by `(t, id)`. Two machines
|
|
24
23
|
* that appended in opposite orders converge on the *same bytes*, and
|
|
25
|
-
* `derive()`'s order independence
|
|
26
|
-
* same level. The
|
|
24
|
+
* `derive()`'s order independence makes that the
|
|
25
|
+
* same level. The ordering test feeds both interleavings and
|
|
27
26
|
* asserts one level; the idempotence leg asserts feeding the union back in
|
|
28
27
|
* cannot double a level, which is what would actually hurt.
|
|
29
28
|
*
|
|
@@ -337,7 +336,7 @@ function readMirror(memoryRows, holderId) {
|
|
|
337
336
|
* trusted as a conclusion — `payload.profile`/`payload.facets` are dropped
|
|
338
337
|
* explicitly and recorded in `refused`. The fold itself is `profile.fold()`
|
|
339
338
|
* with `holder` supplied, so a facet can only cite an entry id that survived
|
|
340
|
-
* the holder filter
|
|
339
|
+
* the holder filter *and* exists in this
|
|
341
340
|
* input (the `citable` set inside the fold).
|
|
342
341
|
*/
|
|
343
342
|
function refold(input = {}) {
|
|
@@ -23,7 +23,7 @@ paid.
|
|
|
23
23
|
## The rule this directory exists to enforce
|
|
24
24
|
|
|
25
25
|
**Cosmetics only.** Never XP, levels, recall breadth, approval friction or
|
|
26
|
-
personalization
|
|
26
|
+
personalization. `validatePack` refuses a
|
|
27
27
|
pack carrying `xp`, `level`, `recallEntries`, `approvals`, `toolGrants`,
|
|
28
28
|
`modelFloor`, `proactivity`, `price`, `entitlement`, … at any depth, and refuses
|
|
29
29
|
a `tier` that contradicts the unlock table — so a "paid pack that also widens a
|
package/lib/avatar/persona.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* that is a settings pane with hand-written branches for every option, which
|
|
8
8
|
* makes a new outfit a code change and a commissioned art pack a fork. The way
|
|
9
9
|
* this module does it: a persona is one JSON record, validated and coerced
|
|
10
|
-
* here, and
|
|
10
|
+
* here, and the renderer assembles layered SVG from a part manifest. A
|
|
11
11
|
* theme is then a file. Zero code changes.
|
|
12
12
|
*
|
|
13
13
|
* Validation is REPAIR-ORIENTED, not reject-oriented, with exactly two hard
|
|
@@ -23,20 +23,20 @@
|
|
|
23
23
|
* it is the user's own file.
|
|
24
24
|
*
|
|
25
25
|
* The one place that IS adversarial is the `register` block and the identity
|
|
26
|
-
* strings, because they end up in the system prompt
|
|
26
|
+
* strings, because they end up in the system prompt. Those are
|
|
27
27
|
* sanitized here at the storage boundary — control characters and newlines are
|
|
28
28
|
* stripped so a name cannot break out of its line — and the *prompt* fragment
|
|
29
29
|
* is additionally bounded and neutralized in `register.js`. Two layers, because
|
|
30
30
|
* a name is user-authored text going into a prompt.
|
|
31
31
|
*
|
|
32
|
-
* Pure: no fs, no Electron.
|
|
32
|
+
* Pure: no fs, no Electron. The main process reads/writes the file.
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
35
|
/** Bump when the record shape changes; `migrate` walks old records forward. */
|
|
36
36
|
const SCHEMA_VERSION = 1;
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
* The part manifest. Ids only — geometry lives in `renderer/avatar
|
|
39
|
+
* The part manifest. Ids only — geometry lives in `renderer/avatar/`,
|
|
40
40
|
* which reads these same ids. Validation is against THIS list so a typo in a
|
|
41
41
|
* hand-edited persona is caught at load rather than rendering an empty head.
|
|
42
42
|
*/
|
|
@@ -82,7 +82,7 @@ function has(list, value) {
|
|
|
82
82
|
|
|
83
83
|
/**
|
|
84
84
|
* Collapse whitespace and strip control characters. A name is a single line of
|
|
85
|
-
* text in a UI, and
|
|
85
|
+
* text in a UI, and a single line in a prompt — so newlines, tabs and
|
|
86
86
|
* the unicode line separators are removed rather than escaped.
|
|
87
87
|
*/
|
|
88
88
|
function cleanText(value, max) {
|
package/lib/avatar/profile.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* profile.js — what *this holder's* memory is allowed to personalise
|
|
5
|
-
* identity plan §4).
|
|
4
|
+
* profile.js — what *this holder's* memory is allowed to personalise.
|
|
6
5
|
*
|
|
7
6
|
* `identity.js` answers "whose memory is it"; this module answers the question
|
|
8
7
|
* that follows immediately: given that holder's own rows, what may the avatar
|
|
@@ -36,21 +35,21 @@
|
|
|
36
35
|
* Absence of evidence produces no facet: never "probably prefers terse
|
|
37
36
|
* answers".
|
|
38
37
|
* 4. COLD START IS NEUTRAL. No evidence ⇒ `coldStart: true`, `facets: []`,
|
|
39
|
-
* `budget.used: 0`.
|
|
38
|
+
* `budget.used: 0`. The renderer shows that as an empty fragment, i.e. an
|
|
40
39
|
* avatar that behaves exactly like an unconfigured install. It does not
|
|
41
|
-
* guess a personality
|
|
40
|
+
* guess a personality.
|
|
42
41
|
* 5. DETERMINISTIC. No clock (`now` is an argument), no randomness, no map
|
|
43
42
|
* iteration order leaking into output: facets are ordered by
|
|
44
43
|
* `(confidence desc, lastSeen desc, id asc)` and, for two facets of the
|
|
45
44
|
* same class on the same day, `key asc` — one extra term so the order is
|
|
46
45
|
* total and a permutation of the input cannot change the output bytes.
|
|
47
|
-
* 6. BOUNDED. The assembled facets are capped at `BUDGET_TOKENS` (900,
|
|
48
|
-
*
|
|
46
|
+
* 6. BOUNDED. The assembled facets are capped at `BUDGET_TOKENS` (900),
|
|
47
|
+
* dropped whole, lowest priority first — never sliced
|
|
49
48
|
* mid-sentence, which is the failure mode a naive `slice()` produces.
|
|
50
49
|
*
|
|
51
50
|
* Pure: no fs, no Electron, no clock, no crypto. It reuses `persona.cleanText`
|
|
52
51
|
* (the storage boundary's sanitizer) and `register.estimateTokens` (the same
|
|
53
|
-
* 4-chars-per-token ceiling
|
|
52
|
+
* 4-chars-per-token ceiling used elsewhere) so the two prompt fragments the renderer
|
|
54
53
|
* concatenates are measured with one ruler.
|
|
55
54
|
*/
|
|
56
55
|
|
|
@@ -73,7 +72,7 @@ const FACET_IDS = Object.freeze([
|
|
|
73
72
|
'toneCalibration',
|
|
74
73
|
]);
|
|
75
74
|
|
|
76
|
-
/**
|
|
75
|
+
/** The profile fragment's ceiling. */
|
|
77
76
|
const BUDGET_TOKENS = 900;
|
|
78
77
|
|
|
79
78
|
/** Longest a single facet `value` may be (it is UI text and prompt text). */
|
|
@@ -360,7 +359,7 @@ function orderFacets(facets) {
|
|
|
360
359
|
/**
|
|
361
360
|
* Take facets in priority order while they fit. Whole facets only: a fragment
|
|
362
361
|
* that ends mid-sentence reads as a truncated thought, which is worse than a
|
|
363
|
-
* shorter one, and
|
|
362
|
+
* shorter one, and the test asserts exactly this.
|
|
364
363
|
*/
|
|
365
364
|
function applyBudget(ordered, limit = BUDGET_TOKENS) {
|
|
366
365
|
const kept = [];
|
|
@@ -384,12 +383,12 @@ function applyBudget(ordered, limit = BUDGET_TOKENS) {
|
|
|
384
383
|
/**
|
|
385
384
|
* Does this row belong to the holder being folded?
|
|
386
385
|
*
|
|
387
|
-
* Unstamped rows belong to whoever is asking: they predate
|
|
386
|
+
* Unstamped rows belong to whoever is asking: they predate the stamping
|
|
388
387
|
* and the alternative — dropping them — would silently empty the profile of
|
|
389
388
|
* every existing install. A row stamped for *someone else* is never folded, and
|
|
390
389
|
* that is the pure-seam half of §3's "isolation is the load-bearing property":
|
|
391
390
|
* `sources` built downstream can only cite ids that survived this filter. The
|
|
392
|
-
* real-seam half
|
|
391
|
+
* real-seam half asserts it again through actual turn assembly,
|
|
393
392
|
* because a filter applied after the wrong cache is exactly the bug a
|
|
394
393
|
* pure-function test misses.
|
|
395
394
|
*/
|
|
@@ -844,9 +843,9 @@ if (Object.keys(FOLDERS).length !== FACET_IDS.length || FACET_IDS.some((id) => t
|
|
|
844
843
|
* @param {object} input
|
|
845
844
|
* @param {string} [input.holder] the holder these rows belong to. Supplying
|
|
846
845
|
* it is how a caller states *whose* memory this is: rows stamped for another
|
|
847
|
-
* holder are dropped here and can therefore never be cited by a facet.
|
|
848
|
-
*
|
|
849
|
-
* already scoped this input", which is why
|
|
846
|
+
* holder are dropped here and can therefore never be cited by a facet. The
|
|
847
|
+
* caller always passes it (the registry's `active`); omitting it means "the caller
|
|
848
|
+
* already scoped this input", which is why the test asserts the same property
|
|
850
849
|
* again at the real turn-assembly seam.
|
|
851
850
|
* @param {Array} [input.entries] memory entries for this holder, aegis1's
|
|
852
851
|
* row shape: `{ id, content, role, tags: [], session, createdAt, … }`.
|
package/lib/avatar/register.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* register.js — the persona's register turned into ONE bounded system-prompt
|
|
5
|
-
* fragment
|
|
5
|
+
* fragment.
|
|
6
6
|
*
|
|
7
7
|
* `persona.js` is the storage boundary: it sanitizes identity/register text on
|
|
8
8
|
* the way to disk. This is the SECOND layer, and the one that matters at the
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
|
|
39
39
|
const personaModule = require('./persona.js');
|
|
40
40
|
|
|
41
|
-
/** ~300 tokens
|
|
41
|
+
/** ~300 tokens. */
|
|
42
42
|
const MAX_FRAGMENT_TOKENS = 300;
|
|
43
43
|
|
|
44
44
|
/**
|
package/lib/avatar/store.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* store.js — the main process's half of level authority
|
|
4
|
+
* store.js — the main process's half of level authority.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* The avatar core shipped as five pure modules with no IO at all:
|
|
7
7
|
* `xp.js` knows what an event is worth and how a ledger replays into a level,
|
|
8
8
|
* `level.js` knows what a level buys, but neither can read a file or a clock.
|
|
9
9
|
* This file is the missing half and it deliberately owns *only* the two things
|
|
@@ -51,11 +51,11 @@ const personaModule = require('./persona.js');
|
|
|
51
51
|
const LEDGER_DIR = 'avatar';
|
|
52
52
|
const LEDGER_FILE = 'ledger.jsonl';
|
|
53
53
|
/**
|
|
54
|
-
* `<userData>/avatar/persona.json` — the persona DOCUMENT
|
|
54
|
+
* `<userData>/avatar/persona.json` — the persona DOCUMENT.
|
|
55
55
|
*
|
|
56
56
|
* It lives here, next to the ledger and not in settings.json, because a persona
|
|
57
57
|
* is a document rather than a preference: it is imported, exported, hand-edited
|
|
58
|
-
* and eventually synced per holder
|
|
58
|
+
* and eventually synced per holder, and every one of those paths
|
|
59
59
|
* wants validation on the way in. The `__avatar` settings namespace holds only
|
|
60
60
|
* the three switches (lib/settings.js), so a renderer can never write a persona
|
|
61
61
|
* through the settings surface.
|
|
@@ -206,7 +206,7 @@ function createAvatarStore({ dir, now = () => Date.now(), env = process.env, log
|
|
|
206
206
|
*
|
|
207
207
|
* `award()` above is the internal path: its callers are hooks that already
|
|
208
208
|
* know which kind they are recording. This is the externally reachable one
|
|
209
|
-
* (
|
|
209
|
+
* (`aegis:avatarAward`), so it is the one that must not be a
|
|
210
210
|
* generic "write a line into the ledger" bridge: an unknown kind is refused
|
|
211
211
|
* with a reason rather than written as a zero-XP line, which keeps the ledger
|
|
212
212
|
* a record of priced events only. `xp` is never accepted from the caller —
|
|
@@ -226,7 +226,7 @@ function createAvatarStore({ dir, now = () => Date.now(), env = process.env, log
|
|
|
226
226
|
return { ok: true, kind: name, xp: xp.xpFor(name), entry };
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
// --- persona document
|
|
229
|
+
// --- persona document ----------------------------------------
|
|
230
230
|
// The persona is read and written ONLY here, by main. Both directions run
|
|
231
231
|
// through persona.js validation, so a hand-edited or imported file is
|
|
232
232
|
// repaired with warnings rather than trusted, and the identity/register text
|
package/lib/avatar/transfer.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* transfer.js — persona export, persona import and per-holder forget
|
|
5
|
-
* (PLAN Phase 29; docs/avatar-identity-plan.md §5 and §7.3).
|
|
4
|
+
* transfer.js — persona export, persona import and per-holder forget.
|
|
6
5
|
*
|
|
7
6
|
* A persona is taste. Memory is not. This file exists to keep those two apart
|
|
8
7
|
* across the one boundary where they would otherwise merge — moving a persona
|
package/lib/avatar/turn.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* turn.js — the ONE seam where a level reaches turn assembly
|
|
4
|
+
* turn.js — the ONE seam where a level reaches turn assembly and
|
|
5
5
|
* where the persona's register fragment is attached to the engine's system
|
|
6
|
-
* prompt
|
|
6
|
+
* prompt.
|
|
7
7
|
*
|
|
8
8
|
* `level.js` says what a level buys and `register.js` says what the persona's
|
|
9
9
|
* tone is; neither of them knows what a turn is. This file is the only place
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* level that silently starts spending per turn is a billing surprise.
|
|
24
24
|
* - No approval, grant or tool field is produced here. Turn assembly reads a
|
|
25
25
|
* recall policy; it is structurally incapable of reading a widened
|
|
26
|
-
* permission out of it, which is what makes the
|
|
26
|
+
* permission out of it, which is what makes the carve-out test a
|
|
27
27
|
* statement about the code rather than a promise about the developers.
|
|
28
28
|
* - `applyModelFloor(model, policy)` turns the L20 floor into the model this
|
|
29
29
|
* turn actually travels on. It is a ROUTING constraint and nothing more: it
|
|
@@ -101,7 +101,7 @@ function recallPolicy(caps, opts = {}) {
|
|
|
101
101
|
* Ids that reason on their own, as this build can tell without a round trip:
|
|
102
102
|
*
|
|
103
103
|
* - DeepSeek's reasoning family — the same anchored set `engine.js` sizes its
|
|
104
|
-
* budget from (DEEPSEEK_REASONING_MODEL_RE, mirrored from
|
|
104
|
+
* budget from (DEEPSEEK_REASONING_MODEL_RE, mirrored from the upstream engine),
|
|
105
105
|
* because a model whose hidden chain-of-thought bills against its own output
|
|
106
106
|
* budget is by definition a model that reasons.
|
|
107
107
|
* - the pooled brain tier (`nexus-brain` and the `-smart`/`-neo` spellings the
|
package/lib/local/agents.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* agents.js — subagent prompt presets for the desktop `task` tool, ported
|
|
5
|
-
* from
|
|
5
|
+
* from the upstream engine reference (same roles, same synthesis presets).
|
|
6
6
|
* A preset is a system prompt: `task` runs it through the normal engine.chat
|
|
7
7
|
* loop as a nested turn (its own tool rounds, same model class), not a
|
|
8
8
|
* separate runtime, so the presets are pure prompt composition.
|
package/lib/local/autonomous.js
CHANGED
|
@@ -311,7 +311,7 @@ function resolveEffort({ effort, env, fanout } = {}) {
|
|
|
311
311
|
|
|
312
312
|
/**
|
|
313
313
|
* The operating directive injected as the autonomous turn's prompt preamble.
|
|
314
|
-
* Ported verbatim in spirit from
|
|
314
|
+
* Ported verbatim in spirit from the upstream engine reference so both
|
|
315
315
|
* clients behave the same; duplicated rather than vendored because the plugin
|
|
316
316
|
* hosts no ESM build of that module.
|
|
317
317
|
*/
|
package/lib/local/engine.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* chosen class is the user's explicit selection.
|
|
9
9
|
*
|
|
10
10
|
* Since the tool-calling port it ALSO owns the agent loop (the client half of
|
|
11
|
-
*
|
|
11
|
+
* the upstream engine's src/backend.js runProvider): every turn carries a real
|
|
12
12
|
* system prompt (prompt.js) and the builtin tool schemas (tools.js), and when
|
|
13
13
|
* a provider answers with tool calls the loop executes them in-process and
|
|
14
14
|
* feeds the results back for as many rounds as the model keeps calling tools
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* executor never has to be exposed to the renderer.
|
|
28
28
|
*
|
|
29
29
|
* Two turn-scoped resources ride along with the loop, mirroring
|
|
30
|
-
*
|
|
30
|
+
* the upstream engine's runProvider exactly:
|
|
31
31
|
* - a lazily-started ShellSession (shell.js) that the `exec` tool shares,
|
|
32
32
|
* so cd/export state persists across calls within one turn instead of
|
|
33
33
|
* each call spawning a fresh process;
|
|
@@ -159,7 +159,7 @@ function providerKeyFromEnv(provider) {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
/**
|
|
162
|
-
* Mirrors
|
|
162
|
+
* Mirrors the upstream engine's src/backend.js DEEPSEEK_REASONING_MODEL_RE +
|
|
163
163
|
* EFFORT_TOKEN_BUDGET verbatim. DeepSeek's reasoning models (deepseek-flash,
|
|
164
164
|
* i.e. "Flash 4.1", deepseek-v4-pro, the deprecated deepseek-reasoner, and the
|
|
165
165
|
* legacy v4-flash/v4.1-flash aliases some configs still carry) spend part of
|
|
@@ -170,7 +170,7 @@ function providerKeyFromEnv(provider) {
|
|
|
170
170
|
* tool calls, just a turn that "completes" with nothing to show for it (the
|
|
171
171
|
* empty-response bug). A user pointing the Custom OpenAI-compatible class
|
|
172
172
|
* straight at DeepSeek's API hits exactly this, so the request is sized by the
|
|
173
|
-
* same effort budget
|
|
173
|
+
* same effort budget the upstream engine uses for its own direct DeepSeek calls.
|
|
174
174
|
*
|
|
175
175
|
* desktop/renderer/budget.js carries the renderer's copy of these two
|
|
176
176
|
* constants plus budgetFor() below; test/budget.test.mjs requires both and
|
|
@@ -437,7 +437,7 @@ function createLocalEngine({
|
|
|
437
437
|
settings,
|
|
438
438
|
tools,
|
|
439
439
|
promptBuilder,
|
|
440
|
-
//
|
|
440
|
+
// The level's recall breadth, injected by the host that owns the
|
|
441
441
|
// avatar ledger (desktop/main.js builds one from lib/avatar/turn.js). It is a
|
|
442
442
|
// SEAM rather than a call into lib/avatar/* because this engine is shared with
|
|
443
443
|
// the CLI (cli/src/deps.js loads this file): the engine must not grow a
|
|
@@ -920,7 +920,7 @@ function createLocalEngine({
|
|
|
920
920
|
// that used to claim a figure for the pooled class is gone — see
|
|
921
921
|
// desktop/renderer/budget.js). Omitting the field is
|
|
922
922
|
// what tells aegis1 "no cap stated — let effort decide", the same
|
|
923
|
-
// contract
|
|
923
|
+
// contract the upstream engine sends.
|
|
924
924
|
//
|
|
925
925
|
// A cap the caller DID state is a different thing, and dropping it was
|
|
926
926
|
// a bug: aegis1 reads a body max_tokens as a ceiling over its ladder,
|
|
@@ -960,7 +960,7 @@ function createLocalEngine({
|
|
|
960
960
|
//
|
|
961
961
|
// Writing is still available, explicitly: the aegis_memory_save tool
|
|
962
962
|
// (mcp/tools.js; the CLI exposes it as /memory). Recall on every turn,
|
|
963
|
-
// store only what the user asks for — which is what
|
|
963
|
+
// store only what the user asks for — which is what the upstream engine's
|
|
964
964
|
// own cross-session memory does, and what this comment claimed to
|
|
965
965
|
// match while sending both halves.
|
|
966
966
|
//
|
|
@@ -1324,7 +1324,7 @@ function createLocalEngine({
|
|
|
1324
1324
|
let truncationRetried = false;
|
|
1325
1325
|
let synthesisDone = false;
|
|
1326
1326
|
|
|
1327
|
-
// Round cap —
|
|
1327
|
+
// Round cap — a bound the upstream engine has had all along and this engine
|
|
1328
1328
|
// did not. Removing the old fixed cap (12) was right in spirit and wrong
|
|
1329
1329
|
// in effect: it left the turn with NO horizon, and on 2026-09-15 the
|
|
1330
1330
|
// question "can you check the plan" ran ~70 rounds, grew the context
|
|
@@ -1333,7 +1333,7 @@ function createLocalEngine({
|
|
|
1333
1333
|
// re-sends the whole conversation, so an unbounded loop gets more
|
|
1334
1334
|
// expensive the longer it runs.
|
|
1335
1335
|
//
|
|
1336
|
-
// The numbers match
|
|
1336
|
+
// The numbers match the upstream engine's (src/autonomous.js) so both clients
|
|
1337
1337
|
// behave the same: 24 rounds for a chat turn, 40 for an autonomous one.
|
|
1338
1338
|
// Env-overridable for a deliberately long job.
|
|
1339
1339
|
// A stated horizon (env) still wins outright — an explicit number is the
|
package/lib/local/git-scope.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Minimal git worktree introspection: where the repo root is, and what is
|
|
5
5
|
* dirty right now with a content hash per path.
|
|
6
6
|
*
|
|
7
|
-
* Ported from
|
|
7
|
+
* Ported from the upstream engine reference (ESM → CommonJS). `turn-guard.js`
|
|
8
8
|
* uses `gitRoot` and `gitStatusSnapshot`; the queue now hosts the commit path
|
|
9
9
|
* too, so `foreignChanges` and `scopedCommit` are carried over as well — they
|
|
10
10
|
* were deliberately absent while nothing here queued work, because a copy of
|
package/lib/local/prompt.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* prompt.js — the desktop client's system prompt (client half of
|
|
5
|
-
*
|
|
5
|
+
* the upstream engine's tool calling).
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* The upstream engine sends a real persona on every provider turn
|
|
8
8
|
* (src/backend.js MAIN_CHAT_PROMPT plus a docs/operating-context.md block).
|
|
9
9
|
* The desktop renderer used to send nothing at all, so every model answered a
|
|
10
10
|
* bare user string with no identity, no work rules and no idea which machine
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* repo roots the host already knows (main.js), so the model stops asking.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
/** Identity + work rules. Ported from
|
|
26
|
+
/** Identity + work rules. Ported from the upstream engine reference. */
|
|
27
27
|
const MAIN_CHAT_PROMPT =
|
|
28
28
|
`You are Aegiscodex, a terminal coding assistant that works in the user's repository. ` +
|
|
29
29
|
`Help with software engineering tasks: read and reason about code, write and edit files, ` +
|
package/lib/local/queue.js
CHANGED
|
@@ -443,8 +443,8 @@ function parsePlanStatus(text) {
|
|
|
443
443
|
if (line.trim() === '' || /^\s+\S/.test(line)) continue;
|
|
444
444
|
inStatus = false; // anything else (e.g. '---') ends the block
|
|
445
445
|
}
|
|
446
|
-
// Every PLAN.md in these repos writes `## Phase
|
|
447
|
-
// the number). A trailing `## Phase
|
|
446
|
+
// Every PLAN.md in these repos writes `## Phase N ✅ — title` (✅ right after
|
|
447
|
+
// the number). A trailing `## Phase N — title ✅` is accepted too, because
|
|
448
448
|
// reconcile reads plans other sessions and agents wrote, and a heading that
|
|
449
449
|
// says it shipped means it shipped wherever the ✅ sits.
|
|
450
450
|
const hm = line.match(/^##\s*Phase\s+(\d+)\b/);
|
package/lib/local/shell.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* shell.js — a persistent shell session for the `exec` tool, ported from
|
|
5
|
-
*
|
|
5
|
+
* the upstream engine reference (client half of the same design).
|
|
6
6
|
*
|
|
7
7
|
* The desktop `exec` tool used to spawn ONE process per call: `cd /foo` in
|
|
8
8
|
* one turn had no effect on the next call, so a model that wanted to work
|
package/lib/local/tools.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* tools.js — the local tool layer for the desktop agent loop (client half of
|
|
5
|
-
*
|
|
5
|
+
* the upstream engine's tool calling).
|
|
6
6
|
*
|
|
7
|
-
* Two jobs, mirroring
|
|
7
|
+
* Two jobs, mirroring the upstream engine reference:
|
|
8
8
|
* 1. Tool schemas in the wire format each API family expects — Anthropic's
|
|
9
9
|
* `{name, description, input_schema}` vs OpenAI-compatible
|
|
10
10
|
* `{type:'function', function:{name, description, parameters}}`.
|
package/lib/local/turn-guard.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Turn-start foreign-write detection.
|
|
5
5
|
*
|
|
6
|
-
* Ported from
|
|
6
|
+
* Ported from the upstream engine reference (ESM → CommonJS).
|
|
7
7
|
*
|
|
8
8
|
* Bug this exists for: the commit-time scope check already knew how to tell
|
|
9
9
|
* "my changes" from "somebody else's in-flight work" — but only *after* a
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Cross-process lock on a git working tree.
|
|
5
5
|
*
|
|
6
|
-
* Ported from
|
|
6
|
+
* Ported from the upstream engine reference (ESM → CommonJS).
|
|
7
7
|
*
|
|
8
8
|
* Bug this exists for: nothing keyed on *the working tree*. A session-scoped
|
|
9
9
|
* lock only stops two processes resuming the SAME transcript, so three
|
package/lib/settings.js
CHANGED
|
@@ -74,13 +74,13 @@ const CONFIRM_MODE_NAMESPACE = '__confirmMode';
|
|
|
74
74
|
* what turns it off. Nothing secret lives here, so no encryption. */
|
|
75
75
|
const MEMORY_PERSIST_NAMESPACE = '__memoryPersist';
|
|
76
76
|
|
|
77
|
-
/** Reserved namespace for the avatar/companion preferences
|
|
77
|
+
/** Reserved namespace for the avatar/companion preferences:
|
|
78
78
|
* `{ enabled, showLevel, motion }` — app-level, not a provider, so it stays out
|
|
79
79
|
* of the provider CRUD surface for the same reason the AEGIS key does. The
|
|
80
80
|
* persona itself is deliberately NOT stored here: it is a document, not a
|
|
81
81
|
* preference, and it lives at `<userData>/avatar/persona.json`, read and
|
|
82
82
|
* written by main through lib/avatar/store.js so a hand-edited or imported
|
|
83
|
-
* persona is validated on the way in
|
|
83
|
+
* persona is validated on the way in.
|
|
84
84
|
* Defaults are the shipped behaviour: the avatar is ON, the level readout is
|
|
85
85
|
* shown, and motion follows the OS `prefers-reduced-motion` setting. */
|
|
86
86
|
const AVATAR_NAMESPACE = '__avatar';
|
|
@@ -374,7 +374,7 @@ function createSettingsStore({ dir, safeStorage } = {}) {
|
|
|
374
374
|
return memoryPersistState();
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
-
// --- Avatar: reserved namespace, plain preferences
|
|
377
|
+
// --- Avatar: reserved namespace, plain preferences ------------
|
|
378
378
|
// The companion's on/off switch, its level readout, and its motion mode.
|
|
379
379
|
// Nothing secret lives here, so no encryption — same reasoning as the quick
|
|
380
380
|
// launcher and memory-persist namespaces above.
|
package/main.js
CHANGED
|
@@ -85,7 +85,7 @@ const { createSettingsStore, isReservedNamespace } = require('./lib/settings.js'
|
|
|
85
85
|
const sessionStore = require('./lib/sync/sessions.js');
|
|
86
86
|
const memoryQueue = require('./lib/sync/memory-queue.js');
|
|
87
87
|
const persistGate = require('./lib/sync/persist-gate.js');
|
|
88
|
-
// Avatar level authority
|
|
88
|
+
// Avatar level authority. The ledger, the replay and the award
|
|
89
89
|
// hooks live here in main — a renderer-held counter is a suggestion, not a
|
|
90
90
|
// fact, the same reasoning persist-gate.js follows. The pure halves
|
|
91
91
|
// (xp.js/level.js/events.js/persona.js) are unchanged and stay IO-free; store.js
|
|
@@ -104,7 +104,7 @@ try {
|
|
|
104
104
|
} catch {
|
|
105
105
|
foreignMemory = require('./vendor/foreign-memory.js');
|
|
106
106
|
}
|
|
107
|
-
// Builtin tool executor for the agent loop (client half of
|
|
107
|
+
// Builtin tool executor for the agent loop (client half of the upstream engine's
|
|
108
108
|
// tool calling). MAIN-process only: it is reachable from the renderer solely
|
|
109
109
|
// through the whitelisted `tools:` IPC surface registered below — see the
|
|
110
110
|
// sandbox note on registerToolsIpc().
|
|
@@ -356,7 +356,7 @@ async function billingResult(run) {
|
|
|
356
356
|
* would not survive the IPC trip — and the entry is left un-stored so the
|
|
357
357
|
* renderer can keep it in the box and point at the subscribe page.
|
|
358
358
|
*
|
|
359
|
-
*
|
|
359
|
+
* Level authority. This is the memory path's front door, so it is
|
|
360
360
|
* the first of the award hooks. Two things happen before the write leaves:
|
|
361
361
|
*
|
|
362
362
|
* 1. The entry is *normalised* (store.js `normalizeMemoryEntry`). aegis1
|
|
@@ -450,7 +450,7 @@ function quotaFromPayload(data) {
|
|
|
450
450
|
* 1000 entries x 2 kB over IPC is pure waste, and the bodies are already
|
|
451
451
|
* either in the cloud or in the queue by the time this resolves.
|
|
452
452
|
*
|
|
453
|
-
*
|
|
453
|
+
* Each batch that the cloud accepted pays `memory.imported` per
|
|
454
454
|
* entry, keyed by the import's own source (foreign-memory.js collapses one
|
|
455
455
|
* source's entries into ONE `import:<source>` session — aegis1 meters distinct
|
|
456
456
|
* session values — so that value is the right per-source key). The replay caps
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aegis-desktop",
|
|
3
3
|
"productName": "AEGIS Desktop",
|
|
4
|
-
"version": "0.8.
|
|
5
|
-
"description": "Thin Electron host for AEGIS
|
|
4
|
+
"version": "0.8.6",
|
|
5
|
+
"description": "Thin Electron host for AEGIS \u2014 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",
|
|
8
8
|
"email": "nborneklint@gmail.com"
|
|
@@ -10,11 +10,6 @@
|
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"main": "main.js",
|
|
12
12
|
"homepage": "https://aegiscloud.org",
|
|
13
|
-
"repository": {
|
|
14
|
-
"type": "git",
|
|
15
|
-
"url": "git+https://github.com/aegisinfo/aegiscode-plugin.git",
|
|
16
|
-
"directory": "desktop"
|
|
17
|
-
},
|
|
18
13
|
"bin": {
|
|
19
14
|
"aegis": "bin/aegis.js"
|
|
20
15
|
},
|
|
@@ -25,7 +20,8 @@
|
|
|
25
20
|
"lib",
|
|
26
21
|
"vendor",
|
|
27
22
|
"bin",
|
|
28
|
-
"build"
|
|
23
|
+
"build",
|
|
24
|
+
"docs"
|
|
29
25
|
],
|
|
30
26
|
"scripts": {
|
|
31
27
|
"start": "electron .",
|
package/renderer/app.js
CHANGED
|
@@ -1330,7 +1330,7 @@ function handleQuickLauncherPush(payload) {
|
|
|
1330
1330
|
// timestamp, createdAt (epoch ms), source, role, tags[], content, session,
|
|
1331
1331
|
// importance, summary(bool), topics[], entities[], sentiment, tokenCount,
|
|
1332
1332
|
// embedding[]. There is no `tier` — L0–L3 is the local CLI engine's concept
|
|
1333
|
-
// (
|
|
1333
|
+
// (the upstream engine's memory module) and does not exist on the cloud rows, so
|
|
1334
1334
|
// source/role are the real provenance axes here.
|
|
1335
1335
|
//
|
|
1336
1336
|
// `embedding` is dropped on ingest: it is a raw float vector, sometimes
|
|
@@ -1771,7 +1771,7 @@ async function importMemory() {
|
|
|
1771
1771
|
els.memoryImportBtn.disabled = true;
|
|
1772
1772
|
els.memoryHint.textContent = 'scanning for other AI tool memory…';
|
|
1773
1773
|
try {
|
|
1774
|
-
//
|
|
1774
|
+
// Dry run. The scan is read-only against the foreign stores
|
|
1775
1775
|
// (client/foreign-memory.js never writes to them) and the user already
|
|
1776
1776
|
// asked for the import by clicking, so this runs unattended: no confirm
|
|
1777
1777
|
// prompt. The preview text left in the hint is the audit trail of what
|
|
@@ -1787,7 +1787,7 @@ async function importMemory() {
|
|
|
1787
1787
|
.map((s) => `${s.label}: ${s.count}`)
|
|
1788
1788
|
.join(' · ');
|
|
1789
1789
|
|
|
1790
|
-
//
|
|
1790
|
+
// The confirmed write, immediately.
|
|
1791
1791
|
els.memoryHint.textContent = `importing ${preview.totals.entries} entries — ${detail}`;
|
|
1792
1792
|
const result = await aegis.memoryImport({ confirm: true, limit: 1000 });
|
|
1793
1793
|
if (result && result.upgrade) {
|
|
@@ -3984,7 +3984,7 @@ async function init() {
|
|
|
3984
3984
|
transcript.attachScrollVeto();
|
|
3985
3985
|
// Read-only diagnostic surface for the headless smoke run
|
|
3986
3986
|
// (test/electron-smoke.mjs). Everything in this file lives inside the IIFE,
|
|
3987
|
-
// so an injected script cannot otherwise see the scroll veto — the
|
|
3987
|
+
// so an injected script cannot otherwise see the scroll veto — the
|
|
3988
3988
|
// harness read `transcript.isScrolledUp()` directly and silently got `null`,
|
|
3989
3989
|
// which made its veto assertion unfalsifiable. Exposes state only: no
|
|
3990
3990
|
// setters, nothing that can drive the UI. Frozen so a stray write in a test
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* assemble.js — persona JSON in, layered SVG out
|
|
4
|
+
* assemble.js — persona JSON in, layered SVG out.
|
|
5
5
|
*
|
|
6
6
|
* A pure string builder. It takes a validated persona record plus one
|
|
7
7
|
* expression id and returns the markup, the layer list it walked, and — the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* avatar.css — the avatar's presentation layer
|
|
1
|
+
/* avatar.css — the avatar's presentation layer.
|
|
2
2
|
*
|
|
3
3
|
* Motion is CSS only, and it is the ONE thing in this phase that can be turned
|
|
4
4
|
* off wholesale. Two switches do that, deliberately overlapping:
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
width: 132px;
|
|
36
36
|
height: auto;
|
|
37
37
|
/* The face is decoration: it must never intercept a pointer, a scroll or a
|
|
38
|
-
drag — the two behaviours
|
|
38
|
+
drag — the two protected behaviours start as input events. */
|
|
39
39
|
pointer-events: none;
|
|
40
40
|
user-select: none;
|
|
41
41
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* avatar.js — the mount: stage, HUD, pane, and the rules that keep the avatar
|
|
5
|
-
* out of everything else's way
|
|
5
|
+
* out of everything else's way.
|
|
6
6
|
*
|
|
7
7
|
* The three modules beside this one are pure or nearly so (`parts.js` is data,
|
|
8
8
|
* `assemble.js` builds a string, `hud.js` builds a view model, `machine.js`
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* 1. **Never blocks input.** No key listener is registered anywhere in
|
|
14
14
|
* `renderer/avatar/`, so the avatar cannot consume a keystroke — Escape
|
|
15
|
-
* still reaches
|
|
15
|
+
* still reaches the interrupt handler and typing still reaches the
|
|
16
16
|
* composer, whatever the avatar is doing. Painting is coalesced to at most
|
|
17
17
|
* one write per animation frame (`schedule()`), so a stream that emits
|
|
18
18
|
* fifty deltas in a frame still costs one repaint, and every entry point is
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
* function the storage layer uses, so a persona cannot opt out of the
|
|
34
34
|
* accessibility preference.
|
|
35
35
|
*
|
|
36
|
-
* Source resolution is deliberately forward-compatible: `window.avatar`
|
|
37
|
-
*
|
|
36
|
+
* Source resolution is deliberately forward-compatible: `window.avatar` is
|
|
37
|
+
* used when it exists, and `preview()` exists so the
|
|
38
38
|
* harnesses (and the customization pane, which previews edits before anything
|
|
39
39
|
* is written) can drive the avatar without it. Both are one code path — the
|
|
40
40
|
* preview state is an override layer over the real one, and it is labelled as
|
|
@@ -77,7 +77,7 @@ function safe(fn, fallback) {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
|
-
* The data source. `window.avatar` is the preload bridge
|
|
80
|
+
* The data source. `window.avatar` is the preload bridge the host will expose
|
|
81
81
|
* (`state()` returning `{ persona, projection, capabilities, warnings }`); when
|
|
82
82
|
* it is missing the avatar renders the honest unknown state rather than
|
|
83
83
|
* inventing numbers.
|
package/renderer/avatar/cards.js
CHANGED
package/renderer/avatar/hud.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* hud.js — the level HUD: XP bar, next unlock, and the honest held state
|
|
5
|
-
* (PLAN Phase 22, spec §1.4).
|
|
4
|
+
* hud.js — the level HUD: XP bar, next unlock, and the honest held state.
|
|
6
5
|
*
|
|
7
6
|
* The HUD is a *renderer*: it draws a projection main handed it and derives
|
|
8
7
|
* nothing. That is not a stylistic preference — §1.5 of the plan says a level is
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* machine.js — the avatar's face, driven by the engine's real signals
|
|
5
|
-
* (PLAN Phase 22: "expression state machine bound to `lib/avatar/events.js`
|
|
6
|
-
* signals").
|
|
4
|
+
* machine.js — the avatar's face, driven by the engine's real signals.
|
|
7
5
|
*
|
|
8
6
|
* The state machine itself is NOT re-implemented here. `lib/avatar/events.js`
|
|
9
7
|
* is a pure module with no timers and no DOM, and index.html loads that same
|
package/renderer/avatar/pane.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* pane.js — the customization pane
|
|
4
|
+
* pane.js — the customization pane.
|
|
5
5
|
*
|
|
6
6
|
* The whole pane is BUILT FROM THE MANIFEST. Every select is a list of
|
|
7
7
|
* `persona.PARTS` ids, every label comes from `parts.LABELS` (and falls back to
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
* 3. **Nothing here touches input routing.** No key handlers, no focus trap, no
|
|
27
27
|
* `preventDefault`, no modal. The pane is a plain block of form controls in
|
|
28
28
|
* the sidebar; opening or closing it cannot consume a keystroke meant for
|
|
29
|
-
* the composer, and the Escape-interrupt path stays exactly where
|
|
30
|
-
* put
|
|
29
|
+
* the composer, and the Escape-interrupt path stays exactly where it
|
|
30
|
+
* was put.
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
'use strict';
|
package/renderer/avatar/parts.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* parts.js — the layered SVG part manifest
|
|
4
|
+
* parts.js — the layered SVG part manifest.
|
|
5
5
|
*
|
|
6
6
|
* `lib/avatar/persona.js` says it plainly: "Ids only — geometry lives in
|
|
7
|
-
* `renderer/avatar
|
|
7
|
+
* `renderer/avatar/`, which reads these same ids." This is that
|
|
8
8
|
* geometry, and the sentence above is enforced rather than trusted: `IDS` below
|
|
9
9
|
* IS `persona.PARTS`, read from the shared module — this file declares no id
|
|
10
10
|
* list of its own. Adding `hair: 'mohawk-9'` to the persona schema whose
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* Everything here is DATA. There is no branch on a part id anywhere in the
|
|
16
16
|
* renderer: the assembler walks `LAYERS` in order and emits whatever shapes the
|
|
17
17
|
* manifest holds, so a commissioned art pack or a seasonal outfit is a file,
|
|
18
|
-
* not a fork. That is the whole point of the phase
|
|
19
|
-
*
|
|
18
|
+
* not a fork. That is the whole point of the phase: the avatar is data,
|
|
19
|
+
* not art.
|
|
20
20
|
*
|
|
21
21
|
* Two deliberate limits, stated here rather than discovered later:
|
|
22
22
|
*
|
package/renderer/budget.js
CHANGED
|
@@ -74,7 +74,7 @@ const DEEPSEEK_REASONING_MODEL_RE = /^deepseek-(v4(\.\d+)?-(flash|pro)|flash|pro
|
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
76
|
* Effort rung -> tokens. Mirrors EFFORT_TOKEN_BUDGET in
|
|
77
|
-
* desktop/lib/local/engine.js, which mirrors
|
|
77
|
+
* desktop/lib/local/engine.js, which mirrors the upstream engine's src/backend.js.
|
|
78
78
|
*
|
|
79
79
|
* These are the numbers the models whose output length cannot be predicted are
|
|
80
80
|
* sized by, which is why the rung replaced the dropdown: a rung is a
|
|
@@ -96,7 +96,7 @@ const EFFORT_TOKEN_BUDGET = { low: 8192, medium: 16384, high: 32768 };
|
|
|
96
96
|
*/
|
|
97
97
|
|
|
98
98
|
/** An unknown or "auto" rung falls to the top: the same default the engine
|
|
99
|
-
* and
|
|
99
|
+
* and the upstream engine apply, so "auto" cannot silently mean "smallest". */
|
|
100
100
|
function effortRung(effort) {
|
|
101
101
|
return effort === 'low' || effort === 'medium' ? effort : 'high';
|
|
102
102
|
}
|
package/renderer/index.html
CHANGED
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
|
|
130
130
|
So the budget is derived from this rung instead. The engine maps
|
|
131
131
|
low/medium/high to 8192/16384/32768 (EFFORT_TOKEN_BUDGET in
|
|
132
|
-
desktop/lib/local/engine.js, mirroring
|
|
132
|
+
desktop/lib/local/engine.js, mirroring the upstream engine's
|
|
133
133
|
src/backend.js), "auto" leaves the choice to the server, and no
|
|
134
134
|
max_tokens is stated by this app at all. The row is always on
|
|
135
135
|
screen: with the dropdown gone there is nothing to swap it for. -->
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* The DOM-touching half of the transcript policy
|
|
4
|
+
* The DOM-touching half of the transcript policy, extracted
|
|
5
5
|
* from app.js so the two symptoms this work exists to prevent can be asserted
|
|
6
6
|
* *behaviourally* — not just as pure math in stream-policy.js:
|
|
7
7
|
*
|
package/renderer/usage.js
CHANGED
|
@@ -68,11 +68,11 @@ function usageTokens(usage) {
|
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* Rough token count for text the wire never measured — a direct port of the
|
|
71
|
-
* CLI's own estimator (
|
|
71
|
+
* CLI's own estimator (the upstream engine's estimateTokens), and the
|
|
72
72
|
* reason its session total is MONOTONIC.
|
|
73
73
|
*
|
|
74
74
|
* This is the whole difference being fixed. The CLI's `appendHistory`
|
|
75
|
-
* (
|
|
75
|
+
* (the upstream engine's history module) writes a `tokens` object for EVERY finished
|
|
76
76
|
* exchange: `{input, output, cacheRead, cacheWrite, real: true}` when the wire
|
|
77
77
|
* reported usage, and `{input: estimateTokens(prompt), output:
|
|
78
78
|
* estimateTokens(reply), real: false}` when it did not. `real` is a FLAG, not a
|
|
@@ -477,7 +477,7 @@ function usageBuckets(usage) {
|
|
|
477
477
|
* Cache hit rate for a usage record: the share of prompt tokens the provider
|
|
478
478
|
* actually served from its cache rather than billing at miss price.
|
|
479
479
|
*
|
|
480
|
-
* Ported from
|
|
480
|
+
* Ported from the upstream engine reference. It reads through `usageBuckets`
|
|
481
481
|
* rather than off the raw object, which is the one deliberate difference: the
|
|
482
482
|
* engine's copy is handed an already-normalised record, while this module is
|
|
483
483
|
* also handed WIRE usage (a provider's raw response body) and ledger rows. A
|
|
@@ -588,7 +588,7 @@ function turnAccounting(usage, model, opts = {}) {
|
|
|
588
588
|
const tokens = usageTokens(usage);
|
|
589
589
|
const u = usage && typeof usage === 'object' ? usage : {};
|
|
590
590
|
// The settled charge can arrive either on the response or folded into the
|
|
591
|
-
// usage object —
|
|
591
|
+
// usage object — the upstream engine does the latter
|
|
592
592
|
// (`{ ...result.usage, costUsd: result.costUsd }`), so both are accepted.
|
|
593
593
|
const settled = typeof opts.costUsd === 'number'
|
|
594
594
|
? opts.costUsd
|
|
@@ -703,7 +703,7 @@ function rollTurn(roll, usage, opts = {}) {
|
|
|
703
703
|
if (turn.tokens == null) {
|
|
704
704
|
// No reported usage. The CLI does not let such a turn vanish from the
|
|
705
705
|
// total: `appendHistory` estimates it from the text and marks it
|
|
706
|
-
// `real: false` (
|
|
706
|
+
// `real: false` (the upstream engine's history module), and
|
|
707
707
|
// `aggregateSessionUsage` then adds it like any other row. Folding the same
|
|
708
708
|
// estimate here is what makes the live roll and the rebuilt roll the same
|
|
709
709
|
// number, and what stops the total standing still on exactly the turns the
|
|
@@ -772,7 +772,7 @@ function rollMessages(messages) {
|
|
|
772
772
|
* The ledger row one finished dispatch must carry into the shared session
|
|
773
773
|
* store, so the rolling total can be REBUILT when the thread is reopened —
|
|
774
774
|
* the desktop's counterpart of the CLI's `appendHistory`
|
|
775
|
-
* (
|
|
775
|
+
* (the upstream engine's history module).
|
|
776
776
|
*
|
|
777
777
|
* One authority for the row, on purpose. The CLI writes a `tokens` object for
|
|
778
778
|
* EVERY exchange and skips none:
|