modelstat 0.0.42 → 0.0.43

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/dist/cli.mjs CHANGED
@@ -4421,7 +4421,15 @@ var init_schemas = __esm({
4421
4421
  installation_id: external_exports.string(),
4422
4422
  identity_id: external_exports.string().nullable()
4423
4423
  })
4424
- ).optional()
4424
+ ).optional(),
4425
+ /** Optional per-session titles — session_id → short redacted title
4426
+ * (≤120 chars) produced by the companion's local titler from the
4427
+ * session's segment abstracts. Server-side this is last-write-wins
4428
+ * per session; companions recompute it from the full session view on
4429
+ * every upload, so the latest batch always carries the freshest
4430
+ * title. Absent for runtimes without a titler (older agents, no-op
4431
+ * browser summariser) — the server keeps whatever it has. */
4432
+ session_titles: external_exports.record(external_exports.string(), external_exports.string().max(120)).optional()
4425
4433
  });
4426
4434
  HeartbeatPayload = external_exports.object({
4427
4435
  device_id: external_exports.string(),
@@ -4743,7 +4751,9 @@ async function parseClaudeCodeJsonl(ctx) {
4743
4751
  if (obj.type === "assistant") {
4744
4752
  const a = obj;
4745
4753
  const usage = a.message?.usage ?? {};
4746
- lastModel = a.message?.model ?? lastModel;
4754
+ if (a.message?.model && a.message.model !== "<synthetic>") {
4755
+ lastModel = a.message.model;
4756
+ }
4747
4757
  if (!a.uuid || !sessionId) {
4748
4758
  skipped += 1;
4749
4759
  continue;
@@ -44387,6 +44397,89 @@ var init_cognition = __esm({
44387
44397
  }
44388
44398
  });
44389
44399
 
44400
+ // ../../packages/companion-core/src/pipeline/title.ts
44401
+ function buildTitleUserPrompt(input) {
44402
+ const lines = input.abstracts.map(
44403
+ (a, i) => ` [part ${i + 1}] ${a.replace(/\s+/g, " ").trim().slice(0, TITLER_ABSTRACT_SLICE_CHARS)}`
44404
+ ).join("\n");
44405
+ const facts = input.facts?.trim();
44406
+ return `${facts ? `Session context: ${facts}.
44407
+
44408
+ ` : ""}Summaries of the session's parts (chronological):
44409
+ ${lines}
44410
+
44411
+ Write the title.`;
44412
+ }
44413
+ function stripCognitionSuffix(abstract) {
44414
+ return abstract.replace(/\s*\[(?:Mood|Mind):[^\]]*\]/g, "").trim();
44415
+ }
44416
+ function sanitiseTitle(raw) {
44417
+ if (!raw) return "";
44418
+ const firstLine = raw.replace(/```[a-z]*\n?/gi, "").split(/[\n\r]/).map((l) => l.trim()).find((l) => l.length > 0) ?? "";
44419
+ let t = firstLine.replace(/\s+/g, " ").replace(/^["'`“”]+/, "").replace(/["'`“”]+$/, "").replace(/[.!]+$/, "").trim();
44420
+ if (!t) return "";
44421
+ t = redact(t).text;
44422
+ return t.slice(0, TITLE_MAX_CHARS).trim();
44423
+ }
44424
+ function fallbackTitle(abstracts) {
44425
+ const first = abstracts.find((a) => a.trim().length > 0);
44426
+ if (!first) return "";
44427
+ const sentence = first.split(/(?<=[.!?])\s/, 1)[0] ?? first;
44428
+ return sanitiseTitle(sentence);
44429
+ }
44430
+ function sampleAbstracts(abstracts, max = TITLER_MAX_ABSTRACTS) {
44431
+ if (abstracts.length <= max) return abstracts;
44432
+ const picks = /* @__PURE__ */ new Set([0, abstracts.length - 1]);
44433
+ for (let i = 1; picks.size < max; i++) {
44434
+ picks.add(Math.round(i * (abstracts.length - 1) / (max - 1)));
44435
+ }
44436
+ return [...picks].sort((a, b) => a - b).map((i) => abstracts[i]);
44437
+ }
44438
+ async function buildSessionTitles(segments, entitle) {
44439
+ const bySession = /* @__PURE__ */ new Map();
44440
+ for (const s of segments) {
44441
+ const arr = bySession.get(s.session_id) ?? [];
44442
+ arr.push(s);
44443
+ bySession.set(s.session_id, arr);
44444
+ }
44445
+ const out = {};
44446
+ for (const [sessionId, segs] of bySession) {
44447
+ const sorted = [...segs].sort((a, b) => a.started_at.localeCompare(b.started_at));
44448
+ const abstracts = sorted.map((s) => stripCognitionSuffix(s.abstract)).filter((a) => a.length > 0);
44449
+ if (abstracts.length === 0) continue;
44450
+ let title = "";
44451
+ if (entitle) {
44452
+ const first = sorted[0];
44453
+ const project = first.tags.find((t) => t.root_key === "projects")?.name;
44454
+ const facts = [
44455
+ project ? `repo ${project}` : null,
44456
+ `${sorted.length} part${sorted.length === 1 ? "" : "s"} on ${first.tool}`
44457
+ ].filter(Boolean).join("; ");
44458
+ try {
44459
+ title = sanitiseTitle(await entitle({ abstracts: sampleAbstracts(abstracts), facts }));
44460
+ } catch {
44461
+ title = "";
44462
+ }
44463
+ }
44464
+ if (!title) title = fallbackTitle(abstracts);
44465
+ if (title) out[sessionId] = title;
44466
+ }
44467
+ return out;
44468
+ }
44469
+ var TITLER_SYSTEM_PROMPT, TITLE_MAX_CHARS, TITLER_MAX_TOKENS, TITLER_TEMPERATURE, TITLER_MAX_ABSTRACTS, TITLER_ABSTRACT_SLICE_CHARS;
44470
+ var init_title = __esm({
44471
+ "../../packages/companion-core/src/pipeline/title.ts"() {
44472
+ "use strict";
44473
+ init_redact();
44474
+ TITLER_SYSTEM_PROMPT = "You write a short TITLE for an AI coding session, given one-sentence summaries of its parts in chronological order. The title names what the session was about at a high level, in 3-8 words. If the session clearly spans more than one theme, name the top themes (at most 3) separated by ' \xB7 '. Use concrete domain keywords (features, components, subsystems). No narration, no filler words like 'session' or 'work on', no quotes, no trailing period, no PII, no code literals, no file paths. Reply with only the title.";
44475
+ TITLE_MAX_CHARS = 80;
44476
+ TITLER_MAX_TOKENS = 60;
44477
+ TITLER_TEMPERATURE = 0.2;
44478
+ TITLER_MAX_ABSTRACTS = 10;
44479
+ TITLER_ABSTRACT_SLICE_CHARS = 240;
44480
+ }
44481
+ });
44482
+
44390
44483
  // ../../packages/companion-core/src/pipeline/index.ts
44391
44484
  async function buildSegmentsForSession(events, adapters2, onProgress) {
44392
44485
  if (events.length === 0) return [];
@@ -44682,6 +44775,7 @@ var init_pipeline = __esm({
44682
44775
  init_redact();
44683
44776
  init_prompts();
44684
44777
  init_cognition();
44778
+ init_title();
44685
44779
  SEGMENT_TIME_GAP_MS = 15 * 6e4;
44686
44780
  SEGMENT_TOPIC_THRESHOLD = 0.35;
44687
44781
  SEGMENT_MAX_TURNS = 100;
@@ -45050,6 +45144,9 @@ async function loadOnce(cfg) {
45050
45144
  const cognizerContext = await model.createContext({
45051
45145
  contextSize: Math.min(cfg.contextSize, 2048)
45052
45146
  });
45147
+ const entitlerContext = await model.createContext({
45148
+ contextSize: cfg.contextSize
45149
+ });
45053
45150
  const summarizer = new llamaMod.LlamaChatSession({
45054
45151
  contextSequence: summariserContext.getSequence(),
45055
45152
  systemPrompt: SUMMARISER_SYSTEM_PROMPT
@@ -45058,7 +45155,11 @@ async function loadOnce(cfg) {
45058
45155
  contextSequence: cognizerContext.getSequence(),
45059
45156
  systemPrompt: COGNITION_SYSTEM_PROMPT
45060
45157
  });
45061
- loaded = { summarizer, cognizer };
45158
+ const entitler = new llamaMod.LlamaChatSession({
45159
+ contextSequence: entitlerContext.getSequence(),
45160
+ systemPrompt: TITLER_SYSTEM_PROMPT
45161
+ });
45162
+ loaded = { summarizer, cognizer, entitler };
45062
45163
  return loaded;
45063
45164
  })();
45064
45165
  try {
@@ -45120,12 +45221,41 @@ function llamaCognize(cfg = defaultLlamaConfig()) {
45120
45221
  }
45121
45222
  };
45122
45223
  }
45224
+ function llamaEntitle(cfg = defaultLlamaConfig()) {
45225
+ return async (input) => {
45226
+ if (input.abstracts.length === 0) return null;
45227
+ let loadedSessions;
45228
+ try {
45229
+ loadedSessions = await loadOnce(cfg);
45230
+ } catch {
45231
+ return null;
45232
+ }
45233
+ const { entitler } = loadedSessions;
45234
+ const run = inflight.then(async () => {
45235
+ entitler.resetChatHistory();
45236
+ const raw = await entitler.prompt(buildTitleUserPrompt(input), {
45237
+ temperature: TITLER_TEMPERATURE,
45238
+ // Same thinking-budget rationale as the cognition pass: the
45239
+ // answer is tiny but Qwen3.5 reasons first.
45240
+ maxTokens: TITLER_MAX_TOKENS + 400
45241
+ });
45242
+ return stripThinking(raw ?? "") || null;
45243
+ });
45244
+ inflight = run.catch(() => void 0);
45245
+ try {
45246
+ return await run;
45247
+ } catch {
45248
+ return null;
45249
+ }
45250
+ };
45251
+ }
45123
45252
  var DEFAULT_LLAMA_MODEL_URL, LLAMA_MAX_TOKENS, loaded, loadPromise, inflight;
45124
45253
  var init_llama = __esm({
45125
45254
  "../../packages/companion-core/src/node/llama.ts"() {
45126
45255
  "use strict";
45127
45256
  init_prompts();
45128
45257
  init_cognition();
45258
+ init_title();
45129
45259
  DEFAULT_LLAMA_MODEL_URL = "https://huggingface.co/lmstudio-community/Qwen3.5-4B-GGUF/resolve/main/Qwen3.5-4B-Q4_K_M.gguf";
45130
45260
  LLAMA_MAX_TOKENS = 1024;
45131
45261
  loaded = null;
@@ -45205,6 +45335,7 @@ __export(node_exports, {
45205
45335
  defaultOllamaConfig: () => defaultOllamaConfig,
45206
45336
  ensureLlamaModel: () => ensureLlamaModel,
45207
45337
  llamaCognize: () => llamaCognize,
45338
+ llamaEntitle: () => llamaEntitle,
45208
45339
  llamaSummarize: () => llamaSummarize,
45209
45340
  ollamaCognize: () => ollamaCognize,
45210
45341
  ollamaEmbed: () => ollamaEmbed,
@@ -45322,6 +45453,7 @@ var init_privacy_filter = __esm({
45322
45453
  var pipeline_exports = {};
45323
45454
  __export(pipeline_exports, {
45324
45455
  buildSegments: () => buildSegments,
45456
+ buildSessionTitles: () => buildSessionTitles2,
45325
45457
  preflightSummariser: () => preflightSummariser
45326
45458
  });
45327
45459
  async function bundledAdapters() {
@@ -45337,6 +45469,11 @@ async function bundledAdapters() {
45337
45469
  summarize: llamaSummarize(llamaCfg),
45338
45470
  tokenize: (text) => Math.max(1, Math.ceil(text.length / 4)),
45339
45471
  cognize: llamaCognize(llamaCfg),
45472
+ // Session-title pass — same bundled model, third chat session with
45473
+ // TITLER_SYSTEM_PROMPT. One short call per session per upload (the
45474
+ // sessions-list title in the dashboard). Best-effort like cognize:
45475
+ // failures fall back to a deterministic title in buildSessionTitles.
45476
+ entitle: llamaEntitle(llamaCfg),
45340
45477
  // Model-based PII redactor (OpenAI Privacy Filter via
45341
45478
  // transformers.js / ONNX). Runs locally on CPU after the regex
45342
45479
  // pass in packages/core/redact.ts. ~1 GB model downloaded on
@@ -45366,6 +45503,10 @@ async function getAdapters() {
45366
45503
  async function buildSegments(events, onProgress) {
45367
45504
  return buildSegmentsForSession(events, await getAdapters(), onProgress);
45368
45505
  }
45506
+ async function buildSessionTitles2(segments) {
45507
+ const a = await getAdapters();
45508
+ return buildSessionTitles(segments, a.entitle);
45509
+ }
45369
45510
  async function preflightSummariser() {
45370
45511
  const a = await getAdapters();
45371
45512
  const out = await a.summarize({
@@ -45457,16 +45598,33 @@ async function scanAll(cb = {}) {
45457
45598
  let segmentsUploaded = 0;
45458
45599
  let buffer = [];
45459
45600
  let pendingCursors = [];
45601
+ const runSegmentsBySession = /* @__PURE__ */ new Map();
45460
45602
  async function flushBatch() {
45461
45603
  if (!buffer.length) return;
45462
45604
  const events = buffer.map(withNonNullTokens);
45463
45605
  const segments = await buildSegments(events, cb.onProgress);
45606
+ for (const seg of segments) {
45607
+ const arr = runSegmentsBySession.get(seg.session_id) ?? [];
45608
+ arr.push(seg);
45609
+ runSegmentsBySession.set(seg.session_id, arr);
45610
+ }
45611
+ const titleInput = [];
45612
+ for (const sessionId of new Set(segments.map((s) => s.session_id))) {
45613
+ titleInput.push(...runSegmentsBySession.get(sessionId) ?? []);
45614
+ }
45615
+ let sessionTitles = {};
45616
+ try {
45617
+ sessionTitles = await buildSessionTitles2(titleInput);
45618
+ } catch (e) {
45619
+ console.warn("session titling failed \u2014 shipping batch untitled:", e.message);
45620
+ }
45464
45621
  const batch = {
45465
45622
  batch_id: batchId(),
45466
45623
  device_id: deviceId,
45467
45624
  agent_version: AGENT_VERSION,
45468
45625
  events,
45469
- segments
45626
+ segments,
45627
+ ...Object.keys(sessionTitles).length ? { session_titles: sessionTitles } : {}
45470
45628
  };
45471
45629
  cb.onUpload?.({ events: events.length, segments: segments.length });
45472
45630
  const res = await uploadBatch(batch);
@@ -45513,7 +45671,7 @@ var init_scan = __esm({
45513
45671
  init_pipeline2();
45514
45672
  init_config2();
45515
45673
  init_api();
45516
- AGENT_VERSION = true ? "agent-0.0.42" : "agent-dev";
45674
+ AGENT_VERSION = true ? "agent-0.0.43" : "agent-dev";
45517
45675
  BATCH_MAX_EVENTS = 2e3;
45518
45676
  ZERO_TOKENS = {
45519
45677
  input: 0,
@@ -45704,7 +45862,7 @@ var PROCESSING_VERSION;
45704
45862
  var init_processing_version = __esm({
45705
45863
  "src/processing-version.ts"() {
45706
45864
  "use strict";
45707
- PROCESSING_VERSION = 3;
45865
+ PROCESSING_VERSION = 4;
45708
45866
  }
45709
45867
  });
45710
45868
 
@@ -47713,7 +47871,7 @@ var init_daemon = __esm({
47713
47871
  init_lock();
47714
47872
  init_scan();
47715
47873
  init_single_flight();
47716
- AGENT_VERSION2 = true ? "agent-0.0.42" : "agent-dev";
47874
+ AGENT_VERSION2 = true ? "agent-0.0.43" : "agent-dev";
47717
47875
  HEARTBEAT_INTERVAL_MS = 1e4;
47718
47876
  SCAN_INTERVAL_MS = 5 * 60 * 1e3;
47719
47877
  DISCOVERY_INTERVAL_MS = 6e4;
@@ -48223,7 +48381,7 @@ function tryOpenBrowser(url) {
48223
48381
  return false;
48224
48382
  }
48225
48383
  }
48226
- var AGENT_VERSION3 = true ? "agent-0.0.42" : "agent-dev";
48384
+ var AGENT_VERSION3 = true ? "agent-0.0.43" : "agent-dev";
48227
48385
  function osFamily() {
48228
48386
  const p = platform4();
48229
48387
  if (p === "darwin") return "macos";