Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/kernel-agents/src/agent.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mergeDisjointRecords } from '@metamask/kernel-utils';
import { mergeDisjointRecords } from '@metamask/kernel-utils/merge-disjoint-records';
import type { Logger } from '@metamask/logger';
import type { LanguageModel } from '@ocap/kernel-language-model-service';

Expand Down
2 changes: 1 addition & 1 deletion packages/kernel-agents/src/attempt.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SampleGenerationError } from '@metamask/kernel-errors';
import { SampleGenerationError } from '@metamask/kernel-errors/bundleable';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Class identity mismatch breaks retry logic across packages

High Severity

The import of SampleGenerationError changed from @metamask/kernel-errors to @metamask/kernel-errors/bundleable. Since these export different class objects, the instanceof check in withRetries on line 47 will fail for errors thrown by packages that still import from the main export (like kernel-agents-repl). This breaks retry functionality for invalid LLM responses when using makeReplAgent, as errors won't be recognized as retryable and will be thrown immediately instead.

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@claude recommend a fix that works for this PR

Copy link

@claude claude bot Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude finished @grypez's task in 1m 6s —— View job


Recommended fix for class identity mismatch

I've analyzed the issue and here's my recommendation:

Problem Summary

The PR creates two separate SampleGenerationError classes:

  • Main export (@metamask/kernel-errors): Full-featured version with marshaling logic, extends BaseError, depends on @metamask/utils
  • Bundleable export (@metamask/kernel-errors/bundleable): Lightweight version without marshaling, extends Error directly, no @metamask/utils dependency

The issue occurs because:

  1. kernel-agents (used in vats) imports from /bundleable (line 1 of attempt.ts:47)
  2. kernel-agents-repl imports from the main export (line 1 of sample-collector.ts:39)
  3. When errors cross package boundaries, instanceof checks fail because they're different class objects

Recommended Solution: Use Error Code Checking Instead of instanceof

Since you've already extracted ErrorCode to error-codes.ts (which is already bundleable), the cleanest solution is to replace instanceof checks with error code checks:

In packages/kernel-agents/src/attempt.ts:

import { ErrorCode } from '@metamask/kernel-errors/error-codes';

// Change line 47 from:
(error) => error instanceof SampleGenerationError,

// To:
(error) => error instanceof Error && 
           'code' in error && 
           error.code === ErrorCode.SampleGenerationError,

Why this works:

  1. ✅ Both versions of SampleGenerationError have a .code property with the same value
  2. ErrorCode can be imported separately without pulling in unbundleable dependencies
  3. ✅ No class identity issues - checking a property value, not class reference
  4. ✅ Works across package boundaries and different module exports
  5. ✅ Maintains type safety with proper runtime checks
  6. ✅ Consistent with the ocap pattern of using structural typing

Alternative (if you need stricter typing):

Create a type guard function in error-codes.ts:

// In packages/kernel-errors/src/error-codes.ts
export const isSampleGenerationError = (error: unknown): error is {
  code: typeof ErrorCode.SampleGenerationError;
  data: { sample: string };
  message: string;
  cause?: Error;
} => {
  return (
    error instanceof Error &&
    'code' in error &&
    error.code === ErrorCode.SampleGenerationError &&
    'data' in error &&
    typeof error.data === 'object' &&
    error.data !== null &&
    'sample' in error.data
  );
};

Then use it in attempt.ts:

import { isSampleGenerationError } from '@metamask/kernel-errors/error-codes';

// Line 47:
(error) => isSampleGenerationError(error),

This approach gives you proper type narrowing and works with both versions of the class.


import type { Logger } from '@metamask/logger';
import type { LanguageModel } from '@ocap/kernel-language-model-service';

Expand Down
3 changes: 2 additions & 1 deletion packages/kernel-agents/src/capabilities/discover.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { E } from '@endo/eventual-send';
import type { DiscoverableExo, MethodSchema } from '@metamask/kernel-utils';
import type { DiscoverableExo } from '@metamask/kernel-utils/discoverable';
import type { MethodSchema } from '@metamask/kernel-utils/schema';

import type { CapabilityRecord, CapabilitySpec } from '../types.ts';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mergeDisjointRecords } from '@metamask/kernel-utils';
import { mergeDisjointRecords } from '@metamask/kernel-utils/merge-disjoint-records';
import type { Logger } from '@metamask/logger';

import { makeEvaluator } from './evaluator.ts';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SampleGenerationError } from '@metamask/kernel-errors';
import { SampleGenerationError } from '@metamask/kernel-errors/bundleable';
import type { Logger } from '@metamask/logger';

import type { SampleCollector } from '../../types.ts';
Expand Down
2 changes: 1 addition & 1 deletion packages/kernel-agents/src/task.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { makeCounter } from '@metamask/kernel-utils';
import { makeCounter } from '@metamask/kernel-utils/counter';

import type { Task, CapabilityRecord } from './types.ts';

Expand Down
2 changes: 1 addition & 1 deletion packages/kernel-agents/src/types/capability.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { JsonSchema } from '@metamask/kernel-utils';
import type { JsonSchema } from '@metamask/kernel-utils/schema';

export type Capability<Args extends Record<string, unknown>, Return = null> = (
args: Args,
Expand Down
11 changes: 10 additions & 1 deletion packages/kernel-errors/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@
"default": "./dist/index.cjs"
}
},
"./bundleable": {
"import": {
"types": "./dist/bundleable/index.d.mts",
"default": "./dist/bundleable/index.mjs"
},
"require": {
"types": "./dist/bundleable/index.d.cts",
"default": "./dist/bundleable/index.cjs"
}
},
"./package.json": "./package.json"
},
"main": "./dist/index.cjs",
Expand Down Expand Up @@ -56,7 +66,6 @@
"test:watch": "vitest --config vitest.config.ts"
},
"dependencies": {
"@libp2p/interface": "2.11.0",
"@metamask/superstruct": "^3.2.1",
"@metamask/utils": "^11.9.0"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/kernel-errors/src/BaseError.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Json } from '@metamask/utils';

import { ErrorCode } from './constants.ts';
import { ErrorCode } from './error-codes.ts';
import type {
MarshaledOcapError,
OcapError,
Expand Down
46 changes: 46 additions & 0 deletions packages/kernel-errors/src/bundleable/SampleGenerationError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { ErrorCode } from '../error-codes.ts';

/**
* Error options with optional stack trace.
*/
type ErrorOptionsWithStack = {
cause?: Error;
stack?: string;
};

/**
* Bundleable version of SampleGenerationError.
* This version does not include marshaling logic to avoid dependencies
* on @metamask/utils that prevent vat bundling.
*
* An error indicating that the LLM generated invalid response.
* This error should trigger resampling from the LLM.
*/
export class SampleGenerationError extends Error {
public readonly code: typeof ErrorCode.SampleGenerationError;

public readonly data: { sample: string };

/**
* Creates a new SampleGenerationError.
*
* @param sample - The invalid sample text generated by the LLM.
* @param cause - The underlying error that caused sample generation to fail.
* @param options - Additional error options including stack.
* @param options.stack - The stack trace of the error.
*/
constructor(sample: string, cause: Error, options?: ErrorOptionsWithStack) {
super('LLM generated invalid response.', { cause });

this.name = 'SampleGenerationError';
this.code = ErrorCode.SampleGenerationError;
this.data = { sample };

if (options?.stack) {
this.stack = options.stack;
}

harden(this);
}
}
harden(SampleGenerationError);
11 changes: 11 additions & 0 deletions packages/kernel-errors/src/bundleable/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Bundleable versions of kernel-errors.
*
* These versions do not include marshaling logic to avoid dependencies
* on @metamask/utils that prevent vat bundling with @endo/bundle-source.
*
* Use these exports when bundling vat code:
* import { SampleGenerationError } from '@metamask/kernel-errors/bundleable';
*/
export { SampleGenerationError } from './SampleGenerationError.ts';
export { ErrorCode, ErrorSentinel } from '../error-codes.ts';
27 changes: 4 additions & 23 deletions packages/kernel-errors/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ import {
import { JsonStruct, object } from '@metamask/utils';
import type { NonEmptyArray } from '@metamask/utils';

import { ErrorCode, ErrorSentinel } from './error-codes.ts';
import type { MarshaledError, MarshaledOcapError } from './types.ts';

// Re-export for backward compatibility
export { ErrorCode, ErrorSentinel } from './error-codes.ts';

/**
* Struct to validate plain {@link Error} objects.
*/
Expand All @@ -20,29 +24,6 @@ export const ErrorStruct = define<Error>(
(value) => value instanceof Error,
);

/**
* Enum defining all error codes for Ocap errors.
*/
export const ErrorCode = {
AbortError: 'ABORT_ERROR',
DuplicateEndowment: 'DUPLICATE_ENDOWMENT',
StreamReadError: 'STREAM_READ_ERROR',
VatAlreadyExists: 'VAT_ALREADY_EXISTS',
VatDeleted: 'VAT_DELETED',
VatNotFound: 'VAT_NOT_FOUND',
SubclusterNotFound: 'SUBCLUSTER_NOT_FOUND',
SampleGenerationError: 'SAMPLE_GENERATION_ERROR',
InternalError: 'INTERNAL_ERROR',
ResourceLimitError: 'RESOURCE_LIMIT_ERROR',
} as const;

export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];

/**
* A sentinel value used to identify marshaled errors.
*/
export const ErrorSentinel = '@@MARSHALED_ERROR';

const ErrorCodeStruct = union(
Object.values(ErrorCode).map((code) => literal(code)) as NonEmptyArray<
Struct<ErrorCode>
Expand Down
22 changes: 22 additions & 0 deletions packages/kernel-errors/src/error-codes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Enum defining all error codes for Ocap errors.
*/
export const ErrorCode = {
AbortError: 'ABORT_ERROR',
DuplicateEndowment: 'DUPLICATE_ENDOWMENT',
StreamReadError: 'STREAM_READ_ERROR',
VatAlreadyExists: 'VAT_ALREADY_EXISTS',
VatDeleted: 'VAT_DELETED',
VatNotFound: 'VAT_NOT_FOUND',
SubclusterNotFound: 'SUBCLUSTER_NOT_FOUND',
SampleGenerationError: 'SAMPLE_GENERATION_ERROR',
InternalError: 'INTERNAL_ERROR',
ResourceLimitError: 'RESOURCE_LIMIT_ERROR',
} as const;

export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];

/**
* A sentinel value used to identify marshaled errors.
*/
export const ErrorSentinel = '@@MARSHALED_ERROR';
1 change: 0 additions & 1 deletion packages/kernel-errors/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ describe('index', () => {
'isMarshaledError',
'isMarshaledOcapError',
'isOcapError',
'isRetryableNetworkError',
'marshalError',
'toError',
'unmarshalError',
Expand Down
1 change: 0 additions & 1 deletion packages/kernel-errors/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,3 @@ export { marshalError } from './marshal/marshalError.ts';
export { unmarshalError } from './marshal/unmarshalError.ts';
export { isMarshaledError } from './marshal/isMarshaledError.ts';
export { isMarshaledOcapError } from './marshal/isMarshaledOcapError.ts';
export { isRetryableNetworkError } from './utils/isRetryableNetworkError.ts';
30 changes: 30 additions & 0 deletions packages/kernel-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,36 @@
"default": "./dist/discoverable.cjs"
}
},
"./counter": {
"import": {
"types": "./dist/counter.d.mts",
"default": "./dist/counter.mjs"
},
"require": {
"types": "./dist/counter.d.cts",
"default": "./dist/counter.cjs"
}
},
"./merge-disjoint-records": {
"import": {
"types": "./dist/merge-disjoint-records.d.mts",
"default": "./dist/merge-disjoint-records.mjs"
},
"require": {
"types": "./dist/merge-disjoint-records.d.cts",
"default": "./dist/merge-disjoint-records.cjs"
}
},
"./schema": {
"import": {
"types": "./dist/schema.d.mts",
"default": "./dist/schema.mjs"
},
"require": {
"types": "./dist/schema.d.cts",
"default": "./dist/schema.cjs"
}
},
"./package.json": "./package.json"
},
"main": "./dist/index.cjs",
Expand Down
13 changes: 13 additions & 0 deletions packages/kernel-utils/src/counter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* A simple counter which increments and returns when called.
*
* @param start - One less than the first returned number.
* @returns A counter.
*/
export const makeCounter = (start: number = 0) => {
let counter: number = start;
return () => {
counter += 1;
return counter;
};
};
15 changes: 2 additions & 13 deletions packages/kernel-utils/src/misc.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,7 @@
import { AbortError } from '@metamask/kernel-errors';

/**
* A simple counter which increments and returns when called.
*
* @param start - One less than the first returned number.
* @returns A counter.
*/
export const makeCounter = (start: number = 0) => {
let counter: number = start;
return () => {
counter += 1;
return counter;
};
};
// Re-export makeCounter for backward compatibility
export { makeCounter } from './counter.ts';

/**
* Delay execution by the specified number of milliseconds.
Expand Down
3 changes: 2 additions & 1 deletion packages/ocap-kernel/src/remotes/ConnectionFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ import { ping } from '@libp2p/ping';
import { webRTC } from '@libp2p/webrtc';
import { webSockets } from '@libp2p/websockets';
import { webTransport } from '@libp2p/webtransport';
import { AbortError, isRetryableNetworkError } from '@metamask/kernel-errors';
import { AbortError } from '@metamask/kernel-errors';
import { fromHex, retryWithBackoff } from '@metamask/kernel-utils';
import { Logger } from '@metamask/logger';
import { multiaddr } from '@multiformats/multiaddr';
import { byteStream } from 'it-byte-stream';
import { createLibp2p } from 'libp2p';

import type { Channel, InboundConnectionHandler } from './types.ts';
import { isRetryableNetworkError } from '../utils/isRetryableNetworkError.ts';

/**
* Connection factory for libp2p network operations.
Expand Down
7 changes: 2 additions & 5 deletions packages/ocap-kernel/src/remotes/network.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
AbortError,
isRetryableNetworkError,
ResourceLimitError,
} from '@metamask/kernel-errors';
import { AbortError, ResourceLimitError } from '@metamask/kernel-errors';
import {
abortableDelay,
DEFAULT_MAX_RETRY_ATTEMPTS,
Expand All @@ -22,6 +18,7 @@ import type {
OnRemoteGiveUp,
RemoteCommsOptions,
} from './types.ts';
import { isRetryableNetworkError } from '../utils/isRetryableNetworkError.ts';

/** Default upper bound for queued outbound messages while reconnecting */
const DEFAULT_MAX_QUEUE = 200;
Expand Down
1 change: 0 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2337,7 +2337,6 @@ __metadata:
resolution: "@metamask/kernel-errors@workspace:packages/kernel-errors"
dependencies:
"@arethetypeswrong/cli": "npm:^0.17.4"
"@libp2p/interface": "npm:2.11.0"
"@metamask/auto-changelog": "npm:^5.3.0"
"@metamask/eslint-config": "npm:^15.0.0"
"@metamask/eslint-config-nodejs": "npm:^15.0.0"
Expand Down
Loading