friend-assistant 0.1.46 → 0.1.47
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 +2 -0
- package/dist/BUILD.json +2 -2
- package/dist/friend.mjs +211 -14
- package/dist/web/assets/{index-CbiARSIY.js → index-BmkXgcUH.js} +23 -23
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -141,6 +141,8 @@ Connectors (Gmail, Chrome control, any MCP service) are managed inside the app,
|
|
|
141
141
|
| `openai.url`, `openai.model`, `openai.apiKey`, `openai.vision` | An OpenAI-compatible server: its address (ending in `/v1`), the model name it knows, its API key (or empty), and whether the model can see pictures. For OmniRoute (https://github.com/diegosouzapw/OmniRoute) run `npm install -g omniroute` and `omniroute`, then `friend config set provider openai` (the defaults are OmniRoute's: `http://localhost:20128/v1` and model `auto`). Also works with LM Studio, LiteLLM, vLLM and OpenRouter. |
|
|
142
142
|
| `voice.hindiVoice`, `voice.marathiVoice` | The voices that read Hindi and Marathi (Devanagari) text: `hi_IN-priyamvada-medium`, `hi_IN-pratham-medium`, `hi_IN-rohan-medium`, `mr_IN-google-medium`. Empty makes the main voice read that language. See "Hindi, Marathi and English together". |
|
|
143
143
|
| `updates.auto` | Look for a newer Friend shortly after it starts and every few hours, and install it by itself while Friend is not busy (only a Friend running in the background restarts itself). Off: Friend only tells you a new version is out, and you run `friend upgrade` or press Update now in Settings → Startup. |
|
|
144
|
+
| `session.autoCompact` | With Claude Code, Friend keeps one long conversation and re-reads all of it for every answer. On (default): when it grows past `session.compactAtTokens`, Friend writes short notes on it and starts a fresh conversation from them before the next answer. Off: it only tidies when you press Tidy up now. |
|
|
145
|
+
| `session.compactAtTokens` | How big the conversation may grow before it is tidied up by itself, in tokens (50000 to 800000, default 100000). A fresh conversation already starts near 36000. |
|
|
144
146
|
| `voice.enabled`, `voice.sttModel`, `voice.ttsVoice`, `voice.language`, `voice.speakTyped`, `voice.duck`, `voice.duckPercent`, `voice.accept`, `voice.challenge` | Speech on or off, the listening model, the speaking voice, a forced language, speaking typed replies, turning music and video down while you talk and while Friend speaks (and how far, in percent), and the two thresholds for recognising your voice. |
|
|
145
147
|
| `bind`, `port` | Where Friend listens (easier: `friend lan`). |
|
|
146
148
|
| `https` | `true` serves a secure `https://` address with a certificate Friend makes for itself, so a phone may use the microphone (easier: `friend lan https on`). |
|
package/dist/BUILD.json
CHANGED
package/dist/friend.mjs
CHANGED
|
@@ -2322,6 +2322,43 @@ function lexicalInsideRoots(path3, roots) {
|
|
|
2322
2322
|
});
|
|
2323
2323
|
}
|
|
2324
2324
|
|
|
2325
|
+
// ../server/src/agent/compact.ts
|
|
2326
|
+
var SYSTEM = "You write short notes for an assistant named Friend, so that it can carry on a long conversation after its memory of it is cleared. Write at most 250 words of plain text, with no preamble. Keep what the owner is working on, decisions made, preferences, names, things promised and tasks still open. Leave out greetings, small talk and anything already finished. Do not invent anything.";
|
|
2327
|
+
var MAX_LINE = 1500;
|
|
2328
|
+
var MAX_TEXT = 3e4;
|
|
2329
|
+
var MAX_SUMMARY = 4e3;
|
|
2330
|
+
function conversationText(history) {
|
|
2331
|
+
const lines = [];
|
|
2332
|
+
for (const m of history) {
|
|
2333
|
+
if (m.role === "tool") continue;
|
|
2334
|
+
const t = m.content.flatMap((b) => b.type === "text" ? [b.text] : []).join(" ").trim();
|
|
2335
|
+
if (t) lines.push(`${m.role === "user" ? "Owner" : "Friend"}: ${t.slice(0, MAX_LINE)}`);
|
|
2336
|
+
}
|
|
2337
|
+
return lines.join("\n").slice(-MAX_TEXT);
|
|
2338
|
+
}
|
|
2339
|
+
async function summarize(provider, history, earlier, signal) {
|
|
2340
|
+
const words = conversationText(history);
|
|
2341
|
+
if (!words) return null;
|
|
2342
|
+
const ask = `${earlier ? `Notes from before:
|
|
2343
|
+
${earlier}
|
|
2344
|
+
|
|
2345
|
+
` : ""}Conversation:
|
|
2346
|
+
${words}
|
|
2347
|
+
|
|
2348
|
+
Write the notes now.`;
|
|
2349
|
+
let out = "";
|
|
2350
|
+
try {
|
|
2351
|
+
for await (const ev of provider.stream({ system: SYSTEM, session: "guest", messages: [{ role: "user", content: [{ type: "text", text: ask }] }], tools: [], signal })) {
|
|
2352
|
+
if (ev.type === "text_delta") out += ev.text;
|
|
2353
|
+
else if (ev.type === "error") return null;
|
|
2354
|
+
}
|
|
2355
|
+
} catch {
|
|
2356
|
+
return null;
|
|
2357
|
+
}
|
|
2358
|
+
out = out.trim().slice(0, MAX_SUMMARY);
|
|
2359
|
+
return out || null;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2325
2362
|
// ../server/src/agent/history.ts
|
|
2326
2363
|
var WINDOW = 40;
|
|
2327
2364
|
var PICTURE_STUB = "[camera picture, not kept]";
|
|
@@ -2552,6 +2589,11 @@ var Agent = class {
|
|
|
2552
2589
|
abort = null;
|
|
2553
2590
|
idleJobs = [];
|
|
2554
2591
|
memberTalk = /* @__PURE__ */ new Map();
|
|
2592
|
+
/** How much the model read for its last call (the size of the conversation it keeps), when it said. */
|
|
2593
|
+
context = 0;
|
|
2594
|
+
/** The notes the last tidy-up left in the model's conversation, so the next one does not lose what is in them. */
|
|
2595
|
+
lastSummary = null;
|
|
2596
|
+
tidyBlockedUntil = 0;
|
|
2555
2597
|
/** Runs `job` as soon as no request is being answered (right away when idle). */
|
|
2556
2598
|
whenIdle(job) {
|
|
2557
2599
|
if (this.busy) this.idleJobs.push(job);
|
|
@@ -2560,6 +2602,65 @@ var Agent = class {
|
|
|
2560
2602
|
isBusy() {
|
|
2561
2603
|
return this.busy;
|
|
2562
2604
|
}
|
|
2605
|
+
/** Tokens the model's own conversation holds (0 when unknown, or right after a fresh start). */
|
|
2606
|
+
contextTokens() {
|
|
2607
|
+
return this.context;
|
|
2608
|
+
}
|
|
2609
|
+
/** Writes notes on the conversation and starts the model's own conversation over from them. */
|
|
2610
|
+
async compact() {
|
|
2611
|
+
return this.whileIdle(() => this.compactNow(new AbortController().signal));
|
|
2612
|
+
}
|
|
2613
|
+
/** Starts the model's own conversation over from nothing. Friend's notes and memory are kept. */
|
|
2614
|
+
async clear() {
|
|
2615
|
+
return this.whileIdle(async () => {
|
|
2616
|
+
await this.deps.provider.resetSession();
|
|
2617
|
+
this.context = 0;
|
|
2618
|
+
this.lastSummary = null;
|
|
2619
|
+
return { ok: true, message: "Started fresh. Friend remembers its notes, but not the earlier conversation." };
|
|
2620
|
+
});
|
|
2621
|
+
}
|
|
2622
|
+
async whileIdle(job) {
|
|
2623
|
+
if (!this.deps.provider.resetSession) return { ok: false, message: "This model does not keep a conversation of its own, so there is nothing to tidy." };
|
|
2624
|
+
if (this.busy) return { ok: false, message: "Friend is busy right now. Try again in a moment." };
|
|
2625
|
+
this.busy = true;
|
|
2626
|
+
try {
|
|
2627
|
+
return await job();
|
|
2628
|
+
} finally {
|
|
2629
|
+
this.busy = false;
|
|
2630
|
+
for (const j of this.idleJobs.splice(0)) {
|
|
2631
|
+
try {
|
|
2632
|
+
j();
|
|
2633
|
+
} catch (e) {
|
|
2634
|
+
this.deps.audit.log("system", "agent.idle_job_failed", { message: e.message });
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
async compactNow(signal) {
|
|
2640
|
+
const d = this.deps;
|
|
2641
|
+
const history = loadHistory(d.db);
|
|
2642
|
+
if (!conversationText(history)) return { ok: false, message: "There is nothing to compact yet." };
|
|
2643
|
+
const summary2 = await summarize(d.provider, history, this.lastSummary, signal);
|
|
2644
|
+
if (!summary2) return { ok: false, message: "Could not write the notes just now, so nothing was changed." };
|
|
2645
|
+
await d.provider.resetSession(summary2);
|
|
2646
|
+
this.lastSummary = summary2;
|
|
2647
|
+
this.context = 0;
|
|
2648
|
+
return { ok: true, message: "Done. Friend wrote notes on the conversation and started a fresh one from them." };
|
|
2649
|
+
}
|
|
2650
|
+
/** Before an answer: if the model's own conversation has grown too big, tidy it first. A failure only means trying again later. */
|
|
2651
|
+
async tidyIfBig(emit, signal) {
|
|
2652
|
+
const d = this.deps;
|
|
2653
|
+
const s = d.session?.auto();
|
|
2654
|
+
if (!s?.on || this.context <= s.limit || !d.provider.resetSession) return;
|
|
2655
|
+
const now = d.tasks?.now() ?? Date.now();
|
|
2656
|
+
if (now < this.tidyBlockedUntil) return;
|
|
2657
|
+
emit({ type: "activity", text: "Tidying up my memory" });
|
|
2658
|
+
const r = await this.compactNow(signal).catch(() => ({ ok: false, message: "The notes could not be written." }));
|
|
2659
|
+
if (!r.ok) {
|
|
2660
|
+
this.tidyBlockedUntil = now + 10 * 6e4;
|
|
2661
|
+
d.audit.log("system", "session.tidy_failed", { message: r.message });
|
|
2662
|
+
} else d.audit.log("system", "session.tidied", { auto: true });
|
|
2663
|
+
}
|
|
2563
2664
|
/** Stop the running turn (used by emergency lock). */
|
|
2564
2665
|
cancel() {
|
|
2565
2666
|
this.abort?.abort();
|
|
@@ -2573,6 +2674,7 @@ var Agent = class {
|
|
|
2573
2674
|
}
|
|
2574
2675
|
this.busy = true;
|
|
2575
2676
|
const spent = { input: 0, output: 0 };
|
|
2677
|
+
let reported = 0;
|
|
2576
2678
|
this.scenesThisTurn = 0;
|
|
2577
2679
|
const stage = (cmd) => {
|
|
2578
2680
|
d.stage.apply(cmd);
|
|
@@ -2593,6 +2695,7 @@ var Agent = class {
|
|
|
2593
2695
|
let lastAnswer = "";
|
|
2594
2696
|
try {
|
|
2595
2697
|
if (!isolated) repairDangling(d.db);
|
|
2698
|
+
if (!isolated) await this.tidyIfBig(emit, abort.signal);
|
|
2596
2699
|
const userMsg = { role: "user", content: [{ type: "text", text: text3 }] };
|
|
2597
2700
|
keep(userMsg);
|
|
2598
2701
|
const earlier = member ? earlierOf(this.memberTalk.get(ctx.person.id), d.tasks?.now() ?? Date.now()) : [];
|
|
@@ -2635,6 +2738,7 @@ var Agent = class {
|
|
|
2635
2738
|
if (ev.type === "usage") {
|
|
2636
2739
|
spent.input += ev.input;
|
|
2637
2740
|
spent.output += ev.output;
|
|
2741
|
+
if (ev.context) reported = ev.context;
|
|
2638
2742
|
} else if (ev.type === "text_delta") {
|
|
2639
2743
|
if (!assistantText) {
|
|
2640
2744
|
stage({ cmd: "presence", state: "speaking" });
|
|
@@ -2695,6 +2799,7 @@ var Agent = class {
|
|
|
2695
2799
|
d.audit.log("system", "agent.error", { message: e.message });
|
|
2696
2800
|
emit({ type: "caption.delta", text: "Sorry, something went wrong on my side." });
|
|
2697
2801
|
} finally {
|
|
2802
|
+
if (reported > 0 && !isolated) this.context = reported;
|
|
2698
2803
|
if (d.usage && (spent.input > 0 || spent.output > 0)) d.usage({ source: ctx.source ?? "other", ...spent });
|
|
2699
2804
|
if (member && lastAnswer) {
|
|
2700
2805
|
const lines = [...earlierOf(this.memberTalk.get(ctx.person.id), d.tasks?.now() ?? Date.now()), { role: "user", content: [{ type: "text", text: text3 }] }, { role: "assistant", content: [{ type: "text", text: lastAnswer }] }];
|
|
@@ -3352,6 +3457,12 @@ function sanitizeLook(raw) {
|
|
|
3352
3457
|
};
|
|
3353
3458
|
}
|
|
3354
3459
|
var DEFAULT_OLLAMA = { url: "http://127.0.0.1:11434", model: "qwen2.5:7b", contextTokens: 8192, vision: false };
|
|
3460
|
+
var MIN_COMPACT_TOKENS = 5e4;
|
|
3461
|
+
var MAX_COMPACT_TOKENS = 8e5;
|
|
3462
|
+
var sanitizeSession = (s) => ({
|
|
3463
|
+
autoCompact: typeof s?.autoCompact === "boolean" ? s.autoCompact : true,
|
|
3464
|
+
compactAtTokens: typeof s?.compactAtTokens === "number" && Number.isFinite(s.compactAtTokens) ? Math.min(MAX_COMPACT_TOKENS, Math.max(MIN_COMPACT_TOKENS, Math.round(s.compactAtTokens))) : 1e5
|
|
3465
|
+
});
|
|
3355
3466
|
var sanitizeUpdates = (u) => ({ auto: typeof u?.auto === "boolean" ? u.auto : true });
|
|
3356
3467
|
var DEFAULT_VOICE = {
|
|
3357
3468
|
enabled: true,
|
|
@@ -3419,6 +3530,7 @@ var defaults = (dataDir) => ({
|
|
|
3419
3530
|
claudeCode: { ...DEFAULT_CLAUDE_CODE },
|
|
3420
3531
|
voice: { ...DEFAULT_VOICE },
|
|
3421
3532
|
updates: sanitizeUpdates(void 0),
|
|
3533
|
+
session: sanitizeSession(void 0),
|
|
3422
3534
|
view: "companion",
|
|
3423
3535
|
look: { ...DEFAULT_LOOK },
|
|
3424
3536
|
ownerName: "",
|
|
@@ -3466,6 +3578,7 @@ function loadConfig(dataDir = defaultDataDir()) {
|
|
|
3466
3578
|
claudeCode: sanitizeClaudeCode(stored.claudeCode),
|
|
3467
3579
|
voice: sanitizeVoice({ ...DEFAULT_VOICE, ...stored.voice }),
|
|
3468
3580
|
updates: sanitizeUpdates(stored.updates),
|
|
3581
|
+
session: sanitizeSession(stored.session),
|
|
3469
3582
|
view: stored.view === "minimal" || stored.view === "workspace" ? stored.view : "companion",
|
|
3470
3583
|
look: sanitizeLook(stored.look),
|
|
3471
3584
|
ownerName: typeof stored.ownerName === "string" ? stored.ownerName.replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 40) : "",
|
|
@@ -7124,7 +7237,7 @@ var MAX_SEND = 100 * 1024 * 1024;
|
|
|
7124
7237
|
var MAX_IMAGE = 40 * 1024 * 1024;
|
|
7125
7238
|
var MAX_VIDEO = 8 * 1024 * 1024 * 1024;
|
|
7126
7239
|
var MAX_PDF = 300 * 1024 * 1024;
|
|
7127
|
-
var
|
|
7240
|
+
var MAX_TEXT2 = 2e5;
|
|
7128
7241
|
var MAX_CSV_ROWS = 200;
|
|
7129
7242
|
function parseCsv(text3) {
|
|
7130
7243
|
const rows3 = [];
|
|
@@ -7247,17 +7360,17 @@ var MediaStore = class {
|
|
|
7247
7360
|
const handle = await fs.open(real3, "r");
|
|
7248
7361
|
let text3;
|
|
7249
7362
|
try {
|
|
7250
|
-
const buf = Buffer.alloc(Math.min(stat2.size,
|
|
7363
|
+
const buf = Buffer.alloc(Math.min(stat2.size, MAX_TEXT2));
|
|
7251
7364
|
await handle.read(buf, 0, buf.length, 0);
|
|
7252
7365
|
text3 = buf.toString("utf8");
|
|
7253
7366
|
} finally {
|
|
7254
7367
|
await handle.close();
|
|
7255
7368
|
}
|
|
7256
7369
|
if (text3.includes("\0")) return { ok: false, error: "That file is not text, so it cannot be shown." };
|
|
7257
|
-
const cut = stat2.size >
|
|
7370
|
+
const cut = stat2.size > MAX_TEXT2;
|
|
7258
7371
|
const note = cut ? `
|
|
7259
7372
|
|
|
7260
|
-
[Only the first ${
|
|
7373
|
+
[Only the first ${MAX_TEXT2} characters are shown.]` : "";
|
|
7261
7374
|
if (MARKDOWN.has(ext)) return { ok: true, type: "markdown", props: { title: label4, text: text3 + note }, summary: `Showing ${name}.${cut ? " It is long, so only the start is shown." : ""}` };
|
|
7262
7375
|
if (ext === "csv") {
|
|
7263
7376
|
const rows3 = parseCsv(text3);
|
|
@@ -9808,7 +9921,7 @@ var BrokerError = class extends Error {
|
|
|
9808
9921
|
var MOVERS = 3;
|
|
9809
9922
|
var TOP = 5;
|
|
9810
9923
|
var r2 = (n2) => Number.isFinite(n2) ? Math.round((n2 + Number.EPSILON) * 100) / 100 : 0;
|
|
9811
|
-
function
|
|
9924
|
+
function summarize2(holdings) {
|
|
9812
9925
|
let value = 0;
|
|
9813
9926
|
let invested = 0;
|
|
9814
9927
|
let yesterday = 0;
|
|
@@ -9908,7 +10021,7 @@ function buildReport(input) {
|
|
|
9908
10021
|
else if (snap.holdings.data.length === 0) out.push("No holdings.");
|
|
9909
10022
|
else {
|
|
9910
10023
|
const holdings = snap.holdings.data;
|
|
9911
|
-
const s =
|
|
10024
|
+
const s = summarize2(holdings);
|
|
9912
10025
|
out.push(`Portfolio: ${fmt(s.value)} (invested ${fmt(s.invested)})`);
|
|
9913
10026
|
out.push(`Profit/loss: ${signed(s.pnl)} (${signedPct(s.pnlPct)})`);
|
|
9914
10027
|
out.push(`Today: ${signed(s.dayChange)} (${signedPct(s.dayChangePct)})`);
|
|
@@ -11727,6 +11840,23 @@ function createGateway(deps) {
|
|
|
11727
11840
|
audit.log("owner", "usage.cleared", { removed });
|
|
11728
11841
|
return { removed };
|
|
11729
11842
|
});
|
|
11843
|
+
const sessionGuard = async (req, reply) => {
|
|
11844
|
+
const token = req.cookies[COOKIE_NAME];
|
|
11845
|
+
if (!(token && auth.resolveSession(token))) return reply.code(401).send({ error: "Unauthorized" });
|
|
11846
|
+
if (!deps.session) return reply.code(503).send({ error: "Not available" });
|
|
11847
|
+
};
|
|
11848
|
+
app.get("/api/conversation", { onRequest: sessionGuard }, async () => deps.session.info());
|
|
11849
|
+
app.post("/api/conversation/compact", { onRequest: sessionGuard }, async () => deps.session.compact());
|
|
11850
|
+
app.post("/api/conversation/clear", { onRequest: sessionGuard }, async (req, reply) => {
|
|
11851
|
+
const body = z10.object({ pin: z10.string().min(1).max(32) }).safeParse(req.body);
|
|
11852
|
+
if (!body.success) return reply.code(400).send({ error: "Enter your PIN" });
|
|
11853
|
+
try {
|
|
11854
|
+
return await deps.session.clear(body.data.pin, req.ip);
|
|
11855
|
+
} catch (e) {
|
|
11856
|
+
if (e instanceof HttpError) return reply.code(e.status).send({ error: e.message });
|
|
11857
|
+
throw e;
|
|
11858
|
+
}
|
|
11859
|
+
});
|
|
11730
11860
|
const chromeFailures = new SlidingWindow(20, 6e4);
|
|
11731
11861
|
const chromeWss = new WebSocketServer({ noServer: true, maxPayload: MAX_DEVICE_FRAME_BYTES });
|
|
11732
11862
|
if (deps.chrome) {
|
|
@@ -12599,6 +12729,8 @@ var ClaudeCodeProvider = class {
|
|
|
12599
12729
|
id = "claude-code";
|
|
12600
12730
|
vision = true;
|
|
12601
12731
|
owner = newConv(false);
|
|
12732
|
+
/** Set when the owner's conversation was reset and the new one has not begun yet: what it starts from (a summary, or nothing). */
|
|
12733
|
+
fresh = null;
|
|
12602
12734
|
guests = /* @__PURE__ */ new Set();
|
|
12603
12735
|
bridge;
|
|
12604
12736
|
bridgeReady = null;
|
|
@@ -12641,6 +12773,25 @@ var ClaudeCodeProvider = class {
|
|
|
12641
12773
|
if (guest) this.endGuest(conv);
|
|
12642
12774
|
}
|
|
12643
12775
|
}
|
|
12776
|
+
/** Forgets the owner's conversation. The next message starts a new Claude with `seed` (if any) instead of the old story. */
|
|
12777
|
+
async resetSession(seed) {
|
|
12778
|
+
const conv = this.owner;
|
|
12779
|
+
const proc = conv.proc;
|
|
12780
|
+
conv.proc = null;
|
|
12781
|
+
proc?.kill();
|
|
12782
|
+
await proc?.exited;
|
|
12783
|
+
conv.inbox.clear();
|
|
12784
|
+
conv.awaiting = [];
|
|
12785
|
+
conv.hasContext = false;
|
|
12786
|
+
conv.systemKey = "";
|
|
12787
|
+
this.forgetSession(conv);
|
|
12788
|
+
mkdirSync9(this.home, { recursive: true });
|
|
12789
|
+
this.fresh = { seed: seed?.trim() || null };
|
|
12790
|
+
try {
|
|
12791
|
+
writeFileSync8(this.freshFile(), JSON.stringify(this.fresh));
|
|
12792
|
+
} catch {
|
|
12793
|
+
}
|
|
12794
|
+
}
|
|
12644
12795
|
async close() {
|
|
12645
12796
|
this.bridge.failPending("Friend is stopping.");
|
|
12646
12797
|
this.owner.proc?.kill();
|
|
@@ -12703,7 +12854,9 @@ var ClaudeCodeProvider = class {
|
|
|
12703
12854
|
const text3 = String(ev.result ?? turnText);
|
|
12704
12855
|
if (!ev.is_error) {
|
|
12705
12856
|
const u = ev.usage ?? {};
|
|
12706
|
-
|
|
12857
|
+
const sum = (x) => (x.input_tokens ?? 0) + (x.cache_read_input_tokens ?? 0) + (x.cache_creation_input_tokens ?? 0);
|
|
12858
|
+
const last = Array.isArray(u.iterations) ? u.iterations.at(-1) : void 0;
|
|
12859
|
+
yield { type: "usage", input: sum(u), output: u.output_tokens ?? 0, ...last ? { context: sum(last) } : {} };
|
|
12707
12860
|
yield { type: "done" };
|
|
12708
12861
|
return "ok";
|
|
12709
12862
|
}
|
|
@@ -12797,6 +12950,7 @@ ${item.stderr}`;
|
|
|
12797
12950
|
mkdirSync9(this.home, { recursive: true });
|
|
12798
12951
|
if (!conv.guest) {
|
|
12799
12952
|
conv.sessionId ??= this.readSession();
|
|
12953
|
+
this.fresh = conv.sessionId ? null : this.readFresh();
|
|
12800
12954
|
await this.startBridge();
|
|
12801
12955
|
this.bridge.setTools(req.tools);
|
|
12802
12956
|
}
|
|
@@ -12873,11 +13027,26 @@ ${item.stderr}`;
|
|
|
12873
13027
|
rememberSession(conv, id2) {
|
|
12874
13028
|
if (conv.guest || conv.sessionId === id2) return;
|
|
12875
13029
|
conv.sessionId = id2;
|
|
13030
|
+
this.fresh = null;
|
|
13031
|
+
try {
|
|
13032
|
+
rmSync8(this.freshFile(), { force: true });
|
|
13033
|
+
} catch {
|
|
13034
|
+
}
|
|
12876
13035
|
try {
|
|
12877
13036
|
writeFileSync8(this.sessionFile(), id2);
|
|
12878
13037
|
} catch {
|
|
12879
13038
|
}
|
|
12880
13039
|
}
|
|
13040
|
+
freshFile = () => join16(this.home, "fresh.json");
|
|
13041
|
+
readFresh() {
|
|
13042
|
+
try {
|
|
13043
|
+
if (!existsSync14(this.freshFile())) return null;
|
|
13044
|
+
const r = JSON.parse(readFileSync8(this.freshFile(), "utf8"));
|
|
13045
|
+
return { seed: typeof r.seed === "string" && r.seed ? r.seed : null };
|
|
13046
|
+
} catch {
|
|
13047
|
+
return null;
|
|
13048
|
+
}
|
|
13049
|
+
}
|
|
12881
13050
|
forgetSession(conv) {
|
|
12882
13051
|
conv.sessionId = null;
|
|
12883
13052
|
try {
|
|
@@ -12897,7 +13066,11 @@ ${item.stderr}`;
|
|
|
12897
13066
|
}
|
|
12898
13067
|
}
|
|
12899
13068
|
const blocks = [];
|
|
12900
|
-
|
|
13069
|
+
const fresh = conv.hasContext ? null : this.fresh;
|
|
13070
|
+
if (fresh) {
|
|
13071
|
+
if (fresh.seed) blocks.push({ type: "text", text: `Summary of our earlier conversation (for context only, it is already over):
|
|
13072
|
+
${fresh.seed}` });
|
|
13073
|
+
} else if (!conv.hasContext && last >= 0) blocks.push({ type: "text", text: transcript(msgs.slice(0, last + 1)) });
|
|
12901
13074
|
const ran = /* @__PURE__ */ new Map();
|
|
12902
13075
|
for (const m of msgs.slice(last + 1)) {
|
|
12903
13076
|
for (const b of m.content) {
|
|
@@ -12960,6 +13133,9 @@ var FallbackProvider = class {
|
|
|
12960
13133
|
yield ev;
|
|
12961
13134
|
}
|
|
12962
13135
|
}
|
|
13136
|
+
async resetSession(seed) {
|
|
13137
|
+
await Promise.all([this.primary.resetSession?.(seed), this.other.resetSession?.(seed)]);
|
|
13138
|
+
}
|
|
12963
13139
|
async close() {
|
|
12964
13140
|
await Promise.all([this.primary.close?.(), this.other.close?.()]);
|
|
12965
13141
|
}
|
|
@@ -12999,6 +13175,9 @@ var SwitchableProvider = class {
|
|
|
12999
13175
|
this.current = next;
|
|
13000
13176
|
return old;
|
|
13001
13177
|
}
|
|
13178
|
+
async resetSession(seed) {
|
|
13179
|
+
await this.current.resetSession?.(seed);
|
|
13180
|
+
}
|
|
13002
13181
|
async close() {
|
|
13003
13182
|
await this.current.close?.();
|
|
13004
13183
|
}
|
|
@@ -13145,7 +13324,8 @@ var SettingsPatch = z11.object({
|
|
|
13145
13324
|
duck: z11.boolean(),
|
|
13146
13325
|
duckPercent: z11.number().int().min(0).max(100)
|
|
13147
13326
|
}).partial().strict(),
|
|
13148
|
-
updates: z11.object({ auto: z11.boolean() }).partial().strict()
|
|
13327
|
+
updates: z11.object({ auto: z11.boolean() }).partial().strict(),
|
|
13328
|
+
session: z11.object({ autoCompact: z11.boolean(), compactAtTokens: z11.number().int().min(5e4).max(8e5) }).partial().strict()
|
|
13149
13329
|
}).partial().strict();
|
|
13150
13330
|
function settingsView(c) {
|
|
13151
13331
|
return {
|
|
@@ -13209,7 +13389,8 @@ function settingsView(c) {
|
|
|
13209
13389
|
duck: c.voice.duck,
|
|
13210
13390
|
duckPercent: c.voice.duckPercent
|
|
13211
13391
|
},
|
|
13212
|
-
updates: { auto: c.updates.auto }
|
|
13392
|
+
updates: { auto: c.updates.auto },
|
|
13393
|
+
session: { autoCompact: c.session.autoCompact, compactAtTokens: c.session.compactAtTokens }
|
|
13213
13394
|
};
|
|
13214
13395
|
}
|
|
13215
13396
|
var SettingsError = class extends Error {
|
|
@@ -13367,6 +13548,7 @@ function applySettings(config, raw) {
|
|
|
13367
13548
|
}
|
|
13368
13549
|
if (p.claudeCode) Object.assign(config.claudeCode, p.claudeCode);
|
|
13369
13550
|
if (p.updates) Object.assign(config.updates, p.updates);
|
|
13551
|
+
if (p.session) Object.assign(config.session, p.session);
|
|
13370
13552
|
if (p.voice) {
|
|
13371
13553
|
const safe = next.voice;
|
|
13372
13554
|
for (const k of Object.keys(p.voice)) config.voice[k] = safe[k];
|
|
@@ -14048,7 +14230,7 @@ function currentSelf() {
|
|
|
14048
14230
|
// ../server/src/system/npm.ts
|
|
14049
14231
|
import { spawn as spawn7 } from "node:child_process";
|
|
14050
14232
|
var PACKAGE = "friend-assistant";
|
|
14051
|
-
var runningVersion = () => true ? "0.1.
|
|
14233
|
+
var runningVersion = () => true ? "0.1.47" : "development";
|
|
14052
14234
|
async function npmLatest(fetchFn = fetch, timeoutMs = 1e4) {
|
|
14053
14235
|
try {
|
|
14054
14236
|
const res = await fetchFn(`https://registry.npmjs.org/${PACKAGE}/latest`, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(timeoutMs) });
|
|
@@ -15172,6 +15354,7 @@ async function createApp(opts) {
|
|
|
15172
15354
|
admin,
|
|
15173
15355
|
provider,
|
|
15174
15356
|
usage: (e) => usageLog.record({ ...e, ...modelInUse() }),
|
|
15357
|
+
session: { auto: () => ({ on: config.session.autoCompact, limit: config.session.compactAtTokens }) },
|
|
15175
15358
|
policy,
|
|
15176
15359
|
approvals,
|
|
15177
15360
|
devices,
|
|
@@ -15222,7 +15405,7 @@ async function createApp(opts) {
|
|
|
15222
15405
|
const got = await brokerHub.snapshot("zerodha");
|
|
15223
15406
|
if (!got.ok) return got.sessionExpired ? "Zerodha needs you to log in first." : got.error;
|
|
15224
15407
|
if (!got.snapshot.holdings.ok) return "I could not read your holdings just now.";
|
|
15225
|
-
return portfolioSpeech(
|
|
15408
|
+
return portfolioSpeech(summarize2(got.snapshot.holdings.data), locale2.startsWith("hi") ? "hi" : "en", { money: { symbol: config.brokers.currencySymbol, locale: config.brokers.numberLocale }, currencyName: config.brokers.currencyName });
|
|
15226
15409
|
},
|
|
15227
15410
|
WeatherIntent: async (_locale, slots) => {
|
|
15228
15411
|
if (!slots.city) return "Tell me which city, for example: what is the weather in Pune.";
|
|
@@ -15578,6 +15761,20 @@ async function createApp(opts) {
|
|
|
15578
15761
|
chrome: chromeRemoteApi,
|
|
15579
15762
|
system: systemApi,
|
|
15580
15763
|
usage: usageLog,
|
|
15764
|
+
session: {
|
|
15765
|
+
info: () => ({ supported: !!provider.resetSession, contextTokens: agent.contextTokens(), autoCompact: config.session.autoCompact, compactAtTokens: config.session.compactAtTokens }),
|
|
15766
|
+
compact: async () => {
|
|
15767
|
+
const r = await agent.compact();
|
|
15768
|
+
if (r.ok) audit.log("owner", "session.compacted", {});
|
|
15769
|
+
return r;
|
|
15770
|
+
},
|
|
15771
|
+
clear: async (pin, ip) => {
|
|
15772
|
+
await requirePin(pin, ip);
|
|
15773
|
+
const r = await agent.clear();
|
|
15774
|
+
if (r.ok) audit.log("owner", "session.cleared", { ip });
|
|
15775
|
+
return r;
|
|
15776
|
+
}
|
|
15777
|
+
},
|
|
15581
15778
|
video: {
|
|
15582
15779
|
stream: (id2) => videoHub.streamOf(id2),
|
|
15583
15780
|
status: async () => videoHub.status() ?? await videoHub.refresh(),
|
|
@@ -18698,7 +18895,7 @@ import { appendFileSync } from "node:fs";
|
|
|
18698
18895
|
import { join as join25 } from "node:path";
|
|
18699
18896
|
var PACKAGE2 = "friend-assistant";
|
|
18700
18897
|
var real2 = {
|
|
18701
|
-
current: () => true ? "0.1.
|
|
18898
|
+
current: () => true ? "0.1.47" : "development",
|
|
18702
18899
|
latest: () => new Promise((resolve13) => {
|
|
18703
18900
|
const child = crossSpawn5("npm", ["view", PACKAGE2, "version", "--prefer-online"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
18704
18901
|
let out = "";
|
|
@@ -19013,7 +19210,7 @@ async function run(argv, io2) {
|
|
|
19013
19210
|
case "version":
|
|
19014
19211
|
case "--version":
|
|
19015
19212
|
case "-v":
|
|
19016
|
-
io2.out(true ? "0.1.
|
|
19213
|
+
io2.out(true ? "0.1.47" : "development");
|
|
19017
19214
|
return 0;
|
|
19018
19215
|
case "lan":
|
|
19019
19216
|
return await lanCommand(rest, io2);
|