aegis-desktop 0.8.12 → 0.8.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/auto.js +423 -0
- package/lib/avatar/packs/core-wardrobe.pack.json +1 -1
- package/lib/avatar/packs/local-voice-core.pack.json +1 -1
- package/lib/avatar/packs/seasonal-winter.pack.json +1 -1
- package/lib/avatar/profile.js +10 -1
- package/lib/fingerprint.js +327 -0
- package/lib/local/autonomous.js +1 -0
- package/lib/local/entitlement.js +81 -0
- package/lib/local/prompt.js +6 -0
- package/lib/local/tools.js +113 -0
- package/lib/settings.js +154 -1
- package/main.js +316 -17
- package/package.json +2 -2
- package/preload.js +84 -4
- package/renderer/app.js +208 -4
- package/renderer/fingerprint.css +143 -0
- package/renderer/fingerprint.js +583 -0
- package/renderer/index.html +163 -1
- package/vendor/aegis.js +180 -18
- package/vendor/agent-gate.mjs +1063 -0
- package/vendor/attest.js +439 -0
- package/vendor/authorship-key.js +366 -0
- package/vendor/device-fingerprint.js +85 -3
- package/vendor/fingerprint.js +1061 -0
- package/vendor/presence-constants.js +104 -0
- package/vendor/presence.js +435 -0
- package/vendor/record-vault.js +834 -0
- package/vendor/touch.js +455 -0
package/lib/auto.js
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* auto.js — the two opt-in automations, and the rules that make them safe to
|
|
5
|
+
* leave switched on.
|
|
6
|
+
*
|
|
7
|
+
* BACKGROUND. desktop/main.js and desktop/preload.js both stated an absolute
|
|
8
|
+
* rule when the queue shipped: "NOTHING DRAINS UNLESS THE USER ASKED. There is
|
|
9
|
+
* no interval, no drain at startup, and no drain-on-idle anywhere in this
|
|
10
|
+
* file." That rule was right when the only trigger was a click, and it is still
|
|
11
|
+
* the behaviour of a default install — but it made "queue a task and walk away"
|
|
12
|
+
* impossible, which is the one thing an unattended queue is FOR. The user asked
|
|
13
|
+
* for both automations explicitly (option A of that discussion), so the rule
|
|
14
|
+
* becomes a DEFAULT rather than an absolute, in the only way that keeps the
|
|
15
|
+
* original sentence true: an explicit, persisted, default-off switch, and a
|
|
16
|
+
* tick that does nothing unless it is armed.
|
|
17
|
+
*
|
|
18
|
+
* There are exactly two automations, because there are exactly two acts in the
|
|
19
|
+
* app that are irreversible-ish and cost something:
|
|
20
|
+
*
|
|
21
|
+
* createAutoDrain — runs pending queue tasks on a timer. Spend: a full tool
|
|
22
|
+
* loop on a real model, in a real checkout. Guarded by the queue's own file
|
|
23
|
+
* lock (a second drain is refused by queue.js, not here), by a re-entrancy
|
|
24
|
+
* flag, by an empty-queue check, and by a lock-holder check that skips the
|
|
25
|
+
* tick instead of racing another host for it.
|
|
26
|
+
* createAutoMint — seals the current session as a φ(α) record once the
|
|
27
|
+
* session goes QUIET. Not once per turn: a record is about a session, so a
|
|
28
|
+
* debounce that resets on every finished turn means one record per session
|
|
29
|
+
* that carries all of its turns — human axis and AI axis alike. Publishing
|
|
30
|
+
* is a separate flag and is never implied by sealing.
|
|
31
|
+
*
|
|
32
|
+
* WHAT THIS MODULE DELIBERATELY DOES NOT DO:
|
|
33
|
+
* - It does not decide whether it MAY run. `settings.getAutoDrain()` /
|
|
34
|
+
* `getAutoMint()` are read on every tick, so turning the toggle off stops
|
|
35
|
+
* the very next tick, and the namespaces are reserved
|
|
36
|
+
* (settings.js RESERVED_NAMESPACES) so no provider row can forge one.
|
|
37
|
+
* - It does not take the drain lock, spawn a worker, or write a record. The
|
|
38
|
+
* queue worker owns the lock; lib/fingerprint.js owns the seal. This file
|
|
39
|
+
* owns the TIMING and the skips, and calls in.
|
|
40
|
+
* - It does not swallow a failure. Every skip and every failure is recorded
|
|
41
|
+
* in `state()` with a reason, because an automation that fails silently is
|
|
42
|
+
* indistinguishable from one that is switched off.
|
|
43
|
+
*
|
|
44
|
+
* Timers are injected (`setIntervalFn`/`setTimeoutFn`) so the tests drive the
|
|
45
|
+
* cadence without waiting for it, and so this module never keeps a Node process
|
|
46
|
+
* alive by itself.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/** How long a session must be quiet before an armed auto-mint seals it. Long
|
|
50
|
+
* enough that a normal pause for thought does not file a half-finished
|
|
51
|
+
* session, short enough that walking away leaves a record behind. */
|
|
52
|
+
const AUTO_MINT_QUIET_DEFAULT_MS = 90_000;
|
|
53
|
+
|
|
54
|
+
/** Floor: sealing on every turn boundary would file one record per exchange and
|
|
55
|
+
* re-read the whole history each time. */
|
|
56
|
+
const AUTO_MINT_QUIET_MIN_MS = 10_000;
|
|
57
|
+
|
|
58
|
+
/** Ceiling: an hour of quiet. Beyond that the app is closed anyway. */
|
|
59
|
+
const AUTO_MINT_QUIET_MAX_MS = 60 * 60_000;
|
|
60
|
+
|
|
61
|
+
function errorText(err) {
|
|
62
|
+
return (err && err.message) || String(err);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The auto-drain timer.
|
|
67
|
+
*
|
|
68
|
+
* `proceed` is main.js's drain-all entry point (the same function the card's
|
|
69
|
+
* "Drain all" button calls) — this module never re-implements the loop, the
|
|
70
|
+
* lock, the settle-back-into-the-file write, or the Stop path.
|
|
71
|
+
*/
|
|
72
|
+
function createAutoDrain({
|
|
73
|
+
proceed,
|
|
74
|
+
settings,
|
|
75
|
+
queue,
|
|
76
|
+
env = process.env,
|
|
77
|
+
log = () => {},
|
|
78
|
+
setIntervalFn = setInterval,
|
|
79
|
+
clearIntervalFn = clearInterval,
|
|
80
|
+
} = {}) {
|
|
81
|
+
if (typeof proceed !== 'function') {
|
|
82
|
+
throw new Error('createAutoDrain: a proceed() entry point is required');
|
|
83
|
+
}
|
|
84
|
+
if (!settings || typeof settings.getAutoDrain !== 'function') {
|
|
85
|
+
throw new Error('createAutoDrain: the settings store is required');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The armed timer, or null. `null` is the OFF state and the only one a
|
|
89
|
+
* default install is ever in. */
|
|
90
|
+
let timer = null;
|
|
91
|
+
/** A tick is mid-flight. A drain takes minutes; without this the next tick
|
|
92
|
+
* would stack a second drainAll() on top of the first (queue.js would refuse
|
|
93
|
+
* it at the lock, but only after a round trip and a second set of frames). */
|
|
94
|
+
let busy = false;
|
|
95
|
+
let ticks = 0;
|
|
96
|
+
let drains = 0;
|
|
97
|
+
let last = null;
|
|
98
|
+
let lastSkip = null;
|
|
99
|
+
|
|
100
|
+
const record = (entry) => {
|
|
101
|
+
last = entry;
|
|
102
|
+
log(entry);
|
|
103
|
+
return entry;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const skip = (reason, extra) => {
|
|
107
|
+
lastSkip = { at: Date.now(), reason, ...(extra || {}) };
|
|
108
|
+
return lastSkip;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
function state() {
|
|
112
|
+
const cfg = settings.autoDrainState();
|
|
113
|
+
return {
|
|
114
|
+
...cfg,
|
|
115
|
+
armed: Boolean(timer),
|
|
116
|
+
busy,
|
|
117
|
+
ticks,
|
|
118
|
+
drains,
|
|
119
|
+
last,
|
|
120
|
+
lastSkip,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function arm() {
|
|
125
|
+
const cfg = settings.getAutoDrain();
|
|
126
|
+
// Off is checked BEFORE the "already armed at this cadence" shortcut, or
|
|
127
|
+
// switching off would leave the old timer running (harmless — its tick
|
|
128
|
+
// skips as 'disabled' — but `armed` would read true in the card, which is
|
|
129
|
+
// the kind of lie that makes a user distrust the switch).
|
|
130
|
+
if (!cfg.enabled) {
|
|
131
|
+
stop();
|
|
132
|
+
return state();
|
|
133
|
+
}
|
|
134
|
+
if (timer && cfg.intervalMs === timer.__intervalMs) return state();
|
|
135
|
+
stop();
|
|
136
|
+
timer = setIntervalFn(() => {
|
|
137
|
+
// Fire and forget: a tick's own failures are recorded in `last`, and an
|
|
138
|
+
// unhandled rejection here would take the main process's error handler
|
|
139
|
+
// with it. tick() never throws.
|
|
140
|
+
tick();
|
|
141
|
+
}, cfg.intervalMs);
|
|
142
|
+
timer.__intervalMs = cfg.intervalMs;
|
|
143
|
+
// A drain must never be the reason a process stays alive; in Electron the
|
|
144
|
+
// window is, and in a test the harness is.
|
|
145
|
+
if (timer && typeof timer.unref === 'function') timer.unref();
|
|
146
|
+
log({ type: 'auto-drain-armed', intervalMs: cfg.intervalMs });
|
|
147
|
+
return state();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function stop() {
|
|
151
|
+
if (!timer) return state();
|
|
152
|
+
clearIntervalFn(timer);
|
|
153
|
+
timer = null;
|
|
154
|
+
log({ type: 'auto-drain-disarmed' });
|
|
155
|
+
return state();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* One decision, and the only place the guards live.
|
|
160
|
+
*
|
|
161
|
+
* Order matters. `disabled` is first so an armed-then-switched-off timer
|
|
162
|
+
* cannot act on a stale config; `busy` before any file read; `empty` before
|
|
163
|
+
* the lock so an idle install costs one JSON parse and no lock I/O; and the
|
|
164
|
+
* lock-holder check LAST, because taking a lock we would only hand straight
|
|
165
|
+
* back is the one skip that has a real cost (it makes another host's drain
|
|
166
|
+
* fail its own acquire).
|
|
167
|
+
*/
|
|
168
|
+
async function tick() {
|
|
169
|
+
ticks += 1;
|
|
170
|
+
const cfg = settings.getAutoDrain();
|
|
171
|
+
if (!cfg.enabled) return record({ type: 'skip', reason: 'disabled' });
|
|
172
|
+
if (busy) return record({ type: 'skip', reason: 'busy' });
|
|
173
|
+
if (!queue || typeof queue.loadQueue !== 'function') {
|
|
174
|
+
return record({ type: 'skip', reason: 'no queue access' });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let items;
|
|
178
|
+
try {
|
|
179
|
+
items = queue.loadQueue(env);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
return record({ type: 'error', phase: 'read', error: errorText(err) });
|
|
182
|
+
}
|
|
183
|
+
if (!queue.pendingCount(items)) return record({ type: 'skip', reason: 'empty' });
|
|
184
|
+
|
|
185
|
+
// Another host holds the drain (the CLI's `aegiscode autonomous`, a
|
|
186
|
+
// systemd unit, another window). queue.js's lock would refuse us anyway —
|
|
187
|
+
// this check is so the refusal is a quiet skip rather than a drain attempt
|
|
188
|
+
// whose every frame reports "locked".
|
|
189
|
+
try {
|
|
190
|
+
const holder = queue.lockHolder(env);
|
|
191
|
+
if (holder && holder.pid && holder.pid !== process.pid) {
|
|
192
|
+
const alive = typeof queue.isAlive === 'function' ? queue.isAlive(holder.pid) : true;
|
|
193
|
+
if (alive) return record({ type: 'skip', reason: 'locked', holder });
|
|
194
|
+
}
|
|
195
|
+
} catch {
|
|
196
|
+
// An unreadable lock file is not a reason to skip: queue.js's own
|
|
197
|
+
// acquireLock decides, and a stale/unparseable marker is exactly the case
|
|
198
|
+
// it reclaims immediately.
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
busy = true;
|
|
202
|
+
try {
|
|
203
|
+
const result = await proceed();
|
|
204
|
+
if (result && result.locked) {
|
|
205
|
+
return record({
|
|
206
|
+
type: 'skip',
|
|
207
|
+
reason: 'locked',
|
|
208
|
+
holder: result.holder || null,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
drains += 1;
|
|
212
|
+
return record({
|
|
213
|
+
type: 'drain',
|
|
214
|
+
ok: !(result && result.ok === false),
|
|
215
|
+
ran: (result && result.ran) || [],
|
|
216
|
+
reason: (result && result.reason) || null,
|
|
217
|
+
});
|
|
218
|
+
} catch (err) {
|
|
219
|
+
return record({ type: 'error', phase: 'drain', error: errorText(err) });
|
|
220
|
+
} finally {
|
|
221
|
+
busy = false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
state,
|
|
227
|
+
/** Read the persisted switch and arm/disarm to match. Called at boot and
|
|
228
|
+
* after every toggle — never called by a timer. */
|
|
229
|
+
start: arm,
|
|
230
|
+
arm,
|
|
231
|
+
stop,
|
|
232
|
+
tick,
|
|
233
|
+
/** The card's toggle. Persists through the settings store, then re-arms,
|
|
234
|
+
* so the answer the renderer paints is the answer the timer is running.
|
|
235
|
+
* Without the re-arm the switch would persist and do nothing until the next
|
|
236
|
+
* restart — the exact silent failure this test file exists to catch. */
|
|
237
|
+
set(patch) {
|
|
238
|
+
settings.setAutoDrain(patch || {});
|
|
239
|
+
arm();
|
|
240
|
+
return state();
|
|
241
|
+
},
|
|
242
|
+
/** Test seam: whatever is armed, for a harness that injects its own timer
|
|
243
|
+
* and needs to fire it. */
|
|
244
|
+
_timer: () => timer,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The auto-mint (seal-on-quiet) timer.
|
|
250
|
+
*
|
|
251
|
+
* `mint` is lib/fingerprint.js's own `mint()` — the same call the panel's "Seal
|
|
252
|
+
* this session" button makes, reading the same session log through the same
|
|
253
|
+
* shared adapter, so an automatic record and a hand-sealed one of the same
|
|
254
|
+
* session are the same record.
|
|
255
|
+
*/
|
|
256
|
+
function createAutoMint({
|
|
257
|
+
mint,
|
|
258
|
+
settings,
|
|
259
|
+
log = () => {},
|
|
260
|
+
quietMs = AUTO_MINT_QUIET_DEFAULT_MS,
|
|
261
|
+
setTimeoutFn = setTimeout,
|
|
262
|
+
clearTimeoutFn = clearTimeout,
|
|
263
|
+
} = {}) {
|
|
264
|
+
if (typeof mint !== 'function') throw new Error('createAutoMint: mint() is required');
|
|
265
|
+
if (!settings || typeof settings.getAutoMint !== 'function') {
|
|
266
|
+
throw new Error('createAutoMint: the settings store is required');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const quiet = (() => {
|
|
270
|
+
const n = Number(quietMs);
|
|
271
|
+
if (!Number.isFinite(n)) return AUTO_MINT_QUIET_DEFAULT_MS;
|
|
272
|
+
const ms = Math.round(n);
|
|
273
|
+
if (ms < AUTO_MINT_QUIET_MIN_MS || ms > AUTO_MINT_QUIET_MAX_MS) {
|
|
274
|
+
return AUTO_MINT_QUIET_DEFAULT_MS;
|
|
275
|
+
}
|
|
276
|
+
return ms;
|
|
277
|
+
})();
|
|
278
|
+
|
|
279
|
+
/** The pending debounce, or null. */
|
|
280
|
+
let timer = null;
|
|
281
|
+
/** Turns that arrived since the last seal. The ONLY thing that makes a seal
|
|
282
|
+
* due: without it, a quiet period with no new work would re-seal the same
|
|
283
|
+
* session over and over, one identical record per quiet minute. */
|
|
284
|
+
let pendingTurns = 0;
|
|
285
|
+
let busy = false;
|
|
286
|
+
let seals = 0;
|
|
287
|
+
let failures = 0;
|
|
288
|
+
let last = null;
|
|
289
|
+
let lastError = null;
|
|
290
|
+
let lastSeal = null;
|
|
291
|
+
|
|
292
|
+
function state() {
|
|
293
|
+
const cfg = settings.autoMintState();
|
|
294
|
+
return {
|
|
295
|
+
...cfg,
|
|
296
|
+
armed: Boolean(timer),
|
|
297
|
+
quietMs: quiet,
|
|
298
|
+
pendingTurns,
|
|
299
|
+
busy,
|
|
300
|
+
seals,
|
|
301
|
+
failures,
|
|
302
|
+
last,
|
|
303
|
+
lastError,
|
|
304
|
+
lastSeal,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function disarm() {
|
|
309
|
+
if (!timer) return;
|
|
310
|
+
clearTimeoutFn(timer);
|
|
311
|
+
timer = null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** A turn finished. Restart the quiet clock; do not seal anything now. */
|
|
315
|
+
function note(info) {
|
|
316
|
+
const cfg = settings.getAutoMint();
|
|
317
|
+
if (!cfg.enabled) {
|
|
318
|
+
// Counted even while off, so switching the toggle on mid-session seals a
|
|
319
|
+
// session that already has work in it rather than waiting for the next
|
|
320
|
+
// turn — which is what a user who just flipped the switch means.
|
|
321
|
+
pendingTurns += 1;
|
|
322
|
+
return state();
|
|
323
|
+
}
|
|
324
|
+
pendingTurns += 1;
|
|
325
|
+
last = { type: 'turn', at: Date.now(), sessionId: (info && info.sessionId) || null };
|
|
326
|
+
disarm();
|
|
327
|
+
timer = setTimeoutFn(() => {
|
|
328
|
+
timer = null;
|
|
329
|
+
flush();
|
|
330
|
+
}, quiet);
|
|
331
|
+
if (timer && typeof timer.unref === 'function') timer.unref();
|
|
332
|
+
return state();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Seal now, if there is anything to seal. This is what the debounce timer
|
|
337
|
+
* calls; it is also the seam a test drives directly instead of waiting.
|
|
338
|
+
*/
|
|
339
|
+
async function flush() {
|
|
340
|
+
const cfg = settings.getAutoMint();
|
|
341
|
+
if (!cfg.enabled) return { type: 'skip', reason: 'disabled' };
|
|
342
|
+
if (busy) return { type: 'skip', reason: 'busy' };
|
|
343
|
+
if (!pendingTurns) return { type: 'skip', reason: 'nothing new' };
|
|
344
|
+
busy = true;
|
|
345
|
+
// Claimed BEFORE the call: a seal that throws must not leave the counter
|
|
346
|
+
// armed, or every later quiet period retries a failure that will not heal
|
|
347
|
+
// (no key, unreadable history) and files a new error each time.
|
|
348
|
+
const claimed = pendingTurns;
|
|
349
|
+
pendingTurns = 0;
|
|
350
|
+
try {
|
|
351
|
+
// `aiSecret` is left undefined on purpose: that makes lib/fingerprint.js
|
|
352
|
+
// walk the shared rungs in client/attest.js, so the AI axis is attested
|
|
353
|
+
// from `$AEGIS_AI_SECRET` in the app's scope and `aiModel` from the same
|
|
354
|
+
// place the CLI resolves it. This host never invents either, and never
|
|
355
|
+
// borrows the authorship key for the AI axis (they answer different
|
|
356
|
+
// questions: who sealed it, and which mind answered).
|
|
357
|
+
const result = await mint({ publish: cfg.publish === true });
|
|
358
|
+
if (!result || result.ok === false) {
|
|
359
|
+
failures += 1;
|
|
360
|
+
lastError = (result && result.error) || 'the seal did not complete';
|
|
361
|
+
return (last = { type: 'error', phase: 'seal', error: lastError });
|
|
362
|
+
}
|
|
363
|
+
seals += 1;
|
|
364
|
+
lastError = null;
|
|
365
|
+
lastSeal = {
|
|
366
|
+
at: Date.now(),
|
|
367
|
+
id: (result.record && result.record.id) || null,
|
|
368
|
+
out: result.out || null,
|
|
369
|
+
turns: claimed,
|
|
370
|
+
// Reported separately by lib/fingerprint.js: the seal is the evidence,
|
|
371
|
+
// the cloud copy is the convenience, and a failed publish never
|
|
372
|
+
// unseals anything.
|
|
373
|
+
vaultError: result.vaultError || null,
|
|
374
|
+
};
|
|
375
|
+
return (last = { type: 'seal', ...lastSeal });
|
|
376
|
+
} catch (err) {
|
|
377
|
+
failures += 1;
|
|
378
|
+
lastError = errorText(err);
|
|
379
|
+
return (last = { type: 'error', phase: 'seal', error: lastError });
|
|
380
|
+
} finally {
|
|
381
|
+
busy = false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
state,
|
|
387
|
+
note,
|
|
388
|
+
flush,
|
|
389
|
+
/** Disarm the debounce and forget the pending count — the renderer's
|
|
390
|
+
* explicit "seal now" path calls this after sealing, so the record it just
|
|
391
|
+
* filed is not filed again by the timer. */
|
|
392
|
+
reset() {
|
|
393
|
+
disarm();
|
|
394
|
+
pendingTurns = 0;
|
|
395
|
+
return state();
|
|
396
|
+
},
|
|
397
|
+
start() {
|
|
398
|
+
// Arming at boot means "the switch is on", not "seal immediately": with
|
|
399
|
+
// pendingTurns at 0 there is nothing to seal until a turn arrives.
|
|
400
|
+
if (!settings.getAutoMint().enabled) disarm();
|
|
401
|
+
return state();
|
|
402
|
+
},
|
|
403
|
+
stop() {
|
|
404
|
+
disarm();
|
|
405
|
+
return state();
|
|
406
|
+
},
|
|
407
|
+
set(patch) {
|
|
408
|
+
settings.setAutoMint(patch || {});
|
|
409
|
+
if (!settings.getAutoMint().enabled) disarm();
|
|
410
|
+
return state();
|
|
411
|
+
},
|
|
412
|
+
/** Test seam: is the debounce armed? */
|
|
413
|
+
_timer: () => timer,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
module.exports = {
|
|
418
|
+
createAutoDrain,
|
|
419
|
+
createAutoMint,
|
|
420
|
+
AUTO_MINT_QUIET_DEFAULT_MS,
|
|
421
|
+
AUTO_MINT_QUIET_MIN_MS,
|
|
422
|
+
AUTO_MINT_QUIET_MAX_MS,
|
|
423
|
+
};
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"tier": "free",
|
|
7
7
|
"unlocks": ["outfit.hoodie", "outfit.tshirt"],
|
|
8
8
|
"author": "AEGIS Code",
|
|
9
|
-
"license": "
|
|
9
|
+
"license": "AGPL-3.0-or-later",
|
|
10
10
|
"summary": "The two starter outfits. Yours from level 1, and never sold back to you.",
|
|
11
11
|
"items": [
|
|
12
12
|
{ "id": "hoodie", "label": "Hoodie", "parts": ["hoodie"] },
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"tier": "free",
|
|
7
7
|
"unlocks": ["voice.core"],
|
|
8
8
|
"author": "AEGIS Code",
|
|
9
|
-
"license": "
|
|
9
|
+
"license": "AGPL-3.0-or-later",
|
|
10
10
|
"summary": "Three bundled synthetic voices. Local engine only, off until the user turns it on, and nothing is ever sent anywhere to say a sentence.",
|
|
11
11
|
"items": [
|
|
12
12
|
{ "id": "aegis-neutral", "label": "Neutral", "engine": "local", "consent": "synthetic" },
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"tier": "paid",
|
|
7
7
|
"unlocks": ["outfit.seasonal"],
|
|
8
8
|
"author": "AEGIS Code",
|
|
9
|
-
"license": "
|
|
9
|
+
"license": "AGPL-3.0-or-later",
|
|
10
10
|
"summary": "A cosmetic pack. Paid packs sell looks only: this changes how the companion is drawn and nothing about what it can do.",
|
|
11
11
|
"items": [
|
|
12
12
|
{ "id": "seasonal-winter", "label": "Winter coat", "parts": ["seasonal-winter", "scarf"] }
|
package/lib/avatar/profile.js
CHANGED
|
@@ -140,7 +140,16 @@ function keyOf(value) {
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
function asArray(value) {
|
|
143
|
-
|
|
143
|
+
if (Array.isArray(value)) return value;
|
|
144
|
+
// A Set is what the folder builders accumulate source ids into (`ids: new
|
|
145
|
+
// Set()` in foldVocabulary/foldLanguage/foldCodebase). Without this branch
|
|
146
|
+
// `asArray(set)` returned [], so `citable(rec.ids)` was always empty, so
|
|
147
|
+
// `makeFacet` returned null for want of a source: vocabulary, language and
|
|
148
|
+
// codebase facets could never be produced at all — three of the seven classes
|
|
149
|
+
// were dead code, and `coldStart` stayed true for a holder whose only evidence
|
|
150
|
+
// was theirs.
|
|
151
|
+
if (value instanceof Set) return [...value];
|
|
152
|
+
return [];
|
|
144
153
|
}
|
|
145
154
|
|
|
146
155
|
function textOf(entry) {
|