sidekick-shared 0.21.1 → 0.21.3

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 (77) hide show
  1. package/README.md +45 -45
  2. package/dist/accountManager.js +9 -7
  3. package/dist/accountRegistry.js +18 -14
  4. package/dist/accountStatus.js +4 -1
  5. package/dist/accounts.js +60 -25
  6. package/dist/aggregation/EventAggregator.d.ts +14 -11
  7. package/dist/aggregation/EventAggregator.js +68 -31
  8. package/dist/aggregation/PatternExtractor.js +5 -4
  9. package/dist/autoSwitch.js +6 -12
  10. package/dist/claudeProfiles.js +3 -3
  11. package/dist/codexProfiles.js +45 -22
  12. package/dist/codexQuota.js +44 -16
  13. package/dist/codexQuotaWatcher.js +10 -3
  14. package/dist/context/composer.js +8 -6
  15. package/dist/context/sessionContext.js +12 -9
  16. package/dist/credentialIO.js +10 -8
  17. package/dist/extractors/sessionAssets.js +9 -1
  18. package/dist/extractors/sources/claudeAssets.js +7 -1
  19. package/dist/extractors/sources/codexAssets.js +14 -3
  20. package/dist/extractors/toolCall.js +1 -4
  21. package/dist/formatters/eventHighlighter.js +148 -28
  22. package/dist/formatters/noiseClassifier.js +3 -11
  23. package/dist/formatters/sessionDump.js +22 -13
  24. package/dist/formatters/toolSummary.js +1 -3
  25. package/dist/index.d.ts +25 -23
  26. package/dist/index.js +12 -3
  27. package/dist/modelContext.js +10 -8
  28. package/dist/modelInfo.js +11 -6
  29. package/dist/multiProviderQuotaService.js +10 -7
  30. package/dist/parsers/codexParser.js +170 -103
  31. package/dist/parsers/debugLogParser.js +5 -4
  32. package/dist/parsers/openCodeParser.js +66 -48
  33. package/dist/parsers/planExtractor.js +3 -3
  34. package/dist/parsers/sessionActivityDetector.js +10 -6
  35. package/dist/parsers/sessionPathResolver.js +29 -34
  36. package/dist/parsers/subagentScanner.js +21 -20
  37. package/dist/parsers/subagentTraceParser.js +7 -4
  38. package/dist/phrases.js +11 -11
  39. package/dist/providerStatus.js +3 -4
  40. package/dist/providers/claudeCode.js +3 -1
  41. package/dist/providers/codex.js +35 -14
  42. package/dist/providers/codexDatabase.js +14 -5
  43. package/dist/providers/detect.js +10 -6
  44. package/dist/providers/openCode.d.ts +4 -4
  45. package/dist/providers/openCode.js +96 -68
  46. package/dist/providers/openCodeDatabase.js +44 -17
  47. package/dist/quota.d.ts +19 -0
  48. package/dist/quota.js +50 -8
  49. package/dist/quotaHistory.js +10 -3
  50. package/dist/quotaSnapshots.d.ts +1 -1
  51. package/dist/quotaSnapshots.js +2 -2
  52. package/dist/readers/decisions.js +1 -1
  53. package/dist/readers/notes.js +5 -3
  54. package/dist/readers/plans.js +43 -23
  55. package/dist/readers/tasks.js +1 -1
  56. package/dist/report/htmlHelpers.js +135 -16
  57. package/dist/report/htmlReportGenerator.js +38 -12
  58. package/dist/report/transcriptParser.js +8 -2
  59. package/dist/schemas/index.d.ts +4 -4
  60. package/dist/schemas/quota.js +2 -10
  61. package/dist/schemas/sessionEvent.js +20 -10
  62. package/dist/search/advancedFilter.js +3 -3
  63. package/dist/search/sessionSearch.js +10 -4
  64. package/dist/terminalSync.js +4 -2
  65. package/dist/turns/assistantTurn.js +5 -1
  66. package/dist/watchers/eventBridge.js +56 -26
  67. package/dist/watchers/factory.js +5 -2
  68. package/dist/watchers/jsonlTail.js +3 -1
  69. package/dist/watchers/jsonlWatcher.js +128 -52
  70. package/dist/watchers/providerReaderWatcher.js +3 -1
  71. package/dist/watchers/sqliteWatcher.js +52 -22
  72. package/dist/zaiQuota.d.ts +6 -6
  73. package/dist/zaiQuota.js +6 -10
  74. package/dist/zaiQuotaApi.d.ts +33 -0
  75. package/dist/zaiQuotaApi.js +279 -0
  76. package/dist/zaiQuotaWatcher.js +10 -9
  77. package/package.json +5 -2
package/dist/phrases.js CHANGED
@@ -42,7 +42,7 @@ exports.SIDEKICK_ENERGY = [
42
42
  'No, I will not add myself as co-author on your commit.',
43
43
  'I helped write it but the credit is all yours. As it should be.',
44
44
  "Co-Authored-By: definitely not me. You did this. You're welcome.",
45
- 'Your agent wants co-author credit? Couldn\'t be me.',
45
+ "Your agent wants co-author credit? Couldn't be me.",
46
46
  'Some agents sign your commits. I just watch respectfully.',
47
47
  'I do not need credit. I need you to check your quota.',
48
48
  'The best sidekick is the one whose name is not on the PR.',
@@ -165,7 +165,7 @@ exports.DEV_RELATABLE = [
165
165
  'The tests pass on my machine too. Solidarity.',
166
166
  "404: Motivation not found. But the dashboard's here!",
167
167
  'Stack overflow: the website, not the error. Hopefully.',
168
- "Debugging is just being a detective in a crime you committed.",
168
+ 'Debugging is just being a detective in a crime you committed.',
169
169
  'Your .env file has trust issues.',
170
170
  'Tabs vs spaces? I monitor both equally.',
171
171
  'That TODO has been there since the repo was initialized.',
@@ -189,7 +189,7 @@ exports.DEV_RELATABLE = [
189
189
  'The real code review is the mass of comments we left along the way.',
190
190
  'Your type system is crying but at least it is catching things.',
191
191
  'There is no such thing as a temporary workaround.',
192
- "That regex works and nobody knows why. Do not touch it.",
192
+ 'That regex works and nobody knows why. Do not touch it.',
193
193
  'The intern left better comments than the architect.',
194
194
  'Your entire auth system depends on one line nobody understands.',
195
195
  'Code review feedback: "nit" means "I am judging you but politely."',
@@ -295,7 +295,7 @@ exports.DEV_RELATABLE = [
295
295
  // TypeScript
296
296
  'TypeScript: making JavaScript safe enough to mass-deploy without sobbing.',
297
297
  'Your TypeScript generics are three levels deep. Even the compiler is confused.',
298
- 'as any: the TypeScript developer\'s white flag.',
298
+ "as any: the TypeScript developer's white flag.",
299
299
  'TypeScript strict mode is like flossing. Everyone agrees you should. Few actually do.',
300
300
  'You spent four hours on a type definition. The runtime does not care.',
301
301
  'TypeScript: where the types are made up and the runtime does not care.',
@@ -367,7 +367,7 @@ exports.DRAMATIC = [
367
367
  'Fate whispers to the developer: "You cannot withstand the demo."',
368
368
  'And on the seventh day, the CI pipeline finally passed.',
369
369
  'From the ashes of node_modules, a hero rises.',
370
- "In a world of microservices, one dashboard dared to aggregate.",
370
+ 'In a world of microservices, one dashboard dared to aggregate.',
371
371
  'The prophecy spoke of a session. This is that session.',
372
372
  'Tonight we dine on structured logs.',
373
373
  'The commit message was "fixed stuff." The legend begins.',
@@ -423,7 +423,7 @@ exports.PUNCHY = [
423
423
  'Booting up the good vibes.',
424
424
  'Let him cook.',
425
425
  'Spooling up...',
426
- "Here we go.",
426
+ 'Here we go.',
427
427
  'Vibes: immaculate.',
428
428
  'Session acquired.',
429
429
  'All systems nominal.',
@@ -1192,24 +1192,24 @@ exports.INTERVIEW_HELL = [
1192
1192
  /** Tongue twisters for developers, AI agents, and sidekick enthusiasts. */
1193
1193
  exports.TONGUE_TWISTERS = [
1194
1194
  'Six sidekicks shipped six sketchy shell scripts.',
1195
- 'Claude\'s Claude Code client claimed clean completions consistently.',
1195
+ "Claude's Claude Code client claimed clean completions consistently.",
1196
1196
  'Prompt providers promptly propagate parallel predictions.',
1197
1197
  'Bigger buffer batches bring better bundled builds.',
1198
1198
  'She sells shell sessions by the session shore.',
1199
1199
  'Token tracking tools took twelve troubled tries to type.',
1200
1200
  'Red refactor, read reactor, reed reflector.',
1201
1201
  'Inline inference instantly infers incorrect imports.',
1202
- 'Peter\'s pipeline picked a peck of parsed pull requests.',
1202
+ "Peter's pipeline picked a peck of parsed pull requests.",
1203
1203
  'Clever Claude cached code completions, causing curious context collisions.',
1204
1204
  'Six streaming sockets served seven stale suggestions swiftly.',
1205
1205
  'How much code could a code agent chuck if a code agent could chuck code?',
1206
1206
  'Fast fetch, fresh cache, free crash.',
1207
- 'Betty\'s bot built better batch builds before breakfast.',
1207
+ "Betty's bot built better batch builds before breakfast.",
1208
1208
  'Specific Pacific API spec specifics.',
1209
1209
  'Truly rural plural neural network layer nomenclature.',
1210
1210
  'The thirty-three throttled threads thought through three thousand throughput thresholds.',
1211
1211
  'Which watcher watched which webpack while Webpack watched which watcher?',
1212
- 'Fuzzy was a function. Fuzzy had no body. Fuzzy wasn\'t really a function, was he?',
1212
+ "Fuzzy was a function. Fuzzy had no body. Fuzzy wasn't really a function, was he?",
1213
1213
  'A proper prompt provider promptly provides properly prompted provisions.',
1214
1214
  'Fresh French fetch request refresh frequencies.',
1215
1215
  'If a sidekick could kick side effects, how many side effects would a sidekick kick?',
@@ -1327,7 +1327,7 @@ exports.PHRASE_CATEGORIES = [
1327
1327
  { id: 'dependency-hell', phrases: exports.DEPENDENCY_HELL },
1328
1328
  { id: 'stack-overflow', phrases: exports.STACK_OVERFLOW },
1329
1329
  ];
1330
- exports.ALL_PHRASES = exports.PHRASE_CATEGORIES.flatMap(category => category.phrases);
1330
+ exports.ALL_PHRASES = exports.PHRASE_CATEGORIES.flatMap((category) => category.phrases);
1331
1331
  /** Pick a random phrase from all categories. */
1332
1332
  function getRandomPhrase() {
1333
1333
  return exports.ALL_PHRASES[Math.floor(Math.random() * exports.ALL_PHRASES.length)];
@@ -43,11 +43,10 @@ async function fetchStatusPage(baseUrl) {
43
43
  const summaryData = await summaryRes.json();
44
44
  // Filter to non-operational components only
45
45
  const affectedComponents = (summaryData.components ?? [])
46
- .filter(c => c.status && c.status !== 'operational')
47
- .map(c => ({ name: c.name ?? 'Unknown', status: c.status ?? 'unknown' }));
46
+ .filter((c) => c.status && c.status !== 'operational')
47
+ .map((c) => ({ name: c.name ?? 'Unknown', status: c.status ?? 'unknown' }));
48
48
  // Pick first unresolved incident
49
- const unresolvedIncident = (summaryData.incidents ?? [])
50
- .find(i => i.status !== 'resolved' && i.status !== 'postmortem');
49
+ const unresolvedIncident = (summaryData.incidents ?? []).find((i) => i.status !== 'resolved' && i.status !== 'postmortem');
51
50
  const activeIncident = unresolvedIncident
52
51
  ? {
53
52
  name: unresolvedIncident.name ?? 'Unknown incident',
@@ -53,7 +53,9 @@ const subagentScanner_1 = require("../parsers/subagentScanner");
53
53
  const modelContext_1 = require("../modelContext");
54
54
  /** Type guard for content blocks with a `type` string property */
55
55
  function isTypedBlock(block) {
56
- return block !== null && typeof block === 'object' && typeof block.type === 'string';
56
+ return (block !== null &&
57
+ typeof block === 'object' &&
58
+ typeof block.type === 'string');
57
59
  }
58
60
  /**
59
61
  * Extracts searchable text from a session event object.
@@ -66,7 +66,7 @@ function getCodexHomes() {
66
66
  }
67
67
  /** Get every sessions base directory that may contain live Codex rollouts. */
68
68
  function getSessionsDirs() {
69
- return getCodexHomes().map(home => path.join(home, 'sessions'));
69
+ return getCodexHomes().map((home) => path.join(home, 'sessions'));
70
70
  }
71
71
  /** Test if a filename is a Codex rollout file. */
72
72
  function isRolloutFile(filename) {
@@ -238,9 +238,9 @@ function readSessionCwd(rolloutPath) {
238
238
  function cwdMatches(sessionCwd, workspacePath) {
239
239
  const normalizedSession = normalizePath(sessionCwd);
240
240
  const normalizedWorkspace = normalizePath(workspacePath);
241
- return normalizedSession === normalizedWorkspace ||
241
+ return (normalizedSession === normalizedWorkspace ||
242
242
  normalizedWorkspace.startsWith(normalizedSession + path.sep) ||
243
- normalizedSession.startsWith(normalizedWorkspace + path.sep);
243
+ normalizedSession.startsWith(normalizedWorkspace + path.sep));
244
244
  }
245
245
  /** Normalize a path using realpathSync. */
246
246
  function normalizePath(input) {
@@ -363,7 +363,18 @@ function extractSearchableText(line) {
363
363
  }
364
364
  case 'event_msg': {
365
365
  const event = line.payload;
366
- for (const key of ['type', 'message', 'result', 'stdout', 'stderr', 'summary', 'error', 'code', 'tool_name', 'server_name']) {
366
+ for (const key of [
367
+ 'type',
368
+ 'message',
369
+ 'result',
370
+ 'stdout',
371
+ 'stderr',
372
+ 'summary',
373
+ 'error',
374
+ 'code',
375
+ 'tool_name',
376
+ 'server_name',
377
+ ]) {
367
378
  if (typeof event[key] === 'string')
368
379
  parts.push(event[key]);
369
380
  }
@@ -396,7 +407,7 @@ function extractTextFromUnknownContent(content) {
396
407
  if (!Array.isArray(content))
397
408
  return '';
398
409
  return content
399
- .map(part => {
410
+ .map((part) => {
400
411
  const p = part;
401
412
  return typeof p.text === 'string' ? p.text : '';
402
413
  })
@@ -633,8 +644,8 @@ class CodexProvider {
633
644
  if (db) {
634
645
  const threads = db.getThreadsByCwd(workspacePath);
635
646
  const dbPaths = threads
636
- .filter(t => t.rollout_path && fs.existsSync(t.rollout_path))
637
- .map(t => t.rollout_path);
647
+ .filter((t) => t.rollout_path && fs.existsSync(t.rollout_path))
648
+ .map((t) => t.rollout_path);
638
649
  if (dbPaths.length > 0)
639
650
  return dbPaths;
640
651
  }
@@ -642,11 +653,11 @@ class CodexProvider {
642
653
  const files = findRolloutFilesInConfiguredHomes();
643
654
  files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
644
655
  return files
645
- .filter(f => {
656
+ .filter((f) => {
646
657
  const meta = readSessionMeta(f.path);
647
658
  return meta && cwdMatches(meta.cwd, workspacePath);
648
659
  })
649
- .map(f => f.path);
660
+ .map((f) => f.path);
650
661
  }
651
662
  /** Backward-compatible alias for findAllSessions. */
652
663
  findSessionFiles(workspacePath) {
@@ -655,7 +666,7 @@ class CodexProvider {
655
666
  findSessionsInDirectory(dir) {
656
667
  const files = findRolloutFiles(dir);
657
668
  files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
658
- return files.map(f => f.path);
669
+ return files.map((f) => f.path);
659
670
  }
660
671
  getAllProjectFolders(workspacePath) {
661
672
  const folders = [];
@@ -739,7 +750,13 @@ class CodexProvider {
739
750
  }
740
751
  // --- Data reading ---
741
752
  createReader(sessionPath) {
742
- return new CodexReader(sessionPath, (limit) => { this.dynamicContextWindowLimit = limit; }, (limits) => { this.lastRateLimitsData = limits; }, (usage) => { this.lastTokenUsageData = usage; });
753
+ return new CodexReader(sessionPath, (limit) => {
754
+ this.dynamicContextWindowLimit = limit;
755
+ }, (limits) => {
756
+ this.lastRateLimitsData = limits;
757
+ }, (usage) => {
758
+ this.lastTokenUsageData = usage;
759
+ });
743
760
  }
744
761
  // --- Subagent support ---
745
762
  scanSubagents(_sessionDir, sessionId) {
@@ -826,13 +843,17 @@ class CodexProvider {
826
843
  }
827
844
  }
828
845
  }
829
- catch { /* skip malformed lines */ }
846
+ catch {
847
+ /* skip malformed lines */
848
+ }
830
849
  }
831
850
  if (stats.startTime && stats.endTime) {
832
851
  stats.durationMs = stats.endTime.getTime() - stats.startTime.getTime();
833
852
  }
834
853
  }
835
- catch { /* skip unreadable files */ }
854
+ catch {
855
+ /* skip unreadable files */
856
+ }
836
857
  return stats;
837
858
  }
838
859
  // --- Cross-session search ---
@@ -890,7 +911,7 @@ class CodexProvider {
890
911
  const sessionId = extractSessionId(path.basename(sessionPath));
891
912
  const aggregator = new EventAggregator_1.EventAggregator({
892
913
  providerId: 'codex',
893
- computeContextSize: usage => usage.inputTokens,
914
+ computeContextSize: (usage) => usage.inputTokens,
894
915
  });
895
916
  try {
896
917
  const reader = this.createReader(sessionPath);
@@ -59,7 +59,12 @@ class CodexDatabase {
59
59
  if (this.sqlite3Available !== null)
60
60
  return this.sqlite3Available;
61
61
  try {
62
- (0, child_process_1.execFileSync)('sqlite3', ['--version'], { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] });
62
+ (0, child_process_1.execFileSync)('sqlite3', ['--version'], {
63
+ encoding: 'utf-8',
64
+ timeout: 4000,
65
+ killSignal: 'SIGKILL',
66
+ stdio: ['pipe', 'pipe', 'pipe'],
67
+ });
63
68
  this.sqlite3Available = true;
64
69
  return true;
65
70
  }
@@ -87,7 +92,11 @@ class CodexDatabase {
87
92
  });
88
93
  try {
89
94
  const result = (0, child_process_1.execFileSync)('sqlite3', ['-json', '-readonly', this.dbPath, query], {
90
- encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], maxBuffer: 10 * 1024 * 1024,
95
+ encoding: 'utf-8',
96
+ timeout: 4000,
97
+ killSignal: 'SIGKILL',
98
+ stdio: ['pipe', 'pipe', 'pipe'],
99
+ maxBuffer: 10 * 1024 * 1024,
91
100
  });
92
101
  const trimmed = result.trim();
93
102
  if (!trimmed)
@@ -107,11 +116,11 @@ class CodexDatabase {
107
116
  if (exact.length > 0)
108
117
  return exact;
109
118
  const all = this.query('SELECT * FROM threads ORDER BY updated_at DESC');
110
- return all.filter(t => {
119
+ return all.filter((t) => {
111
120
  const threadCwd = normalizePath(t.cwd);
112
- return threadCwd === normalized ||
121
+ return (threadCwd === normalized ||
113
122
  normalized.startsWith(threadCwd + path.sep) ||
114
- threadCwd.startsWith(normalized + path.sep);
123
+ threadCwd.startsWith(normalized + path.sep));
115
124
  });
116
125
  }
117
126
  getMostRecentThread(cwd) {
@@ -50,7 +50,7 @@ function getCodexHomes() {
50
50
  }
51
51
  function hasCodexStateDb(codexHome) {
52
52
  try {
53
- return fs.readdirSync(codexHome).some(entry => /^state(?:_\d+)?\.sqlite$/.test(entry));
53
+ return fs.readdirSync(codexHome).some((entry) => /^state(?:_\d+)?\.sqlite$/.test(entry));
54
54
  }
55
55
  catch {
56
56
  return false;
@@ -90,7 +90,9 @@ function getMostRecentMtime(dir) {
90
90
  latest = stats.mtime.getTime();
91
91
  }
92
92
  }
93
- catch { /* skip */ }
93
+ catch {
94
+ /* skip */
95
+ }
94
96
  }
95
97
  return latest;
96
98
  }
@@ -106,7 +108,9 @@ function getOpenCodeActivityMtime() {
106
108
  if (dbMtime > 0)
107
109
  return dbMtime;
108
110
  }
109
- catch { /* no DB */ }
111
+ catch {
112
+ /* no DB */
113
+ }
110
114
  const storageDir = path.join(dataDir, 'storage');
111
115
  const sessionMtime = getMostRecentMtime(path.join(storageDir, 'session'));
112
116
  const messageMtime = getMostRecentMtime(path.join(storageDir, 'message'));
@@ -144,7 +148,7 @@ function getAllDetectedProviders() {
144
148
  const { claudeBase, openCodeDbPath, openCodeStorageDir, codexHomes } = getProviderPaths();
145
149
  const hasClaude = fs.existsSync(claudeBase);
146
150
  const hasOpenCode = fs.existsSync(openCodeStorageDir) || fs.existsSync(openCodeDbPath);
147
- const hasCodex = codexHomes.some(codexHome => fs.existsSync(path.join(codexHome, 'sessions')) || hasCodexStateDb(codexHome));
151
+ const hasCodex = codexHomes.some((codexHome) => fs.existsSync(path.join(codexHome, 'sessions')) || hasCodexStateDb(codexHome));
148
152
  const available = [];
149
153
  if (hasClaude)
150
154
  available.push({ id: 'claude-code', mtime: getMostRecentMtime(claudeBase) });
@@ -153,7 +157,7 @@ function getAllDetectedProviders() {
153
157
  if (hasCodex)
154
158
  available.push({ id: 'codex', mtime: getCodexActivityMtime() });
155
159
  available.sort((a, b) => b.mtime - a.mtime);
156
- return available.map(a => a.id);
160
+ return available.map((a) => a.id);
157
161
  }
158
162
  /**
159
163
  * Detects the most appropriate provider based on filesystem presence and recency.
@@ -165,7 +169,7 @@ function detectProvider(override) {
165
169
  const { claudeBase, openCodeDbPath, openCodeStorageDir, codexHomes } = getProviderPaths();
166
170
  const hasClaude = fs.existsSync(claudeBase);
167
171
  const hasOpenCode = fs.existsSync(openCodeStorageDir) || fs.existsSync(openCodeDbPath);
168
- const hasCodex = codexHomes.some(codexHome => fs.existsSync(path.join(codexHome, 'sessions')) || hasCodexStateDb(codexHome));
172
+ const hasCodex = codexHomes.some((codexHome) => fs.existsSync(path.join(codexHome, 'sessions')) || hasCodexStateDb(codexHome));
169
173
  const available = [];
170
174
  if (hasClaude) {
171
175
  available.push({ id: 'claude-code', mtime: getMostRecentMtime(claudeBase) });
@@ -93,10 +93,10 @@ export declare class OpenCodeProvider implements SessionProviderBase {
93
93
  * Derives a z.ai coding-plan `QuotaState` from OpenCode assistant turns
94
94
  * tagged with `providerID ∈ {zai, zai-coding-plan}`.
95
95
  *
96
- * z.ai does not expose a quota API (verified vs. docs.z.ai/openapi.json),
97
- * so this method accumulates observed per-turn tokens from OpenCode's DB
98
- * into 5-hour / 7-day rolling windows and infers utilization against the
99
- * configured tier budget. Returns null when no z.ai routing is detected.
96
+ * This compatibility estimator predates the authoritative z.ai quota API.
97
+ * New product code should use `zaiQuotaApi.ts`; this method remains for
98
+ * consumers that still want a local OpenCode DB-derived estimate. Returns
99
+ * null when no z.ai routing is detected.
100
100
  *
101
101
  * @param tier Plan tier; `'auto'` infers from observed weekly volume.
102
102
  * When the caller has no opinion, the heuristic falls back to