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
5 changes: 5 additions & 0 deletions .changeset/js-output-backend-timestamp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@e2b/code-interpreter': patch
---

Fix `OutputMessage.timestamp` in the JS Code Interpreter to use the backend timestamp (Unix epoch in nanoseconds) sent with each stdout/stderr message, instead of the client clock (`new Date().getTime() * 1000`). The type is unchanged (`number`); this aligns the value with the documented contract and the Python SDK.
4 changes: 2 additions & 2 deletions packages/code-interpreter-js/src/messaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ export async function parseOutput(
await onStdout({
error: false,
line: msg.text,
timestamp: new Date().getTime() * 1000,
timestamp: msg.timestamp,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 String timestamps break the numeric callback contract

When the backend sends a string timestamp, parseOutput delivers a string despite OutputMessage.timestamp being typed as a number. Both output callbacks expose the wrong type to callers.

Learn more

The exported parseOutput accepts JSON lines from the kernel. Nanosecond values exceed JavaScript's safe integer range, so a JSON producer can encode them as decimal strings; the previously supported conversion handled those strings. The callback now receives the raw string even though OutputMessage declares a number. The stderr callback has the same behavior.

Example: Given {"type":"stdout","text":"hello","timestamp":"1700000000000999999"}, the callback receives a string rather than a number.

Recommended fix: Decide on a representation that preserves the backend value and has an honest public type. Normalize both stdout and stderr through one converter; if strings must remain supported, test their runtime type and precision.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Nanosecond output times lose precision

For ordinary numeric nanosecond timestamps, parseOutput exposes a rounded value because JSON.parse uses imprecise JavaScript numbers. Callers cannot recover the kernel's exact output time.

Learn more

The kernel's output timestamp is in Unix-epoch nanoseconds. At today's epoch, it is around 1.7e18, well above Number.MAX_SAFE_INTEGER. parseOutput parses the line with JSON.parse, which rounds numeric JSON values before they reach the callback. Exposing that rounded number as a nanosecond timestamp cannot preserve the underlying instant at nanosecond resolution; stderr follows the same path.

Example: A numeric JSON timestamp of 1700000000000000001 parses to the same JavaScript number as 1700000000000000000, so two different kernel timestamps become indistinguishable.

Recommended fix: Establish whether the kernel can transmit nanoseconds as decimal strings, then expose them as bigint or string with an accurate public type. Alternatively document and test an intentional lower-resolution representation, such as epoch milliseconds, instead of claiming nanosecond precision.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

})
}
break
Expand All @@ -362,7 +362,7 @@ export async function parseOutput(
await onStderr({
error: true,
line: msg.text,
timestamp: new Date().getTime() * 1000,
timestamp: msg.timestamp,
})
}
break
Expand Down
50 changes: 49 additions & 1 deletion packages/code-interpreter-js/tests/messaging.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { expect, test } from 'vitest'

import { SandboxError } from '../src'
import { extractError } from '../src/messaging'
import {
Execution,
OutputMessage,
extractError,
parseOutput,
} from '../src/messaging'

test('includes the response body in server errors', async () => {
const error = await extractError(
Expand All @@ -25,3 +30,46 @@ test('falls back to the status text for an empty body', async () => {

expect(error).toEqual(new SandboxError('500 Internal Server Error'))
})

test('stdout output carries the backend timestamp, not the client clock', async () => {
const backendTimestamp = 1700000000000000000
const execution = new Execution()
let received: OutputMessage | undefined

await parseOutput(
execution,
JSON.stringify({
type: 'stdout',
text: 'hello',
timestamp: backendTimestamp,
}),
(msg) => {
received = msg
}
)

expect(received?.timestamp).toBe(backendTimestamp)
expect(received?.error).toBe(false)
})

test('stderr output carries the backend timestamp, not the client clock', async () => {
const backendTimestamp = 1700000000000000000
const execution = new Execution()
let received: OutputMessage | undefined

await parseOutput(
execution,
JSON.stringify({
type: 'stderr',
text: 'boom',
timestamp: backendTimestamp,
}),
undefined,
(msg) => {
received = msg
}
)

expect(received?.timestamp).toBe(backendTimestamp)
expect(received?.error).toBe(true)
})