modelstat 0.11.0 → 0.12.0

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
@@ -99,8 +99,7 @@ async function resolveGitContext(cwd) {
99
99
  remote_url: null,
100
100
  remote_host: null,
101
101
  remote_slug: null,
102
- branch: null,
103
- commit_sha: null
102
+ branch: null
104
103
  };
105
104
  cache.set(target, empty);
106
105
  return empty;
@@ -115,14 +114,12 @@ async function resolveGitContext(cwd) {
115
114
  };
116
115
  const remoteUrl = await ran(["config", "--get", "remote.origin.url"]);
117
116
  const branch = await ran(["rev-parse", "--abbrev-ref", "HEAD"]);
118
- const sha = await ran(["rev-parse", "HEAD"]);
119
117
  const parsed = remoteUrl ? parseRemote(remoteUrl) : { host: null, slug: null };
120
118
  const ctx = {
121
119
  remote_url: remoteUrl,
122
120
  remote_host: parsed.host,
123
121
  remote_slug: parsed.slug,
124
- branch,
125
- commit_sha: sha
122
+ branch
126
123
  };
127
124
  cache.set(target, ctx);
128
125
  return ctx;
@@ -4740,7 +4737,7 @@ var init_redact = __esm({
4740
4737
 
4741
4738
  // ../../packages/core/src/session-metadata.ts
4742
4739
  function emptyDetectedRefs() {
4743
- return { repos: [], pull_requests: [], commits: [], issues: [] };
4740
+ return { repos: [], pull_requests: [], issues: [] };
4744
4741
  }
4745
4742
  function repoFrom(host, slug, source) {
4746
4743
  return { host, slug, branches: [], source };
@@ -4826,21 +4823,6 @@ function detectReferences(text, source = "content") {
4826
4823
  confidence: 0.9
4827
4824
  });
4828
4825
  }
4829
- for (const m of text.matchAll(GITHUB_COMMIT)) {
4830
- const slug = `${m[1]}/${m[2]}`;
4831
- out.commits.push({ sha: m[3] ?? "", slug, url: m[0], source, confidence: 0.95 });
4832
- out.repos.push(repoFrom("github.com", slug, source));
4833
- }
4834
- for (const m of text.matchAll(GITLAB_COMMIT)) {
4835
- out.commits.push({
4836
- sha: m[2] ?? "",
4837
- slug: m[1] ?? null,
4838
- url: m[0],
4839
- source,
4840
- confidence: 0.95
4841
- });
4842
- if (m[1]) out.repos.push(repoFrom("gitlab.com", m[1], source));
4843
- }
4844
4826
  for (const m of text.matchAll(SLUG_HASH)) {
4845
4827
  const slug = m[1] ?? "";
4846
4828
  const lead = text.slice(Math.max(0, (m.index ?? 0) - 20), m.index ?? 0).toLowerCase();
@@ -4911,7 +4893,6 @@ function dedupeSessionMetadata(parts) {
4911
4893
  for (const p of parts) {
4912
4894
  all.repos.push(...p.repos);
4913
4895
  all.pull_requests.push(...p.pull_requests);
4914
- all.commits.push(...p.commits);
4915
4896
  all.issues.push(...p.issues);
4916
4897
  }
4917
4898
  const repos = dedupe(
@@ -4940,22 +4921,6 @@ function dedupeSessionMetadata(parts) {
4940
4921
  };
4941
4922
  }
4942
4923
  );
4943
- const commits = dedupe(
4944
- all.commits,
4945
- // Key on slug + sha: two different commits in two repos can share a short
4946
- // 7-hex prefix, and must not collapse into one.
4947
- (c) => `${(c.slug ?? "").toLowerCase()}@${c.sha.toLowerCase()}`,
4948
- (a, b) => {
4949
- const [win, lose] = score(a) >= score(b) ? [a, b] : [b, a];
4950
- return {
4951
- sha: win.sha,
4952
- slug: win.slug ?? lose.slug,
4953
- url: win.url ?? lose.url,
4954
- source: win.source,
4955
- confidence: Math.max(win.confidence, lose.confidence)
4956
- };
4957
- }
4958
- );
4959
4924
  const issues = dedupe(
4960
4925
  all.issues,
4961
4926
  (i) => `${i.provider}:${(i.slug ?? "").toLowerCase()}#${i.key.toLowerCase()}`,
@@ -4979,12 +4944,11 @@ function dedupeSessionMetadata(parts) {
4979
4944
  return {
4980
4945
  repos: keepValid(RepoRef, repos).slice(0, 50),
4981
4946
  pull_requests: keepValid(PullRequestRef, pull_requests).slice(0, 100),
4982
- commits: keepValid(CommitRef, commits).slice(0, 200),
4983
4947
  issues: keepValid(IssueRef, reconciledIssues).slice(0, 100)
4984
4948
  };
4985
4949
  }
4986
4950
  function isEmptySessionMetadata(m) {
4987
- return m.repos.length === 0 && m.pull_requests.length === 0 && m.commits.length === 0 && m.issues.length === 0;
4951
+ return m.repos.length === 0 && m.pull_requests.length === 0 && m.issues.length === 0;
4988
4952
  }
4989
4953
  function detectEventReferences(text) {
4990
4954
  if (!text) return null;
@@ -4993,11 +4957,10 @@ function detectEventReferences(text) {
4993
4957
  return {
4994
4958
  repos: m.repos.slice(0, 24),
4995
4959
  pull_requests: m.pull_requests.slice(0, 24),
4996
- commits: m.commits.slice(0, 24),
4997
4960
  issues: m.issues.slice(0, 24)
4998
4961
  };
4999
4962
  }
5000
- var REF_SOURCES, RefSource, SOURCE_RANK, RepoRef, PullRequestRef, CommitRef, ISSUE_PROVIDERS, IssueRef, SessionMetadata, EventReferences, GITHUB_PR, GITLAB_MR, BITBUCKET_PR, GITHUB_ISSUE, GITLAB_ISSUE, GITHUB_COMMIT, GITLAB_COMMIT, LINEAR_ISSUE, JIRA_ISSUE, SLUG_HASH, BARE_TICKET;
4963
+ var REF_SOURCES, RefSource, SOURCE_RANK, RepoRef, PullRequestRef, ISSUE_PROVIDERS, IssueRef, SessionMetadata, EventReferences, GITHUB_PR, GITLAB_MR, BITBUCKET_PR, GITHUB_ISSUE, GITLAB_ISSUE, LINEAR_ISSUE, JIRA_ISSUE, SLUG_HASH, BARE_TICKET;
5001
4964
  var init_session_metadata = __esm({
5002
4965
  "../../packages/core/src/session-metadata.ts"() {
5003
4966
  "use strict";
@@ -5031,14 +4994,6 @@ var init_session_metadata = __esm({
5031
4994
  hotfixed: external_exports.boolean().nullable().optional(),
5032
4995
  reopened: external_exports.boolean().nullable().optional()
5033
4996
  });
5034
- CommitRef = external_exports.object({
5035
- /** 7–40 char hex SHA. */
5036
- sha: external_exports.string().max(64),
5037
- slug: external_exports.string().max(200).nullable().default(null),
5038
- url: external_exports.string().max(400).nullable().default(null),
5039
- source: RefSource.default("content"),
5040
- confidence: external_exports.number().min(0).max(1).default(0.8)
5041
- });
5042
4997
  ISSUE_PROVIDERS = [
5043
4998
  "github",
5044
4999
  "gitlab",
@@ -5058,13 +5013,11 @@ var init_session_metadata = __esm({
5058
5013
  SessionMetadata = external_exports.object({
5059
5014
  repos: external_exports.array(RepoRef).max(50).default([]),
5060
5015
  pull_requests: external_exports.array(PullRequestRef).max(100).default([]),
5061
- commits: external_exports.array(CommitRef).max(200).default([]),
5062
5016
  issues: external_exports.array(IssueRef).max(100).default([])
5063
5017
  });
5064
5018
  EventReferences = external_exports.object({
5065
5019
  repos: external_exports.array(RepoRef).max(24).default([]),
5066
5020
  pull_requests: external_exports.array(PullRequestRef).max(24).default([]),
5067
- commits: external_exports.array(CommitRef).max(24).default([]),
5068
5021
  issues: external_exports.array(IssueRef).max(24).default([])
5069
5022
  });
5070
5023
  GITHUB_PR = /https?:\/\/github\.com\/([\w.-]+)\/([\w.-]+)\/pull\/(\d+)/gi;
@@ -5072,8 +5025,6 @@ var init_session_metadata = __esm({
5072
5025
  BITBUCKET_PR = /https?:\/\/bitbucket\.org\/([\w.-]+)\/([\w.-]+)\/pull-requests\/(\d+)/gi;
5073
5026
  GITHUB_ISSUE = /https?:\/\/github\.com\/([\w.-]+)\/([\w.-]+)\/issues\/(\d+)/gi;
5074
5027
  GITLAB_ISSUE = /https?:\/\/gitlab\.com\/([\w./-]+?)\/-\/issues\/(\d+)/gi;
5075
- GITHUB_COMMIT = /https?:\/\/github\.com\/([\w.-]+)\/([\w.-]+)\/commit\/([0-9a-f]{7,40})/gi;
5076
- GITLAB_COMMIT = /https?:\/\/gitlab\.com\/([\w./-]+?)\/-\/commit\/([0-9a-f]{7,40})/gi;
5077
5028
  LINEAR_ISSUE = /https?:\/\/linear\.app\/[\w.-]+\/issue\/([A-Z][A-Z0-9]*-\d+)/gi;
5078
5029
  JIRA_ISSUE = /https?:\/\/[\w.-]+\/browse\/([A-Z][A-Z0-9]+-\d+)/gi;
5079
5030
  SLUG_HASH = /(?:^|[\s([{<])([\w.-]+\/[\w.-]+)#(\d+)\b/g;
@@ -5103,8 +5054,7 @@ var init_schemas = __esm({
5103
5054
  // "github.com"
5104
5055
  remote_slug: external_exports.string().nullable(),
5105
5056
  // "org/repo"
5106
- branch: external_exports.string().nullable(),
5107
- commit_sha: external_exports.string().nullable()
5057
+ branch: external_exports.string().nullable()
5108
5058
  });
5109
5059
  RawEvent = external_exports.object({
5110
5060
  source_event_id: external_exports.string(),
@@ -5141,7 +5091,7 @@ var init_schemas = __esm({
5141
5091
  // summarize prompt; it never gets stored long-term server-side, only
5142
5092
  // used to construct the summarize input.
5143
5093
  content_excerpt: external_exports.string().max(320).optional(),
5144
- // Public code references (PRs, issues, commits, repos) detected on-device
5094
+ // Public code references (PRs, issues, repos) detected on-device
5145
5095
  // from this turn's FULL text — the high-recall feed the server rolls up into
5146
5096
  // SessionMetadata. Only public reference shapes (forge URLs, slugs, numbers,
5147
5097
  // ticket keys) ride here, never raw text — so it is derived pre-redaction
@@ -5191,7 +5141,19 @@ var init_schemas = __esm({
5191
5141
  source_event_ids: external_exports.array(external_exports.string()).max(2e3),
5192
5142
  /** Optional embedding of the abstract (BGE-small-en-v1.5, 384 dims).
5193
5143
  * Present when the daemon has an Embedder adapter configured. */
5194
- abstract_embedding: external_exports.array(external_exports.number()).length(384).optional()
5144
+ abstract_embedding: external_exports.array(external_exports.number()).length(384).optional(),
5145
+ /** Privacy-preserving on-device behavioral signal — COUNTS/RATIOS ONLY,
5146
+ * never raw text (mirrors RedactionReport). Powers server-side prompt-
5147
+ * friction detection. Optional so older daemons that omit it still validate. */
5148
+ behavior: external_exports.object({
5149
+ /** Developer messages in this segment. */
5150
+ user_turns: external_exports.number().int().nonnegative().default(0),
5151
+ /** User messages that land right after the assistant — a re-prompt /
5152
+ * correction proxy. */
5153
+ correction_count: external_exports.number().int().nonnegative().default(0),
5154
+ /** 0-1 frustration estimate (re-prompt density + negative mood tags). */
5155
+ frustration: external_exports.number().min(0).max(1).default(0)
5156
+ }).optional()
5195
5157
  });
5196
5158
  ToolAction = external_exports.object({
5197
5159
  /** Where it ran: `shell`, `mcp`, `builtin`, `browser`. (tier 0) */
@@ -5299,7 +5261,7 @@ var init_schemas = __esm({
5299
5261
  * no-op browser summariser). */
5300
5262
  session_titles: external_exports.record(external_exports.string(), external_exports.string().max(120)).optional(),
5301
5263
  /** Optional per-session deterministic metadata — session_id →
5302
- * {@link SessionMetadata}: the repos, pull requests, commits, and issues the
5264
+ * {@link SessionMetadata}: the repos, pull requests, and issues the
5303
5265
  * session touched, detected on-device across git context, tool calls,
5304
5266
  * redacted content, and the local model (so it works for any provider).
5305
5267
  * Additive — old daemons omit it, old servers ignore it (the wire has no
@@ -6043,8 +6005,7 @@ async function parseClaudeCodeJsonl(ctx) {
6043
6005
  remote_url: null,
6044
6006
  remote_host: slug?.includes("/") ? "github.com" : null,
6045
6007
  remote_slug: slug,
6046
- branch: gitBranch,
6047
- commit_sha: null
6008
+ branch: gitBranch
6048
6009
  } : null,
6049
6010
  tokens: {
6050
6011
  input: usage.input_tokens ?? 0,
@@ -6421,8 +6382,7 @@ async function parseCodexRollout(ctx) {
6421
6382
  remote_url: null,
6422
6383
  remote_host: slug.includes("/") ? "github.com" : null,
6423
6384
  remote_slug: slug,
6424
- branch: null,
6425
- commit_sha: null
6385
+ branch: null
6426
6386
  } : null,
6427
6387
  tokens: {
6428
6388
  input: tk.input_tokens ?? 0,
@@ -35317,11 +35277,11 @@ var init_prompts = __esm({
35317
35277
  "use strict";
35318
35278
  OLLAMA_CHAT_MODEL = "qwen3:4b";
35319
35279
  OLLAMA_EMBED_MODEL = "bge-small-en-v1.5";
35320
- SUMMARISER_SYSTEM_PROMPT = "You summarise an AI coding session in ONE sentence, \u2264 240 characters. If the user message includes sampled conversation excerpts, base your summary on what the developer was actually working on (the substance \u2014 what was being built, debugged, refactored, or designed). If only metadata is given, paraphrase the metadata. Never quote the excerpts verbatim. No PII, no code literals, no file paths, no API keys. Reply with only the sentence.";
35321
- SUMMARISER_MAX_TOKENS = 120;
35280
+ SUMMARISER_SYSTEM_PROMPT = 'You summarise an AI coding session in 1-2 sentences, \u2264 400 characters. Name the CONCRETE work: the action taken, WHAT it acted on, and the specific target \u2014 the repository (e.g. erpc/erpc), branch, service, or component \u2014 whenever the session facts or excerpts identify it. Good: "Triggered a GitHub Actions release for erpc/erpc"; "Fixed a null dereference in the auth middleware of api-gateway". Lead with an outcome verb and pack in concrete domain keywords (frameworks, features, decisions). Base the substance on the excerpts when present, else the metadata. Never quote excerpts verbatim. No PII, no secrets, no API keys, no absolute file paths. Reply with only the summary.';
35281
+ SUMMARISER_MAX_TOKENS = 180;
35322
35282
  SUMMARISER_TEMPERATURE = 0.2;
35323
35283
  QWEN_CHARS_PER_TOKEN = 3.3;
35324
- ABSTRACT_OUTPUT_MAX_CHARS = 240;
35284
+ ABSTRACT_OUTPUT_MAX_CHARS = 400;
35325
35285
  }
35326
35286
  });
35327
35287
 
@@ -35538,15 +35498,6 @@ async function buildSessionMetadata(segments, events, opts = {}) {
35538
35498
  });
35539
35499
  }
35540
35500
  if (e.git.branch) refs.issues.push(...detectBranchTickets(e.git.branch));
35541
- if (e.git.commit_sha) {
35542
- refs.commits.push({
35543
- sha: e.git.commit_sha,
35544
- slug: e.git.remote_slug ?? null,
35545
- url: null,
35546
- source: "git",
35547
- confidence: 0.6
35548
- });
35549
- }
35550
35501
  parts.push(refs);
35551
35502
  }
35552
35503
  if (opts.resolveGit) {
@@ -35843,7 +35794,7 @@ async function summariseSlice(sessionId, slice, adapters2) {
35843
35794
  Sampled excerpts from the conversation (already redacted of PII and secrets):
35844
35795
  ${excerptBlock}
35845
35796
 
35846
- Write the SHORTEST keyword-dense paragraph (1-3 sentences, \u2264${ABSTRACT_OUTPUT_MAX_CHARS} chars) naming exactly what was achieved. Lead with an outcome verb. Pack with concrete domain keywords (frameworks, features, components, decisions). Skip narration and filler.`;
35797
+ Write a \u2264${ABSTRACT_OUTPUT_MAX_CHARS}-char summary (1-2 sentences) naming exactly what was achieved: the concrete action, what it acted on, and the specific target (repo/branch/service/component) when identifiable from the context above. Lead with an outcome verb and pack in concrete domain keywords (frameworks, features, decisions). Skip narration and filler.`;
35847
35798
  const rawAbstract = await adapters2.summarize({
35848
35799
  prompt,
35849
35800
  maxTokens: SUMMARISER_MAX_TOKENS,
@@ -35871,15 +35822,16 @@ Write the SHORTEST keyword-dense paragraph (1-3 sentences, \u2264${ABSTRACT_OUTP
35871
35822
  }
35872
35823
  }
35873
35824
  const redacted = { text: abstractText, counts };
35874
- let abstractWithCognition = redacted.text;
35825
+ let cognition = null;
35875
35826
  if (adapters2.cognize) {
35876
35827
  try {
35877
- const tags2 = await adapters2.cognize({ abstract: redacted.text });
35878
- const suffix = formatCognitionSuffix(tags2);
35879
- if (suffix) abstractWithCognition = `${redacted.text} ${suffix}`;
35828
+ cognition = await adapters2.cognize({ abstract: redacted.text });
35880
35829
  } catch {
35881
35830
  }
35882
35831
  }
35832
+ const cognitionSuffix = cognition ? formatCognitionSuffix(cognition) : "";
35833
+ const abstractWithCognition = cognitionSuffix ? `${redacted.text} ${cognitionSuffix}` : redacted.text;
35834
+ const behavior = computeBehavior(slice, cognition);
35883
35835
  const tags = [
35884
35836
  { root_key: "agents", name: first.agent, confidence: 1 },
35885
35837
  { root_key: "providers", name: first.provider, confidence: 1 }
@@ -35948,7 +35900,35 @@ Write the SHORTEST keyword-dense paragraph (1-3 sentences, \u2264${ABSTRACT_OUTP
35948
35900
  // number-valued catchall for pf_*.
35949
35901
  redaction: redacted.counts,
35950
35902
  source_event_ids: sourceEventIds,
35951
- abstract_embedding: segmentEmbedding && segmentEmbedding.length === 384 ? segmentEmbedding : void 0
35903
+ abstract_embedding: segmentEmbedding && segmentEmbedding.length === 384 ? segmentEmbedding : void 0,
35904
+ behavior
35905
+ };
35906
+ }
35907
+ function computeBehavior(slice, cognition) {
35908
+ let userTurns = 0;
35909
+ let correctionCount = 0;
35910
+ let prevWasAssistant = false;
35911
+ for (const ev of slice) {
35912
+ if (ev.kind === "user_message") {
35913
+ userTurns++;
35914
+ if (prevWasAssistant) correctionCount++;
35915
+ prevWasAssistant = false;
35916
+ } else if (ev.kind === "assistant_message") {
35917
+ prevWasAssistant = true;
35918
+ }
35919
+ }
35920
+ const frustratedMood = cognition?.emotions?.some((e) => {
35921
+ const lower = e.toLowerCase();
35922
+ return FRUSTRATION_MARKERS.some((m) => lower.includes(m));
35923
+ }) ?? false;
35924
+ const frustration = Math.min(
35925
+ 1,
35926
+ Math.max(correctionCount / 4, frustratedMood ? 0.8 : 0)
35927
+ );
35928
+ return {
35929
+ user_turns: userTurns,
35930
+ correction_count: correctionCount,
35931
+ frustration: Math.round(frustration * 100) / 100
35952
35932
  };
35953
35933
  }
35954
35934
  function sampleAndRedactExcerpts(slice) {
@@ -36003,7 +35983,7 @@ function inferEnvironment(branch) {
36003
35983
  if (b === "dev" || b === "develop" || b.startsWith("dev/")) return "Dev";
36004
35984
  return null;
36005
35985
  }
36006
- var SEGMENT_TIME_GAP_MS, SEGMENT_TOPIC_THRESHOLD, SEGMENT_MAX_TURNS, SEGMENT_MAX_DURATION_MS, SEGMENT_MAX_CONTENT_CHARS, ABSTRACT_MAX_CHARS;
35986
+ var SEGMENT_TIME_GAP_MS, SEGMENT_TOPIC_THRESHOLD, SEGMENT_MAX_TURNS, SEGMENT_MAX_DURATION_MS, SEGMENT_MAX_CONTENT_CHARS, ABSTRACT_MAX_CHARS, FRUSTRATION_MARKERS;
36007
35987
  var init_pipeline = __esm({
36008
35988
  "../../packages/daemon-core/src/pipeline/index.ts"() {
36009
35989
  "use strict";
@@ -36025,6 +36005,17 @@ var init_pipeline = __esm({
36025
36005
  SEGMENT_MAX_DURATION_MS = 30 * 6e4;
36026
36006
  SEGMENT_MAX_CONTENT_CHARS = 12e3;
36027
36007
  ABSTRACT_MAX_CHARS = 512;
36008
+ FRUSTRATION_MARKERS = [
36009
+ "frustrat",
36010
+ "annoy",
36011
+ "stuck",
36012
+ "confus",
36013
+ "irritat",
36014
+ "block",
36015
+ "stress",
36016
+ "angr",
36017
+ "overwhelm"
36018
+ ];
36028
36019
  }
36029
36020
  });
36030
36021
 
@@ -37328,7 +37319,7 @@ var init_scan = __esm({
37328
37319
  init_api();
37329
37320
  init_config2();
37330
37321
  init_pipeline2();
37331
- DAEMON_VERSION = true ? "daemon-0.11.0" : "daemon-dev";
37322
+ DAEMON_VERSION = true ? "daemon-0.12.0" : "daemon-dev";
37332
37323
  BATCH_MAX_EVENTS = INGEST_BATCH_MAX_EVENTS;
37333
37324
  BATCH_MAX_TOOL_CALLS = 2e4;
37334
37325
  BATCH_BUFFER_HARD_CAP = BATCH_MAX_EVENTS * 2;
@@ -37895,7 +37886,7 @@ var PROCESSING_VERSION;
37895
37886
  var init_processing_version = __esm({
37896
37887
  "src/processing-version.ts"() {
37897
37888
  "use strict";
37898
- PROCESSING_VERSION = 9;
37889
+ PROCESSING_VERSION = 10;
37899
37890
  }
37900
37891
  });
37901
37892
 
@@ -40275,7 +40266,7 @@ var init_daemon = __esm({
40275
40266
  init_scan();
40276
40267
  init_single_flight();
40277
40268
  init_update();
40278
- DAEMON_VERSION2 = true ? "daemon-0.11.0" : "daemon-dev";
40269
+ DAEMON_VERSION2 = true ? "daemon-0.12.0" : "daemon-dev";
40279
40270
  HEARTBEAT_INTERVAL_MS = 1e4;
40280
40271
  SCAN_INTERVAL_MS = 5 * 60 * 1e3;
40281
40272
  DISCOVERY_INTERVAL_MS = 6e4;
@@ -40907,7 +40898,7 @@ function tryOpenBrowser(url) {
40907
40898
  return false;
40908
40899
  }
40909
40900
  }
40910
- var DAEMON_VERSION3 = true ? "daemon-0.11.0" : "daemon-dev";
40901
+ var DAEMON_VERSION3 = true ? "daemon-0.12.0" : "daemon-dev";
40911
40902
  function osFamily() {
40912
40903
  const p = platform6();
40913
40904
  if (p === "darwin") return "macos";