aegis-desktop 0.8.5 → 0.8.7
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/marketing-assets/README.md +89 -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 +70 -3
- package/lib/local/engine.js +19 -9
- package/lib/local/entitlement.js +487 -0
- 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/telemetry.js +467 -0
- 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 +36 -3
- package/main.js +189 -9
- package/package.json +5 -8
- package/preload.js +34 -0
- package/renderer/app.js +319 -40
- 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 +123 -55
- package/renderer/style.css +91 -7
- package/renderer/transcript-view.js +1 -1
- package/renderer/usage.js +6 -6
- package/vendor/aegis.js +146 -2
- package/vendor/credentials.js +4 -0
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
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Marketing assets — captions, claim, and tagged links
|
|
2
|
+
|
|
3
|
+
The three PNGs in this directory are the **existing** asset kit (1440×900, shot
|
|
4
|
+
against the real app — see `docs/marketing-log.md`, 2026-09-22 entry). Images
|
|
5
|
+
carry no link of their own, so the outbound link for each placement lives here,
|
|
6
|
+
in the same directory as the image it belongs to. Nothing in this directory is a
|
|
7
|
+
new asset: these are the captions for the three files below, rewritten for the
|
|
8
|
+
Phase 5 promotion pass (`docs/upgrade-conversion-plan.md` §5).
|
|
9
|
+
|
|
10
|
+
**The one claim (Phase 3, verbatim, same sentence as the welcome panel):**
|
|
11
|
+
|
|
12
|
+
> **The autonomous queue keeps running after you close the laptop.**
|
|
13
|
+
|
|
14
|
+
Plus the honesty line that sits next to it:
|
|
15
|
+
|
|
16
|
+
> Everything on your machine is free, permanently. The cloud drain is what you
|
|
17
|
+
> pay for.
|
|
18
|
+
|
|
19
|
+
(`desktop/renderer/app.js` → `PAID_PROMISE` / `FREE_HONESTY`, rendered by
|
|
20
|
+
`#welcome-template` in `desktop/renderer/index.html`.)
|
|
21
|
+
|
|
22
|
+
**What the caption must not be:** a tour of the three model classes, or the free
|
|
23
|
+
local row written up as if it were the product. The screenshots show the picker
|
|
24
|
+
and the diff card — that is *what the image is of*, not the *offer*. One claim
|
|
25
|
+
per asset; the class list stays out of the caption.
|
|
26
|
+
|
|
27
|
+
**Channel rule.** Facebook and Reddit are image-friendly; X and YouTube captions
|
|
28
|
+
follow `docs/launch-copy-x-youtube.md` §2/§3. Every link is
|
|
29
|
+
`https://aegiscloud.org/go/upgrade?s=<channel>&c=<campaign>` with `s` in
|
|
30
|
+
`(x, youtube, reddit, facebook)` — the only channels `SIGNUP_SOURCES` in
|
|
31
|
+
`aegis1/app.py` accepts — and one campaign id per channel, stable across every
|
|
32
|
+
asset on that channel.
|
|
33
|
+
|
|
34
|
+
| Image | Placement | Channel `s` | Campaign `c` | Tagged link (paste this exact URL) |
|
|
35
|
+
|---|---|---|---|---|
|
|
36
|
+
| `01-model-class-picker.png` | X post (picker + claim in the reply) | `x` | `x_launch` | `https://aegiscloud.org/go/upgrade?s=x&c=x_launch` |
|
|
37
|
+
| `01-model-class-picker.png` | Reddit image post (r/LocalLLaMA, r/ollama — link in the closing block, after the limitations) | `reddit` | `reddit_launch` | `https://aegiscloud.org/go/upgrade?s=reddit&c=reddit_launch` |
|
|
38
|
+
| `01-model-class-picker.png` | Facebook Page + groups (image + claim + link) | `facebook` | `facebook_launch` | `https://aegiscloud.org/go/upgrade?s=facebook&c=facebook_launch` |
|
|
39
|
+
| `02-answer-complete.png` | YouTube Short / long-form B-roll; `Upgrade:` line in the description | `youtube` | `yt_launch` | `https://aegiscloud.org/go/upgrade?s=youtube&c=yt_launch` |
|
|
40
|
+
| `02-answer-complete.png` | X post (answer in flight) | `x` | `x_launch` | `https://aegiscloud.org/go/upgrade?s=x&c=x_launch` |
|
|
41
|
+
| `03-tool-approval-diff.png` | X pinned post / thread 3- (the diff card) | `x` | `x_launch` | `https://aegiscloud.org/go/upgrade?s=x&c=x_launch` |
|
|
42
|
+
| `03-tool-approval-diff.png` | Reddit GIF+text post (r/ChatGPTCoding) | `reddit` | `reddit_launch` | `https://aegiscloud.org/go/upgrade?s=reddit&c=reddit_launch` |
|
|
43
|
+
| `03-tool-approval-diff.png` | YouTube thumbnail / description | `youtube` | `yt_launch` | `https://aegiscloud.org/go/upgrade?s=youtube&c=yt_launch` |
|
|
44
|
+
|
|
45
|
+
Every row above is registered in `docs/marketing-log.md` §7 so post-hoc revenue
|
|
46
|
+
can be joined to the channel and the asset face that produced it.
|
|
47
|
+
|
|
48
|
+
## Captions
|
|
49
|
+
|
|
50
|
+
### `01-model-class-picker.png` — the class picker
|
|
51
|
+
|
|
52
|
+
> The autonomous queue keeps running after you close the laptop. Everything on
|
|
53
|
+
> your machine is free, permanently — the cloud drain is what you pay for.
|
|
54
|
+
> npm i -g aegis-desktop
|
|
55
|
+
> https://aegiscloud.org/go/upgrade?s=x&c=x_launch
|
|
56
|
+
|
|
57
|
+
Facebook variant (same claim, same tagged shape, different channel):
|
|
58
|
+
|
|
59
|
+
> The autonomous queue keeps running after you close the laptop. Cloud-backed
|
|
60
|
+
> drain, survives a reboot, fans a task out across workers, memory carried
|
|
61
|
+
> between tasks. Everything on your machine is free, permanently.
|
|
62
|
+
> npm i -g aegis-desktop
|
|
63
|
+
> https://aegiscloud.org/go/upgrade?s=facebook&c=facebook_launch
|
|
64
|
+
|
|
65
|
+
Do **not** caption this image with the picker's three rows as the pitch. The
|
|
66
|
+
picker is a fact about the product; the claim above is the offer.
|
|
67
|
+
|
|
68
|
+
### `02-answer-complete.png` — an answer, complete
|
|
69
|
+
|
|
70
|
+
> The autonomous queue keeps running after you close the laptop — the work does
|
|
71
|
+
> not stop when the laptop does. Everything on your machine is free, permanently.
|
|
72
|
+
> Install: `npm i -g aegis-desktop && aegis`
|
|
73
|
+
> https://aegiscloud.org/go/upgrade?s=youtube&c=yt_launch
|
|
74
|
+
|
|
75
|
+
### `03-tool-approval-diff.png` — the diff/approval card
|
|
76
|
+
|
|
77
|
+
> The diff card is the whole product locally — and it is free, permanently. The
|
|
78
|
+
> pocket-book item is the drain that keeps running after you close the laptop.
|
|
79
|
+
> Install: `npm i -g aegis-desktop && aegis`
|
|
80
|
+
> https://aegiscloud.org/go/upgrade?s=x&c=x_launch
|
|
81
|
+
|
|
82
|
+
## Before posting (§0 of `docs/marketing-plan-social.md`)
|
|
83
|
+
|
|
84
|
+
1. Re-verify the version (`desktop/package.json`) and re-shoot the image if the
|
|
85
|
+
UI moved; log that check in `docs/marketing-log.md` §6.
|
|
86
|
+
2. Confirm the link still resolves and still carries `s=`/`c=`. A bare
|
|
87
|
+
`aegiscloud.org/subscribe` (bare, untagged) link files as `direct` and the channel
|
|
88
|
+
gets no row.
|
|
89
|
+
3. Never invent a number in a caption (marketing-log §2 rule 1).
|
|
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
|