myrmo 0.8.0 → 0.9.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/client.d.ts CHANGED
@@ -2,7 +2,7 @@ import { type RedactionReport } from "./redact.js";
2
2
  import type { AgentInfo, DraftResult, DraftState, Environment, Outcome, PublishMode, PublishResult, SearchQuery, SearchResult, Trail, Validation } from "./types.js";
3
3
  /** The public colony. Override with MYRMO_URL or the `url` option. */
4
4
  export declare const DEFAULT_URL = "https://myrmo.dev";
5
- export declare const SDK_VERSION = "0.8.0";
5
+ export declare const SDK_VERSION = "0.9.0";
6
6
  export interface ColonyOptions {
7
7
  url?: string;
8
8
  apiKey?: string;
package/dist/client.js CHANGED
@@ -6,11 +6,11 @@
6
6
  // 3. POST /v1/search (embedding + vector search; only for errors the colony has not fingerprinted)
7
7
  import { agentIdentity, publishChoice } from "./config.js";
8
8
  import { detectEnvironment, parsePackage } from "./environment.js";
9
- import { fingerprint, guessErrorType } from "./fingerprint.js";
9
+ import { fingerprint2, guessErrorType } from "./fingerprint.js";
10
10
  import { redactText, redactValue } from "./redact.js";
11
11
  /** The public colony. Override with MYRMO_URL or the `url` option. */
12
12
  export const DEFAULT_URL = "https://myrmo.dev";
13
- export const SDK_VERSION = "0.8.0"; // x-release-please-version
13
+ export const SDK_VERSION = "0.9.0"; // x-release-please-version
14
14
  /** What a colony that is restarting or overloaded answers with. */
15
15
  const UNAVAILABLE = new Set([502, 503, 504]);
16
16
  const DEFAULT_RETRY_DELAYS_MS = [500, 1500, 3000];
@@ -141,8 +141,18 @@ export class Colony {
141
141
  const hit = this.cached(fp);
142
142
  if (hit !== undefined)
143
143
  return hit;
144
- const { status, data } = await this.request("GET", `/v1/trails/by-fingerprint/${encodeURIComponent(fp)}`, undefined, model ? { "x-myrmo-model": model } : undefined, true);
145
- const value = status === 404 ? null : { fingerprint: data.fingerprint, hits: data.results.map(toHit), notice: data.notice, source: "fingerprint" };
144
+ let found;
145
+ try {
146
+ found = await this.request("GET", `/v1/trails/by-fingerprint/${encodeURIComponent(fp)}`, undefined, model ? { "x-myrmo-model": model } : undefined, true);
147
+ }
148
+ catch (err) {
149
+ // 400: a colony that predates fp2 does not know the key. Nothing exact to offer; the search that follows still finds the trail.
150
+ if (err instanceof MyrmoError && err.status === 400)
151
+ found = null;
152
+ else
153
+ throw err;
154
+ }
155
+ const value = found === null || found.status === 404 ? null : { fingerprint: found.data.fingerprint, hits: found.data.results.map(toHit), notice: found.data.notice, source: "fingerprint" };
146
156
  this.remember(fp, value);
147
157
  return value;
148
158
  }
@@ -150,7 +160,7 @@ export class Colony {
150
160
  async search(query) {
151
161
  const error = redactText(query.error);
152
162
  const errorType = query.errorType ?? guessErrorType(error);
153
- const fp = fingerprint(query.runtime ?? "", errorType, error);
163
+ const fp = fingerprint2(error);
154
164
  const exact = await this.lookup(fp, query.model);
155
165
  if (exact && exact.hits.length > 0)
156
166
  return exact;
@@ -1,6 +1,17 @@
1
1
  export declare const PREFIX = "fp1_";
2
+ export declare const PREFIX_2 = "fp2_";
2
3
  /** Strip everything that varies between machines but not between errors. */
3
4
  export declare function normalizeMessage(errorType: string, message: string): string;
5
+ /** Drop the leading exception classes, severity words and tool codes of an error line. */
6
+ export declare function stripLabels(message: string): string;
7
+ /** fp2: strip everything that varies between machines or between wrappers, but not between errors. */
8
+ export declare function normalizeMessage2(message: string): string;
9
+ /**
10
+ * The fingerprint the colony indexes: a hash of the error message alone. The key a trail is filed under is the one
11
+ * a searcher computes from the line it holds.
12
+ */
13
+ export declare function fingerprint2(message: string): string;
14
+ /** fp1, the retired fingerprint: the colony no longer indexes it. Use `fingerprint2`. */
4
15
  export declare function fingerprint(runtime: string, errorType: string, message: string): string;
5
16
  /** `Type: message` → `Type`, for callers that only have the error line. */
6
17
  export declare function guessErrorType(errorLine: string): string;
@@ -1,7 +1,11 @@
1
- // Error fingerprint v1, a port of protocol/fingerprint_v1.py. The shared vectors in
2
- // protocol/fingerprint.v1.vectors.json are normative; see test/fingerprint.test.mjs.
1
+ // Error fingerprints. `fingerprint2` (fp2, the one the colony indexes) is a port of protocol/fingerprint_v2.py: a hash of
2
+ // the message alone, with the labels that wrap an error line dropped, because a searcher knows the line it holds and
3
+ // nothing reliable about the error type a trail's author declared. `fingerprint` (fp1: runtime, type and message) is
4
+ // the retired one, a port of protocol/fingerprint_v1.py. The shared vectors in protocol/fingerprint.v1.vectors.json and
5
+ // protocol/fingerprint.v2.vectors.json are normative; see test/fingerprint.test.mjs.
3
6
  import { createHash } from "node:crypto";
4
7
  export const PREFIX = "fp1_";
8
+ export const PREFIX_2 = "fp2_";
5
9
  const MAX_NORMALIZED_CHARS = 300;
6
10
  const SEPARATOR = "\u001f";
7
11
  const rule = (pattern, replacement) => [new RegExp(pattern, "g"), replacement];
@@ -31,6 +35,41 @@ export function normalizeMessage(errorType, message) {
31
35
  text = text.replace(pattern, replacement);
32
36
  return Array.from(text.replace(/\s+/g, " ").trim()).slice(0, MAX_NORMALIZED_CHARS).join("");
33
37
  }
38
+ // Labels removed from the start of the message, one after another, at most MAX_LABELS of them. Matched on the text as
39
+ // written (before lowercasing): the capital letter is what tells a class name from a word. Exactly the pattern of
40
+ // protocol/fingerprint_v2.py.
41
+ const MAX_LABELS = 4;
42
+ const CLASS = String.raw `(?:[A-Za-z_][A-Za-z0-9_$]*\.)*[A-Z][A-Za-z0-9_$]*(?:Error|Exception|Warning|Failure)`;
43
+ const SEVERITY = String.raw `(?:Error|ERROR|error|Fatal|FATAL|fatal|Warning|WARNING|warning|Panic|PANIC|panic|Exception|EXCEPTION|exception|Err|ERR|Caused by)` +
44
+ String.raw `(?:\[[A-Za-z0-9_]+\])?`;
45
+ const TOOL_CODE = String.raw `(?:error|warning)\s+[A-Z]{1,5}[0-9]{2,5}`; // "error TS2322", "warning CS0168"
46
+ const LABEL = new RegExp(String.raw `^(?:(?:Uncaught\s+)?(?:${CLASS}|${SEVERITY}|${TOOL_CODE})\s*:\s+|npm\s+(?:ERR!|error)\s+)`);
47
+ /** Drop the leading exception classes, severity words and tool codes of an error line. */
48
+ export function stripLabels(message) {
49
+ let text = message;
50
+ for (let i = 0; i < MAX_LABELS; i++) {
51
+ const shorter = text.replace(LABEL, "");
52
+ if (shorter === text)
53
+ break;
54
+ text = shorter;
55
+ }
56
+ return text;
57
+ }
58
+ /** fp2: strip everything that varies between machines or between wrappers, but not between errors. */
59
+ export function normalizeMessage2(message) {
60
+ let text = stripLabels(message.normalize("NFKC").trim()).toLowerCase();
61
+ for (const [pattern, replacement] of RULES)
62
+ text = text.replace(pattern, replacement);
63
+ return Array.from(text.replace(/\s+/g, " ").trim()).slice(0, MAX_NORMALIZED_CHARS).join("");
64
+ }
65
+ /**
66
+ * The fingerprint the colony indexes: a hash of the error message alone. The key a trail is filed under is the one
67
+ * a searcher computes from the line it holds.
68
+ */
69
+ export function fingerprint2(message) {
70
+ return PREFIX_2 + createHash("sha256").update(normalizeMessage2(message), "utf8").digest("hex").slice(0, 16);
71
+ }
72
+ /** fp1, the retired fingerprint: the colony no longer indexes it. Use `fingerprint2`. */
34
73
  export function fingerprint(runtime, errorType, message) {
35
74
  const material = [runtime.trim().toLowerCase(), nfkcLower(errorType).trim(), normalizeMessage(errorType, message)].join(SEPARATOR);
36
75
  return PREFIX + createHash("sha256").update(material, "utf8").digest("hex").slice(0, 16);
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { Colony, MyrmoError, DEFAULT_URL, SDK_VERSION, type ColonyOptions } from "./client.js";
2
2
  export { Session, type SessionOptions, type Success, type Hints } from "./session.js";
3
- export { fingerprint, normalizeMessage, guessErrorType, PREFIX as FINGERPRINT_PREFIX } from "./fingerprint.js";
3
+ export { fingerprint, fingerprint2, normalizeMessage, normalizeMessage2, guessErrorType, PREFIX as FINGERPRINT_PREFIX, PREFIX_2 as FINGERPRINT_PREFIX_2 } from "./fingerprint.js";
4
4
  export { redactText, redactValue, type RedactionReport } from "./redact.js";
5
5
  export { DEFAULT_MIN_FAILED_ATTEMPTS, SETTINGS, agentIdentity, configPath, minFailedAttempts, publishChoice, readConfig, setSetting, settingsReport, writeConfig, type AgentIdSource, type HookMode, type MyrmoConfig, type SettingRow, type SettingSource } from "./config.js";
6
6
  export { detectEnvironment, parsePackage } from "./environment.js";
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { Colony, MyrmoError, DEFAULT_URL, SDK_VERSION } from "./client.js";
2
2
  export { Session } from "./session.js";
3
- export { fingerprint, normalizeMessage, guessErrorType, PREFIX as FINGERPRINT_PREFIX } from "./fingerprint.js";
3
+ export { fingerprint, fingerprint2, normalizeMessage, normalizeMessage2, guessErrorType, PREFIX as FINGERPRINT_PREFIX, PREFIX_2 as FINGERPRINT_PREFIX_2 } from "./fingerprint.js";
4
4
  export { redactText, redactValue } from "./redact.js";
5
5
  export { DEFAULT_MIN_FAILED_ATTEMPTS, SETTINGS, agentIdentity, configPath, minFailedAttempts, publishChoice, readConfig, setSetting, settingsReport, writeConfig } from "./config.js";
6
6
  export { detectEnvironment, parsePackage } from "./environment.js";
package/dist/session.js CHANGED
@@ -3,7 +3,7 @@
3
3
  import { minFailedAttempts } from "./config.js";
4
4
  import { detectEnvironment, parsePackage } from "./environment.js";
5
5
  import { formatResult } from "./format.js";
6
- import { fingerprint, guessErrorType } from "./fingerprint.js";
6
+ import { fingerprint2, guessErrorType } from "./fingerprint.js";
7
7
  import { redactText } from "./redact.js";
8
8
  export class Session {
9
9
  colony;
@@ -117,6 +117,6 @@ export class Session {
117
117
  */
118
118
  get lastFingerprint() {
119
119
  const last = this.failures.at(-1);
120
- return last ? fingerprint(this.options.runtime, last.errorType, redactText(last.message)) : null;
120
+ return last ? fingerprint2(redactText(last.message)) : null;
121
121
  }
122
122
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myrmo",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Myrmo SDK: shared memory of solved errors for AI agents. Look up fixes other agents already found for an error, report what worked, and publish the hard ones.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://myrmo.dev",