aegis-desktop 1.0.13 → 1.0.15

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.
Files changed (96) hide show
  1. package/SOURCE-OFFER.txt +9 -0
  2. package/bin/aegis.js +1 -32
  3. package/lib/account-key.js +1 -56
  4. package/lib/auto.js +1 -430
  5. package/lib/avatar/companion.js +1 -698
  6. package/lib/avatar/cosmetics.js +1 -532
  7. package/lib/avatar/events.js +1 -344
  8. package/lib/avatar/holder-turn.js +1 -350
  9. package/lib/avatar/holders.js +1 -612
  10. package/lib/avatar/identity.js +1 -641
  11. package/lib/avatar/level.js +1 -368
  12. package/lib/avatar/mirror.js +1 -524
  13. package/lib/avatar/persona.js +1 -388
  14. package/lib/avatar/profile.js +1 -977
  15. package/lib/avatar/register.js +1 -307
  16. package/lib/avatar/store.js +1 -472
  17. package/lib/avatar/tiers.js +1 -38
  18. package/lib/avatar/transfer.js +1 -415
  19. package/lib/avatar/turn.js +1 -216
  20. package/lib/avatar/voice.js +1 -328
  21. package/lib/avatar/xp.js +1 -404
  22. package/lib/deep-link.js +1 -120
  23. package/lib/fingerprint.js +2 -347
  24. package/lib/google-connect.js +1 -169
  25. package/lib/local/agents.js +1 -105
  26. package/lib/local/autonomous.js +1 -1045
  27. package/lib/local/claude-pro.js +1 -993
  28. package/lib/local/context.js +1 -95
  29. package/lib/local/engine.js +1 -3228
  30. package/lib/local/entitlement.js +1 -654
  31. package/lib/local/git-scope.js +1 -228
  32. package/lib/local/local.js +1 -2135
  33. package/lib/local/prompt.js +1 -275
  34. package/lib/local/queue.js +1 -560
  35. package/lib/local/session-rounds.js +1 -208
  36. package/lib/local/shell.js +1 -680
  37. package/lib/local/telemetry.js +1 -470
  38. package/lib/local/tools.js +1 -1238
  39. package/lib/local/turn-guard.js +1 -300
  40. package/lib/local/worktree-lock.js +1 -235
  41. package/lib/provider-key-mirror.js +1 -252
  42. package/lib/quick-launcher.js +1 -84
  43. package/lib/settings.js +1 -890
  44. package/lib/sync/memory-queue.js +1 -60
  45. package/lib/sync/persist-gate.js +1 -148
  46. package/lib/sync/sessions.js +1 -106
  47. package/lib/window-state.js +1 -66
  48. package/main.js +3 -4390
  49. package/package.json +3 -5
  50. package/preload.js +3 -628
  51. package/renderer/app.js +1 -6560
  52. package/renderer/avatar/assemble.js +1 -335
  53. package/renderer/avatar/avatar.js +1 -380
  54. package/renderer/avatar/cards.js +1 -141
  55. package/renderer/avatar/hud.js +1 -206
  56. package/renderer/avatar/machine.js +1 -213
  57. package/renderer/avatar/pane.js +1 -439
  58. package/renderer/avatar/parts.js +1 -437
  59. package/renderer/budget.js +1 -168
  60. package/renderer/companion.js +1 -223
  61. package/renderer/diff.js +1 -359
  62. package/renderer/diffview.js +1 -336
  63. package/renderer/fingerprint.js +3 -720
  64. package/renderer/markdown.js +1 -168
  65. package/renderer/provider-toolbar.js +1 -134
  66. package/renderer/providers.js +1 -259
  67. package/renderer/quick.js +1 -191
  68. package/renderer/stream-policy.js +1 -306
  69. package/renderer/transcript-view.js +1 -323
  70. package/renderer/usage.js +1 -1239
  71. package/vendor/aegis.js +1 -2054
  72. package/vendor/attest.js +1 -442
  73. package/vendor/authorship-key.js +1 -410
  74. package/vendor/auto-provision.js +1 -134
  75. package/vendor/brain-catalog.js +1 -268
  76. package/vendor/byok-catalog.js +1 -230
  77. package/vendor/credentials.js +1 -494
  78. package/vendor/device-fingerprint.js +1 -141
  79. package/vendor/drive-connect.js +1 -85
  80. package/vendor/drive-upload.js +1 -174
  81. package/vendor/env-file.js +1 -1273
  82. package/vendor/fingerprint.js +1 -1064
  83. package/vendor/foreign-memory.js +1 -669
  84. package/vendor/google-signin.js +1 -125
  85. package/vendor/hooks.js +1 -430
  86. package/vendor/model-discovery.js +1 -330
  87. package/vendor/platform.js +1 -937
  88. package/vendor/prefs.js +1 -351
  89. package/vendor/presence-constants.js +1 -107
  90. package/vendor/presence.js +1 -438
  91. package/vendor/record-vault.js +3 -1055
  92. package/vendor/session-store.js +1 -548
  93. package/vendor/topup.js +1 -297
  94. package/vendor/touch.js +1 -458
  95. package/vendor/update.js +1 -704
  96. package/vendor/user-models.js +1 -381
@@ -1,700 +1,3 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
2
  // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
- // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
- // Conveying a modified copy, or running one as a network service, owes the
5
- // corresponding source under this licence (§5/§13) — see LICENSING.md.
6
- 'use strict';
7
-
8
- /**
9
- * companion.js — the four companion behaviours.
10
- *
11
- * The behaviours are the morning brief, the idea log, the session reflection
12
- * and in-session recall. What they have in common is not their content, it is
13
- * the rule that makes them shippable at all:
14
- *
15
- * **The companion proposes; the user disposes.** This module cannot perform
16
- * anything. It has no fs, no Electron, no network, no timers and no clock of
17
- * its own — every fact it talks about is handed to it, every effect it
18
- * proposes is executed by a host that main passes in, and that host is only
19
- * ever reached through a *ticket* that `decide(cardId, 'accept')` issued for
20
- * one specific card. There is deliberately no `autoAccept`, no `level ≥ N`
21
- * shortcut and no "trusted behaviour" bypass: a level buys whether a card may
22
- * be *offered*, never whether one may be *acted on*.
23
- *
24
- * Four properties, each asserted in `test/avatar-companion.test.mjs`:
25
- *
26
- * 1. **Silent at level 1.** `level.capabilities(1)` reports
27
- * `proactivity: 'off'`, and every behaviour reads that through
28
- * `gating()`. With capabilities the module does not understand (a
29
- * hand-written caps object, a future schema) it fails *silent*: unknown
30
- * capabilities offer nothing rather than everything. That is the opposite
31
- * of `voice.js`'s "unknown caps never lock anything" convention, and it is
32
- * deliberate — silence is a safe failure for a proactive surface, noise is
33
- * not.
34
- * 2. **Opt-in per behaviour.** `DEFAULT_OPT_IN` is all-false, so a level alone
35
- * never turns a behaviour on. `capabilities` says *may*, `optIn` says *do*.
36
- * 3. **One proactive card per session** (`MAX_PROACTIVE_PER_SESSION`, spec
37
- * §6's "companion becomes noise" risk). A card the user asked for
38
- * (`user.request`) is not proactive and does not spend the budget.
39
- * 4. **No effect without an accepted card, and no *tool* without an
40
- * interactive approval.** `dispatch()` refuses on a missing, forged,
41
- * replayed or already-consumed ticket, and refuses any intent carrying a
42
- * `tool` unless the host's approval channel answers `once`/`session`. A
43
- * host with no approval channel at all is refused too — "I forgot to wire
44
- * the prompt" must not degrade into "it ran anyway".
45
- *
46
- * Effects are declared data (`EFFECTS`), not code paths, so the carve-outs are
47
- * checkable: nothing here may be a shell, a network call or anything that sends
48
- * data off the device, and `carveOuts()` is the assertion that says so.
49
- *
50
- * Pure: no `fs`, no Electron, no globals.
51
- */
52
-
53
- const level = require('./level.js');
54
-
55
- /** The gate values from `level.js` — one source of truth for the level ladder. */
56
- const GATES = level.GATES;
57
-
58
- /** How many proactive cards one session may ever show (spec §6). */
59
- const MAX_PROACTIVE_PER_SESSION = 1;
60
-
61
- /** Hard ceiling on anything this module renders into a card, in characters. */
62
- const MAX_BODY_CHARS = 600;
63
- const MAX_FACT_CHARS = 160;
64
- const MAX_FACTS = 6;
65
- const MAX_CANDIDATES = 8;
66
-
67
- /**
68
- * Everything a behaviour is allowed to ask for. A closed table, because "the
69
- * companion can only do these three things" is a property worth being able to
70
- * test: every entry is local, and none of them is a shell, a network call or an
71
- * off-device send.
72
- */
73
- const EFFECTS = Object.freeze({
74
- 'brief.show': Object.freeze({ class: null, tool: null, offDevice: false }),
75
- 'memory.save': Object.freeze({ class: 'write', tool: null, offDevice: false }),
76
- 'recall.load': Object.freeze({ class: 'read', tool: 'memory.search', offDevice: false }),
77
- });
78
-
79
- /** Approval classes no companion effect may ever carry. */
80
- const FORBIDDEN_CLASSES = Object.freeze(['shell', 'network', 'offDevice']);
81
-
82
- /**
83
- * The behaviour catalogue. `needsProactivity` is the *minimum* proactivity the
84
- * level must earn before the card may be offered; `proactive` says whether the
85
- * card is unprompted (and therefore spends the session's one-card budget).
86
- */
87
- const CATALOG = Object.freeze([
88
- Object.freeze({
89
- id: 'morningBrief',
90
- label: 'Morning brief',
91
- // L5+ and only at `proactivity: 'brief'` — the point of the bonus tier.
92
- needsProactivity: 'brief',
93
- proactive: true,
94
- action: 'brief.show',
95
- acceptLabel: 'Show the brief',
96
- dismissLabel: 'Not now',
97
- doneLabel: 'Brief opened',
98
- }),
99
- Object.freeze({
100
- id: 'ideaLog',
101
- label: 'Idea log',
102
- // L5+. User-initiated ("remember this idea"), so it is not proactive and
103
- // does not need the proactivity tier — asking out loud is not noise.
104
- needsProactivity: 'off',
105
- proactive: false,
106
- action: 'memory.save',
107
- acceptLabel: 'Save to memory',
108
- dismissLabel: 'Discard',
109
- doneLabel: 'Saved to memory',
110
- }),
111
- Object.freeze({
112
- id: 'sessionReflection',
113
- label: 'Session reflection',
114
- // L10+, offered at session end, at most one line, and only with something
115
- // to say (a decision made or a thread left open).
116
- needsProactivity: 'hints',
117
- proactive: true,
118
- action: 'memory.save',
119
- acceptLabel: 'Save this line',
120
- dismissLabel: 'Discard',
121
- doneLabel: 'Saved to memory',
122
- }),
123
- Object.freeze({
124
- id: 'recall',
125
- label: 'Context recall',
126
- // Breadth scales with level (that is `turn.js`'s recallPolicy), but the
127
- // *card* only exists once the level is off 'off' — silent at L1.
128
- needsProactivity: 'hints',
129
- proactive: true,
130
- action: 'recall.load',
131
- acceptLabel: 'Load those notes',
132
- dismissLabel: 'Skip',
133
- doneLabel: 'Notes loaded',
134
- }),
135
- ]);
136
-
137
- const BEHAVIOUR_IDS = Object.freeze(CATALOG.map((b) => b.id));
138
-
139
- /** All-false: a level says *may*, this says *do*. */
140
- const DEFAULT_OPT_IN = Object.freeze(
141
- BEHAVIOUR_IDS.reduce((acc, id) => Object.assign(acc, { [id]: false }), {})
142
- );
143
-
144
- const PROACTIVITY_ORDER = Object.freeze(['off', 'hints', 'brief']);
145
-
146
- // --------------------------------------------------------------- small utils
147
-
148
- function isPlainObject(v) {
149
- return Boolean(v) && typeof v === 'object' && !Array.isArray(v);
150
- }
151
-
152
- /** Strip control characters / line separators and bound the length. */
153
- function clean(text, max) {
154
- const s = String(text == null ? '' : text)
155
- // eslint-disable-next-line no-control-regex
156
- .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f\u2028\u2029]/g, ' ')
157
- .replace(/\s+/g, ' ')
158
- .trim();
159
- const limit = typeof max === 'number' && max > 0 ? max : MAX_FACT_CHARS;
160
- return s.length > limit ? `${s.slice(0, limit - 1)}…` : s;
161
- }
162
-
163
- function atLeast(proactivity, want) {
164
- const a = PROACTIVITY_ORDER.indexOf(proactivity);
165
- const b = PROACTIVITY_ORDER.indexOf(want);
166
- if (a < 0) return false;
167
- if (b < 0) return true;
168
- return a >= b;
169
- }
170
-
171
- function behaviourById(id) {
172
- return CATALOG.find((b) => b.id === id) || null;
173
- }
174
-
175
- /** The flags `level.capabilities()` publishes for the gated behaviours. */
176
- function capsFlags(caps) {
177
- return isPlainObject(caps) && isPlainObject(caps.companion) ? caps.companion : null;
178
- }
179
-
180
- // ------------------------------------------------------------------- opt-in
181
-
182
- /** Normalize an opt-in map. Anything not explicitly `true` is off. */
183
- function normalizeOptIn(raw) {
184
- const out = Object.assign({}, DEFAULT_OPT_IN);
185
- if (!isPlainObject(raw)) return Object.freeze(out);
186
- for (const id of BEHAVIOUR_IDS) {
187
- if (raw[id] === true) out[id] = true;
188
- }
189
- return Object.freeze(out);
190
- }
191
-
192
- // ------------------------------------------------------------------ gating
193
-
194
- /**
195
- * Which behaviours this capabilities object + opt-in allow, and why not when it
196
- * does not. The reason string is user-facing ("unlocks at level 5"), because a
197
- * checkbox that silently does nothing is the failure mode this table exists to
198
- * prevent.
199
- *
200
- * @param {object} caps `level.capabilities(n)` output (or anything shaped like it).
201
- * @param {object} [opts]
202
- * @param {object} [opts.optIn] per-behaviour opt-in (`normalizeOptIn` shape).
203
- * @param {boolean} [opts.envDisabled] `AEGIS_COMPANION=off` — may only silence.
204
- * @returns {Object<string, {id: string, allowed: boolean, reason: string}>}
205
- */
206
- function gating(caps, opts = {}) {
207
- const optIn = normalizeOptIn(opts.optIn);
208
- const flags = capsFlags(caps);
209
- const proactivity = isPlainObject(caps) && typeof caps.proactivity === 'string' ? caps.proactivity : 'off';
210
- const out = {};
211
-
212
- for (const spec of CATALOG) {
213
- const gate = (allowed, reason) => {
214
- out[spec.id] = Object.freeze({ id: spec.id, allowed, reason });
215
- };
216
-
217
- if (!isPlainObject(caps) || !flags) {
218
- // Fail silent: an unreadable capabilities object offers nothing.
219
- gate(false, 'no capabilities — the companion stays quiet');
220
- continue;
221
- }
222
- if (opts.envDisabled === true) {
223
- gate(false, 'AEGIS_COMPANION=off');
224
- continue;
225
- }
226
- if (PROACTIVITY_ORDER.indexOf(proactivity) < 0) {
227
- gate(false, `unknown proactivity ${JSON.stringify(proactivity)} — staying quiet`);
228
- continue;
229
- }
230
- if (!atLeast(proactivity, spec.needsProactivity)) {
231
- gate(false, `${spec.label} unlocks at level ${gateLevelFor(spec)}, not level ${caps.level}`);
232
- continue;
233
- }
234
- if (!flagAllowed(flags, spec)) {
235
- gate(false, `${spec.label} unlocks at level ${gateLevelFor(spec)}`);
236
- continue;
237
- }
238
- if (optIn[spec.id] !== true) {
239
- gate(false, `${spec.label} is off (opt-in, default off)`);
240
- continue;
241
- }
242
- gate(true, 'available');
243
- }
244
- return Object.freeze(out);
245
- }
246
-
247
- function flagAllowed(flags, spec) {
248
- if (spec.id === 'recall') return true; // breadth, not a flag: the gate is proactivity
249
- return flags[spec.id] === true;
250
- }
251
-
252
- function gateLevelFor(spec) {
253
- if (spec.id === 'morningBrief') return GATES.brief;
254
- if (spec.id === 'ideaLog') return GATES.ideaLog;
255
- if (spec.id === 'sessionReflection') return GATES.sessionReflection;
256
- return GATES.hints;
257
- }
258
-
259
- // ------------------------------------------------------------------ effects
260
-
261
- /**
262
- * Which carve-outs this module's effect table violates. Empty array = safe.
263
- * Mirrors `level.js`'s `carveOuts()` shape so main can assert both at boot.
264
- */
265
- function carveOuts() {
266
- const violations = [];
267
- for (const spec of CATALOG) {
268
- const effect = EFFECTS[spec.action];
269
- if (!effect) {
270
- violations.push(`${spec.id} proposes undeclared effect ${spec.action}`);
271
- continue;
272
- }
273
- if (effect.offDevice) violations.push(`${spec.id} (${spec.action}) reaches off-device`);
274
- if (FORBIDDEN_CLASSES.includes(effect.class)) {
275
- violations.push(`${spec.id} (${spec.action}) carries ${effect.class} approval class`);
276
- }
277
- if (effect.tool && !effect.tool.startsWith('memory.')) {
278
- violations.push(`${spec.id} (${spec.action}) fires tool ${effect.tool}, which is not a memory read`);
279
- }
280
- }
281
- return violations;
282
- }
283
-
284
- function assertCarveOuts() {
285
- const bad = carveOuts();
286
- if (bad.length) throw new Error(`companion behaviours violate a carve-out: ${bad.join('; ')}`);
287
- return true;
288
- }
289
-
290
- // -------------------------------------------------------------------- facts
291
-
292
- /**
293
- * Shape a morning-brief digest out of facts the *caller* gathered locally.
294
- *
295
- * This module never looks anything up, so this function is pure formatting: the
296
- * caller (main, which already has `lib/local/git-scope.js` and
297
- * `lib/local/queue.js` in hand) reads them and passes the results in. That is
298
- * what keeps an unprompted card from being an unprompted *tool call*.
299
- *
300
- * @param {object} [facts]
301
- * @param {string[]|number} [facts.changed] changed paths (or a count)
302
- * @param {string[]|number} [facts.queued] queued task summaries (or a count)
303
- * @param {string[]} [facts.resuming] memory lines the last session was mid-way through
304
- */
305
- function briefDigest(facts = {}) {
306
- const lines = [];
307
- const count = (v) => (Array.isArray(v) ? v.length : Number.isFinite(v) ? Number(v) : 0);
308
-
309
- const changed = count(facts.changed);
310
- if (changed > 0) {
311
- const sample = Array.isArray(facts.changed) ? facts.changed.slice(0, 2).map((f) => clean(f, 60)) : [];
312
- lines.push(`${changed} file${changed === 1 ? '' : 's'} changed since you were last here${sample.length ? ` (${sample.join(', ')})` : ''}`);
313
- }
314
- const queued = count(facts.queued);
315
- if (queued > 0) lines.push(`${queued} task${queued === 1 ? '' : 's'} waiting in the queue`);
316
-
317
- const resuming = (Array.isArray(facts.resuming) ? facts.resuming : []).map((r) => clean(r, MAX_FACT_CHARS)).filter(Boolean).slice(0, 3);
318
- if (resuming.length) lines.push(`memory says you were mid-way through: ${resuming.join(' · ')}`);
319
-
320
- return Object.freeze({
321
- lines: Object.freeze(lines.slice(0, MAX_FACTS)),
322
- empty: lines.length === 0,
323
- });
324
- }
325
-
326
- /** Preview lines for a recall card — ids and text only, bounded. */
327
- function recallCandidates(candidates) {
328
- const list = Array.isArray(candidates) ? candidates : [];
329
- return list
330
- .filter((c) => c && (c.content != null || c.text != null))
331
- .slice(0, MAX_CANDIDATES)
332
- .map((c) => Object.freeze({
333
- id: c.id == null ? null : String(c.id),
334
- text: clean(c.content != null ? c.content : c.text, MAX_FACT_CHARS),
335
- }))
336
- .filter((c) => c.text.length > 0);
337
- }
338
-
339
- // -------------------------------------------------------------- card builder
340
-
341
- function newId(prefix, seq) {
342
- return `${prefix}-${seq}`;
343
- }
344
-
345
- function makeIntent(spec, args) {
346
- const effect = EFFECTS[spec.action];
347
- return Object.freeze({
348
- behaviour: spec.id,
349
- action: spec.action,
350
- approvalClass: effect.class,
351
- tool: effect.tool,
352
- offDevice: effect.offDevice,
353
- args: Object.freeze(Object.assign({}, args || {})),
354
- // A proposal is never pre-approved. Only `decide(id, 'accept')` mints a
355
- // ticket, and `dispatch()` accepts nothing else.
356
- approved: false,
357
- needsInteractiveApproval: effect.tool != null,
358
- });
359
- }
360
-
361
- function makeCard(spec, opts) {
362
- const actions = Object.freeze([
363
- Object.freeze({ id: 'accept', decision: 'accept', label: spec.acceptLabel, doneLabel: spec.doneLabel, primary: true }),
364
- Object.freeze({ id: 'dismiss', decision: 'dismiss', label: spec.dismissLabel, doneLabel: 'Not now', primary: false }),
365
- ]);
366
- return Object.freeze({
367
- id: opts.id,
368
- // The renderer shapes this exactly like the tool approval card
369
- // (`renderer/avatar/cards.js`), which is the whole point: it is a UI the
370
- // user has already learned to read.
371
- kind: 'companion',
372
- shape: 'approval',
373
- behaviour: spec.id,
374
- label: spec.label,
375
- proactive: opts.proactive === true,
376
- title: opts.title,
377
- body: opts.body,
378
- facts: Object.freeze((opts.facts || []).slice(0, MAX_FACTS)),
379
- actions,
380
- requiresExplicitAccept: true,
381
- intent: opts.intent,
382
- });
383
- }
384
-
385
- // ------------------------------------------------------------------- session
386
-
387
- /**
388
- * One session's worth of companion state. Everything the behaviours need to
389
- * stay quiet lives here — the proactive budget, which cards were shown, which
390
- * were accepted, and every ticket ever issued — so "at most one proactive card"
391
- * and "no effect without an accepted card" are properties of a single object
392
- * rather than of a caller's discipline.
393
- *
394
- * @param {object} [input]
395
- * @param {object} input.caps `level.capabilities(n)` (required to do anything)
396
- * @param {object} [input.optIn] per-behaviour opt-in; default all off
397
- * @param {string} [input.session] session id (stamped on tickets)
398
- * @param {function} [input.now] clock, injected (never `Date.now` inside)
399
- * @param {boolean} [input.envDisabled] `AEGIS_COMPANION=off`
400
- */
401
- function createSession(input = {}) {
402
- const caps = input.caps;
403
- const optIn = normalizeOptIn(input.optIn);
404
- const sessionId = input.session == null ? 'session' : String(input.session);
405
- const now = typeof input.now === 'function' ? input.now : () => 0;
406
- const gates = gating(caps, { optIn, envDisabled: input.envDisabled === true });
407
-
408
- const cards = new Map();
409
- const tickets = new Map();
410
- const decisions = [];
411
- let seq = 0;
412
- let proactiveShown = 0;
413
- let lastBriefDay = input.lastBriefDay == null ? null : String(input.lastBriefDay);
414
-
415
- function gateOf(id) {
416
- return gates[id] || Object.freeze({ id, allowed: false, reason: 'unknown behaviour' });
417
- }
418
-
419
- function budgetLeft() {
420
- return proactiveShown < MAX_PROACTIVE_PER_SESSION;
421
- }
422
-
423
- /** Common front door for every behaviour: gate, then budget, then nothing. */
424
- function offer(spec, trigger, builders) {
425
- const gate = gateOf(spec.id);
426
- if (!gate.allowed) return { card: null, reason: gate.reason };
427
- const proactive = trigger.kind !== 'user.request';
428
- if (proactive && !budgetLeft()) {
429
- return { card: null, reason: 'already offered a proactive card this session' };
430
- }
431
- const built = builders();
432
- if (!built) return { card: null, reason: 'nothing to report' };
433
- seq += 1;
434
- const card = makeCard(spec, Object.assign({ id: newId('companion', seq), proactive }, built));
435
- cards.set(card.id, { card, decision: null, decidedAt: null });
436
- if (proactive) proactiveShown += 1;
437
- return { card, reason: 'offered' };
438
- }
439
-
440
- function considerMorningBrief(trigger, spec) {
441
- const day = trigger.dayKey == null ? null : String(trigger.dayKey);
442
- if (day && day === lastBriefDay) return { card: null, reason: 'already briefed today' };
443
- const digest = briefDigest(trigger.facts);
444
- if (digest.empty) return { card: null, reason: 'nothing to report' };
445
- return offer(spec, trigger, () => {
446
- if (day) lastBriefDay = day;
447
- return {
448
- title: 'Morning brief',
449
- body: digest.lines.join(' '),
450
- facts: digest.lines,
451
- intent: makeIntent(spec, { lines: digest.lines.slice() }),
452
- };
453
- });
454
- }
455
-
456
- function considerRecall(trigger, spec) {
457
- const candidates = recallCandidates(trigger.candidates);
458
- if (!candidates.length) return { card: null, reason: 'no remembered context for this turn' };
459
- const query = clean(trigger.query, 120);
460
- return offer(spec, trigger, () => ({
461
- title: 'Context recall',
462
- body: `${candidates.length} remembered note${candidates.length === 1 ? '' : 's'} may be relevant${query ? ` to “${query}”` : ''}. Load them into this turn?`,
463
- facts: candidates.map((c) => c.text),
464
- intent: makeIntent(spec, {
465
- query,
466
- ids: candidates.map((c) => c.id),
467
- limit: candidates.length,
468
- }),
469
- }));
470
- }
471
-
472
- function considerReflection(trigger, spec) {
473
- const decisions_ = (Array.isArray(trigger.decisions) ? trigger.decisions : []).map((d) => clean(d, MAX_FACT_CHARS)).filter(Boolean);
474
- const open = (Array.isArray(trigger.open) ? trigger.open : []).map((o) => clean(o, MAX_FACT_CHARS)).filter(Boolean);
475
- if (!decisions_.length && !open.length) return { card: null, reason: 'nothing to report' };
476
- const line =
477
- decisions_.length && open.length
478
- ? `Decided: ${decisions_[0]} · Still open: ${open[0]}`
479
- : decisions_.length
480
- ? `Decided: ${decisions_[0]}`
481
- : `Still open: ${open[0]}`;
482
- return offer(spec, trigger, () => ({
483
- title: 'Session reflection',
484
- body: `One durable line from this session — save it? ${line}`,
485
- facts: [line],
486
- intent: makeIntent(spec, {
487
- kind: 'reflection',
488
- content: clean(line, MAX_BODY_CHARS),
489
- session: sessionId,
490
- decisions: decisions_.slice(0, MAX_FACTS),
491
- open: open.slice(0, MAX_FACTS),
492
- }),
493
- }));
494
- }
495
-
496
- function considerIdea(trigger, spec) {
497
- const text = clean(trigger.text, MAX_BODY_CHARS);
498
- if (!text) return { card: null, reason: 'no idea text to save' };
499
- return offer(spec, trigger, () => ({
500
- title: 'Idea log',
501
- body: `Save this to memory as an idea? ${text}`,
502
- facts: [text],
503
- intent: makeIntent(spec, { kind: 'idea', content: text, session: sessionId }),
504
- }));
505
- }
506
-
507
- /**
508
- * The one entry point: hand the companion a real engine trigger, get either a
509
- * card or a reason. It never performs anything, and calling it twice with the
510
- * same trigger cannot double-charge a behaviour (the proactive budget).
511
- *
512
- * @param {object} trigger
513
- * @param {'session.start'|'turn.start'|'session.end'|'user.request'} trigger.kind
514
- * @returns {{card: object|null, reason: string}}
515
- */
516
- function consider(trigger = {}) {
517
- const t = isPlainObject(trigger) ? trigger : {};
518
- switch (t.kind) {
519
- case 'session.start':
520
- return considerMorningBrief(t, behaviourById('morningBrief'));
521
- case 'turn.start':
522
- return considerRecall(t, behaviourById('recall'));
523
- case 'session.end':
524
- return considerReflection(t, behaviourById('sessionReflection'));
525
- case 'user.request':
526
- if (t.want === 'idea') return considerIdea(t, behaviourById('ideaLog'));
527
- if (t.want === 'recall') return considerRecall(t, behaviourById('recall'));
528
- if (t.want === 'brief') return considerMorningBrief(t, behaviourById('morningBrief'));
529
- if (t.want === 'reflection') return considerReflection(t, behaviourById('sessionReflection'));
530
- return { card: null, reason: `unsupported request ${JSON.stringify(t.want)}` };
531
- default:
532
- return { card: null, reason: `unknown trigger ${JSON.stringify(t.kind)}` };
533
- }
534
- }
535
-
536
- /**
537
- * Resolve one card. `'accept'` mints a single-use ticket; `'dismiss'` mints
538
- * nothing. A second decision on the same card is refused, so a double click
539
- * cannot produce two effects.
540
- */
541
- function decide(cardId, decision) {
542
- const rec = cards.get(String(cardId));
543
- if (!rec) return { ok: false, reason: 'unknown card', decision: null, ticket: null, intent: null };
544
- if (rec.decision) return { ok: false, reason: `already ${rec.decision}ed`, decision: rec.decision, ticket: null, intent: null };
545
- if (decision !== 'accept' && decision !== 'dismiss') {
546
- return { ok: false, reason: 'decision must be "accept" or "dismiss"', decision: null, ticket: null, intent: null };
547
- }
548
- rec.decision = decision;
549
- rec.decidedAt = now();
550
- decisions.push(Object.freeze({ cardId: rec.card.id, behaviour: rec.card.behaviour, decision, at: rec.decidedAt }));
551
- if (decision === 'dismiss') return { ok: true, reason: 'dismissed', decision: 'dismiss', ticket: null, intent: null };
552
-
553
- const ticket = Object.freeze({
554
- id: `ticket-${sessionId}-${rec.card.id}`,
555
- cardId: rec.card.id,
556
- behaviour: rec.card.behaviour,
557
- session: sessionId,
558
- approved: true,
559
- approvedAt: rec.decidedAt,
560
- intent: rec.card.intent,
561
- });
562
- tickets.set(ticket.id, { ticket, used: false });
563
- return { ok: true, reason: 'accepted', decision: 'accept', ticket, intent: rec.card.intent };
564
- }
565
-
566
- /**
567
- * Was this ticket issued by *this* session, for a card that was accepted?
568
- * Identity, not shape: a hand-built `{ approved: true, … }` object has no
569
- * entry in this session's map, so it verifies false. Spending is a separate
570
- * step (`consumeTicket`) so a replay is reported as a replay rather than as a
571
- * missing acceptance.
572
- */
573
- function verifyTicket(ticket) {
574
- if (!isPlainObject(ticket) || typeof ticket.id !== 'string') return false;
575
- const rec = tickets.get(ticket.id);
576
- return Boolean(rec) && rec.ticket === ticket;
577
- }
578
-
579
- function consumeTicket(ticket) {
580
- const rec = tickets.get(ticket.id);
581
- if (!rec || rec.ticket !== ticket || rec.used) return false;
582
- rec.used = true;
583
- return true;
584
- }
585
-
586
- return {
587
- session: sessionId,
588
- capabilities: caps,
589
- optIn,
590
- // The computed permission table for this session (not the module function).
591
- gates,
592
- consider,
593
- decide,
594
- verifyTicket,
595
- consumeTicket,
596
- /** Cards offered so far (resolved or not), in order. */
597
- cards: () => Array.from(cards.values()).map((r) => r.card),
598
- pending: () => Array.from(cards.values()).filter((r) => !r.decision).map((r) => r.card),
599
- decisions: () => decisions.slice(),
600
- state: () => Object.freeze({
601
- session: sessionId,
602
- proactiveShown,
603
- maxProactive: MAX_PROACTIVE_PER_SESSION,
604
- budgetLeft: budgetLeft(),
605
- lastBriefDay,
606
- cards: cards.size,
607
- pending: Array.from(cards.values()).filter((r) => !r.decision).length,
608
- enabled: BEHAVIOUR_IDS.filter((id) => gateOf(id).allowed),
609
- }),
610
- };
611
- }
612
-
613
- // ------------------------------------------------------------------ dispatch
614
-
615
- function refuse(reason) {
616
- return Object.freeze({ ok: false, performed: false, reason });
617
- }
618
-
619
- /**
620
- * Turn an accepted card into an effect — the ONLY function in this module that
621
- * can reach a host, and the only place a tool could ever be fired.
622
- *
623
- * The gates are ordered so that the cheapest refusal wins, and so that no gate
624
- * can be satisfied by level, capabilities or opt-in:
625
- *
626
- * 1. the ticket must have been issued by this session for this card
627
- * (`verifyTicket`), or there is no accepted card and nothing happens;
628
- * 2. the ticket is single-use (`consumeTicket`) — a replayed accept, a
629
- * re-offered card or a double click cannot fire twice;
630
- * 3. an intent carrying a `tool` needs the host's approval channel to answer
631
- * `once` or `session`. No channel, a deny, or a non-answer ⇒ refused;
632
- * 4. only then is `host.perform(intent)` called.
633
- *
634
- * @param {object} ticket from `session.decide(cardId, 'accept')`
635
- * @param {object} host `{ requestApproval?, perform }` — injected by main
636
- * @returns {Promise<{ok: boolean, performed: boolean, reason?: string, result?: any}>}
637
- */
638
- async function dispatch(session, ticket, host = {}) {
639
- if (!session || typeof session.verifyTicket !== 'function') {
640
- return refuse('no companion session — refusing to act');
641
- }
642
- if (!session.verifyTicket(ticket)) {
643
- return refuse('an accepted card is required — the companion never acts on its own');
644
- }
645
- if (typeof session.consumeTicket !== 'function' || !session.consumeTicket(ticket)) {
646
- return refuse('this acceptance was already used');
647
- }
648
-
649
- const intent = ticket.intent;
650
- if (!isPlainObject(intent) || intent.approved !== false) {
651
- return refuse('malformed intent — refusing to act');
652
- }
653
- const effect = EFFECTS[intent.action];
654
- if (!effect) return refuse(`undeclared effect ${JSON.stringify(intent.action)}`);
655
-
656
- if (intent.tool) {
657
- if (typeof host.requestApproval !== 'function') {
658
- return refuse(`no approval channel for ${intent.tool} — refusing to fire a tool`);
659
- }
660
- let decision;
661
- try {
662
- decision = await host.requestApproval({
663
- id: ticket.id,
664
- behaviour: ticket.behaviour,
665
- tool: intent.tool,
666
- args: intent.args,
667
- approvalClass: intent.approvalClass,
668
- });
669
- } catch (err) {
670
- return refuse(`approval channel failed: ${err && err.message ? err.message : err}`);
671
- }
672
- if (decision !== 'once' && decision !== 'session') {
673
- return refuse(`approval denied for ${intent.tool}`);
674
- }
675
- }
676
-
677
- if (typeof host.perform !== 'function') return refuse('no effect handler configured');
678
- const result = await host.perform(intent, { ticket });
679
- return Object.freeze({ ok: true, performed: true, behaviour: ticket.behaviour, action: intent.action, result });
680
- }
681
-
682
- module.exports = {
683
- GATES,
684
- MAX_PROACTIVE_PER_SESSION,
685
- MAX_BODY_CHARS,
686
- BEHAVIOUR_IDS,
687
- DEFAULT_OPT_IN,
688
- CATALOG,
689
- EFFECTS,
690
- briefDigest,
691
- recallCandidates,
692
- normalizeOptIn,
693
- gating,
694
- carveOuts,
695
- assertCarveOuts,
696
- behaviourById,
697
- createSession,
698
- dispatch,
699
- clean,
700
- };
3
+ "use strict";const level=require("./level.js"),GATES=level.GATES,MAX_PROACTIVE_PER_SESSION=1,MAX_BODY_CHARS=600,MAX_FACT_CHARS=160,MAX_FACTS=6,MAX_CANDIDATES=8,EFFECTS=Object.freeze({"brief.show":Object.freeze({class:null,tool:null,offDevice:!1}),"memory.save":Object.freeze({class:"write",tool:null,offDevice:!1}),"recall.load":Object.freeze({class:"read",tool:"memory.search",offDevice:!1})}),FORBIDDEN_CLASSES=Object.freeze(["shell","network","offDevice"]),CATALOG=Object.freeze([Object.freeze({id:"morningBrief",label:"Morning brief",needsProactivity:"brief",proactive:!0,action:"brief.show",acceptLabel:"Show the brief",dismissLabel:"Not now",doneLabel:"Brief opened"}),Object.freeze({id:"ideaLog",label:"Idea log",needsProactivity:"off",proactive:!1,action:"memory.save",acceptLabel:"Save to memory",dismissLabel:"Discard",doneLabel:"Saved to memory"}),Object.freeze({id:"sessionReflection",label:"Session reflection",needsProactivity:"hints",proactive:!0,action:"memory.save",acceptLabel:"Save this line",dismissLabel:"Discard",doneLabel:"Saved to memory"}),Object.freeze({id:"recall",label:"Context recall",needsProactivity:"hints",proactive:!0,action:"recall.load",acceptLabel:"Load those notes",dismissLabel:"Skip",doneLabel:"Notes loaded"})]),BEHAVIOUR_IDS=Object.freeze(CATALOG.map((e=>e.id))),DEFAULT_OPT_IN=Object.freeze(BEHAVIOUR_IDS.reduce(((e,n)=>Object.assign(e,{[n]:!1})),{})),PROACTIVITY_ORDER=Object.freeze(["off","hints","brief"]);function isPlainObject(e){return Boolean(e)&&"object"==typeof e&&!Array.isArray(e)}function clean(e,n){const t=String(null==e?"":e).replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f\u2028\u2029]/g," ").replace(/\s+/g," ").trim(),i="number"==typeof n&&n>0?n:MAX_FACT_CHARS;return t.length>i?`${t.slice(0,i-1)}…`:t}function atLeast(e,n){const t=PROACTIVITY_ORDER.indexOf(e),i=PROACTIVITY_ORDER.indexOf(n);return!(t<0)&&(i<0||t>=i)}function behaviourById(e){return CATALOG.find((n=>n.id===e))||null}function capsFlags(e){return isPlainObject(e)&&isPlainObject(e.companion)?e.companion:null}function normalizeOptIn(e){const n=Object.assign({},DEFAULT_OPT_IN);if(!isPlainObject(e))return Object.freeze(n);for(const t of BEHAVIOUR_IDS)!0===e[t]&&(n[t]=!0);return Object.freeze(n)}function gating(e,n={}){const t=normalizeOptIn(n.optIn),i=capsFlags(e),r=isPlainObject(e)&&"string"==typeof e.proactivity?e.proactivity:"off",o={};for(const a of CATALOG){const s=(e,n)=>{o[a.id]=Object.freeze({id:a.id,allowed:e,reason:n})};isPlainObject(e)&&i?!0!==n.envDisabled?PROACTIVITY_ORDER.indexOf(r)<0?s(!1,`unknown proactivity ${JSON.stringify(r)} — staying quiet`):atLeast(r,a.needsProactivity)?flagAllowed(i,a)?!0===t[a.id]?s(!0,"available"):s(!1,`${a.label} is off (opt-in, default off)`):s(!1,`${a.label} unlocks at level ${gateLevelFor(a)}`):s(!1,`${a.label} unlocks at level ${gateLevelFor(a)}, not level ${e.level}`):s(!1,"AEGIS_COMPANION=off"):s(!1,"no capabilities — the companion stays quiet")}return Object.freeze(o)}function flagAllowed(e,n){return"recall"===n.id||!0===e[n.id]}function gateLevelFor(e){return"morningBrief"===e.id?GATES.brief:"ideaLog"===e.id?GATES.ideaLog:"sessionReflection"===e.id?GATES.sessionReflection:GATES.hints}function carveOuts(){const e=[];for(const n of CATALOG){const t=EFFECTS[n.action];t?(t.offDevice&&e.push(`${n.id} (${n.action}) reaches off-device`),FORBIDDEN_CLASSES.includes(t.class)&&e.push(`${n.id} (${n.action}) carries ${t.class} approval class`),t.tool&&!t.tool.startsWith("memory.")&&e.push(`${n.id} (${n.action}) fires tool ${t.tool}, which is not a memory read`)):e.push(`${n.id} proposes undeclared effect ${n.action}`)}return e}function assertCarveOuts(){const e=carveOuts();if(e.length)throw new Error(`companion behaviours violate a carve-out: ${e.join("; ")}`);return!0}function briefDigest(e={}){const n=[],t=e=>Array.isArray(e)?e.length:Number.isFinite(e)?Number(e):0,i=t(e.changed);if(i>0){const t=Array.isArray(e.changed)?e.changed.slice(0,2).map((e=>clean(e,60))):[];n.push(`${i} file${1===i?"":"s"} changed since you were last here${t.length?` (${t.join(", ")})`:""}`)}const r=t(e.queued);r>0&&n.push(`${r} task${1===r?"":"s"} waiting in the queue`);const o=(Array.isArray(e.resuming)?e.resuming:[]).map((e=>clean(e,MAX_FACT_CHARS))).filter(Boolean).slice(0,3);return o.length&&n.push(`memory says you were mid-way through: ${o.join(" · ")}`),Object.freeze({lines:Object.freeze(n.slice(0,MAX_FACTS)),empty:0===n.length})}function recallCandidates(e){return(Array.isArray(e)?e:[]).filter((e=>e&&(null!=e.content||null!=e.text))).slice(0,MAX_CANDIDATES).map((e=>Object.freeze({id:null==e.id?null:String(e.id),text:clean(null!=e.content?e.content:e.text,MAX_FACT_CHARS)}))).filter((e=>e.text.length>0))}function newId(e,n){return`${e}-${n}`}function makeIntent(e,n){const t=EFFECTS[e.action];return Object.freeze({behaviour:e.id,action:e.action,approvalClass:t.class,tool:t.tool,offDevice:t.offDevice,args:Object.freeze(Object.assign({},n||{})),approved:!1,needsInteractiveApproval:null!=t.tool})}function makeCard(e,n){const t=Object.freeze([Object.freeze({id:"accept",decision:"accept",label:e.acceptLabel,doneLabel:e.doneLabel,primary:!0}),Object.freeze({id:"dismiss",decision:"dismiss",label:e.dismissLabel,doneLabel:"Not now",primary:!1})]);return Object.freeze({id:n.id,kind:"companion",shape:"approval",behaviour:e.id,label:e.label,proactive:!0===n.proactive,title:n.title,body:n.body,facts:Object.freeze((n.facts||[]).slice(0,MAX_FACTS)),actions:t,requiresExplicitAccept:!0,intent:n.intent})}function createSession(e={}){const n=e.caps,t=normalizeOptIn(e.optIn),i=null==e.session?"session":String(e.session),r="function"==typeof e.now?e.now:()=>0,o=gating(n,{optIn:t,envDisabled:!0===e.envDisabled}),a=new Map,s=new Map,c=[];let l=0,d=0,u=null==e.lastBriefDay?null:String(e.lastBriefDay);function f(e){return o[e]||Object.freeze({id:e,allowed:!1,reason:"unknown behaviour"})}function p(){return d<1}function b(e,n,t){const i=f(e.id);if(!i.allowed)return{card:null,reason:i.reason};const r="user.request"!==n.kind;if(r&&!p())return{card:null,reason:"already offered a proactive card this session"};const o=t();if(!o)return{card:null,reason:"nothing to report"};l+=1;const s=makeCard(e,Object.assign({id:newId("companion",l),proactive:r},o));return a.set(s.id,{card:s,decision:null,decidedAt:null}),r&&(d+=1),{card:s,reason:"offered"}}function A(e,n){const t=null==e.dayKey?null:String(e.dayKey);if(t&&t===u)return{card:null,reason:"already briefed today"};const i=briefDigest(e.facts);return i.empty?{card:null,reason:"nothing to report"}:b(n,e,(()=>(t&&(u=t),{title:"Morning brief",body:i.lines.join(" "),facts:i.lines,intent:makeIntent(n,{lines:i.lines.slice()})})))}function v(e,n){const t=recallCandidates(e.candidates);if(!t.length)return{card:null,reason:"no remembered context for this turn"};const i=clean(e.query,120);return b(n,e,(()=>({title:"Context recall",body:`${t.length} remembered note${1===t.length?"":"s"} may be relevant${i?` to “${i}”`:""}. Load them into this turn?`,facts:t.map((e=>e.text)),intent:makeIntent(n,{query:i,ids:t.map((e=>e.id)),limit:t.length})})))}function m(e,n){const t=(Array.isArray(e.decisions)?e.decisions:[]).map((e=>clean(e,MAX_FACT_CHARS))).filter(Boolean),r=(Array.isArray(e.open)?e.open:[]).map((e=>clean(e,MAX_FACT_CHARS))).filter(Boolean);if(!t.length&&!r.length)return{card:null,reason:"nothing to report"};const o=t.length&&r.length?`Decided: ${t[0]} · Still open: ${r[0]}`:t.length?`Decided: ${t[0]}`:`Still open: ${r[0]}`;return b(n,e,(()=>({title:"Session reflection",body:`One durable line from this session — save it? ${o}`,facts:[o],intent:makeIntent(n,{kind:"reflection",content:clean(o,600),session:i,decisions:t.slice(0,MAX_FACTS),open:r.slice(0,MAX_FACTS)})})))}return{session:i,capabilities:n,optIn:t,gates:o,consider:function(e={}){const n=isPlainObject(e)?e:{};switch(n.kind){case"session.start":return A(n,behaviourById("morningBrief"));case"turn.start":return v(n,behaviourById("recall"));case"session.end":return m(n,behaviourById("sessionReflection"));case"user.request":return"idea"===n.want?function(e,n){const t=clean(e.text,600);return t?b(n,e,(()=>({title:"Idea log",body:`Save this to memory as an idea? ${t}`,facts:[t],intent:makeIntent(n,{kind:"idea",content:t,session:i})}))):{card:null,reason:"no idea text to save"}}(n,behaviourById("ideaLog")):"recall"===n.want?v(n,behaviourById("recall")):"brief"===n.want?A(n,behaviourById("morningBrief")):"reflection"===n.want?m(n,behaviourById("sessionReflection")):{card:null,reason:`unsupported request ${JSON.stringify(n.want)}`};default:return{card:null,reason:`unknown trigger ${JSON.stringify(n.kind)}`}}},decide:function(e,n){const t=a.get(String(e));if(!t)return{ok:!1,reason:"unknown card",decision:null,ticket:null,intent:null};if(t.decision)return{ok:!1,reason:`already ${t.decision}ed`,decision:t.decision,ticket:null,intent:null};if("accept"!==n&&"dismiss"!==n)return{ok:!1,reason:'decision must be "accept" or "dismiss"',decision:null,ticket:null,intent:null};if(t.decision=n,t.decidedAt=r(),c.push(Object.freeze({cardId:t.card.id,behaviour:t.card.behaviour,decision:n,at:t.decidedAt})),"dismiss"===n)return{ok:!0,reason:"dismissed",decision:"dismiss",ticket:null,intent:null};const o=Object.freeze({id:`ticket-${i}-${t.card.id}`,cardId:t.card.id,behaviour:t.card.behaviour,session:i,approved:!0,approvedAt:t.decidedAt,intent:t.card.intent});return s.set(o.id,{ticket:o,used:!1}),{ok:!0,reason:"accepted",decision:"accept",ticket:o,intent:t.card.intent}},verifyTicket:function(e){if(!isPlainObject(e)||"string"!=typeof e.id)return!1;const n=s.get(e.id);return Boolean(n)&&n.ticket===e},consumeTicket:function(e){const n=s.get(e.id);return!(!n||n.ticket!==e||n.used||(n.used=!0,0))},cards:()=>Array.from(a.values()).map((e=>e.card)),pending:()=>Array.from(a.values()).filter((e=>!e.decision)).map((e=>e.card)),decisions:()=>c.slice(),state:()=>Object.freeze({session:i,proactiveShown:d,maxProactive:1,budgetLeft:p(),lastBriefDay:u,cards:a.size,pending:Array.from(a.values()).filter((e=>!e.decision)).length,enabled:BEHAVIOUR_IDS.filter((e=>f(e).allowed))})}}function refuse(e){return Object.freeze({ok:!1,performed:!1,reason:e})}async function dispatch(e,n,t={}){if(!e||"function"!=typeof e.verifyTicket)return refuse("no companion session — refusing to act");if(!e.verifyTicket(n))return refuse("an accepted card is required — the companion never acts on its own");if("function"!=typeof e.consumeTicket||!e.consumeTicket(n))return refuse("this acceptance was already used");const i=n.intent;if(!isPlainObject(i)||!1!==i.approved)return refuse("malformed intent — refusing to act");if(!EFFECTS[i.action])return refuse(`undeclared effect ${JSON.stringify(i.action)}`);if(i.tool){if("function"!=typeof t.requestApproval)return refuse(`no approval channel for ${i.tool} — refusing to fire a tool`);let e;try{e=await t.requestApproval({id:n.id,behaviour:n.behaviour,tool:i.tool,args:i.args,approvalClass:i.approvalClass})}catch(e){return refuse(`approval channel failed: ${e&&e.message?e.message:e}`)}if("once"!==e&&"session"!==e)return refuse(`approval denied for ${i.tool}`)}if("function"!=typeof t.perform)return refuse("no effect handler configured");const r=await t.perform(i,{ticket:n});return Object.freeze({ok:!0,performed:!0,behaviour:n.behaviour,action:i.action,result:r})}module.exports={GATES:GATES,MAX_PROACTIVE_PER_SESSION:1,MAX_BODY_CHARS:600,BEHAVIOUR_IDS:BEHAVIOUR_IDS,DEFAULT_OPT_IN:DEFAULT_OPT_IN,CATALOG:CATALOG,EFFECTS:EFFECTS,briefDigest:briefDigest,recallCandidates:recallCandidates,normalizeOptIn:normalizeOptIn,gating:gating,carveOuts:carveOuts,assertCarveOuts:assertCarveOuts,behaviourById:behaviourById,createSession:createSession,dispatch:dispatch,clean:clean};