diff --git a/content/mips/mip-3.md b/content/mips/mip-3.md index 8fa7e5d..f3a2192 100644 --- a/content/mips/mip-3.md +++ b/content/mips/mip-3.md @@ -1,6 +1,11 @@ # MIP-3: Linear Memory -**Status:** TODO(author) +**Proposal status:** Final + +**Network status:** Activated with the MONAD_NINE upgrade on Monad mainnet on March 19, 2026, 14:30 UTC, and on testnet on March 10, 2026, 14:30 UTC. + +**Verified:** October 4, 2026, against the [canonical MIP-3 specification](https://github.com/monad-crypto/MIPs/blob/b49e9034087924cf208266e6be9cb910457fcf9c/MIPs/MIP-3.md) and the [official v0.13.0 release notes](https://docs.monad.xyz/developer-essentials/changelog/releases#v0-13-0-monad_nine). + **Summary:** Replaces the EVM's quadratic memory-expansion cost with a linear model, so large memory regions become predictable to price. @@ -18,6 +23,11 @@ model, so large memory regions become predictable to price. > TODO(author). +## Sources + +- [Canonical MIP-3 specification (pinned revision)](https://github.com/monad-crypto/MIPs/blob/b49e9034087924cf208266e6be9cb910457fcf9c/MIPs/MIP-3.md): proposal status. +- [Monad v0.13.0 (MONAD_NINE) release notes](https://docs.monad.xyz/developer-essentials/changelog/releases#v0-13-0-monad_nine): MIP-3 is gated by the MONAD_NINE revision, activated at timestamp 1773153000 on testnet and 1773930600 on mainnet. + ## Discussion link https://mipland.org/mip-3 diff --git a/content/mips/mip-4.md b/content/mips/mip-4.md index 949ff25..769d30f 100644 --- a/content/mips/mip-4.md +++ b/content/mips/mip-4.md @@ -1,6 +1,11 @@ # MIP-4: Reserve Balance Introspection -**Status:** TODO(author) +**Proposal status:** Final + +**Network status:** Activated with the MONAD_NINE upgrade on Monad mainnet on March 19, 2026, 14:30 UTC, and on testnet on March 10, 2026, 14:30 UTC. + +**Verified:** October 4, 2026, against the [canonical MIP-4 specification](https://github.com/monad-crypto/MIPs/blob/b49e9034087924cf208266e6be9cb910457fcf9c/MIPs/MIP-4.md) and the [official v0.13.0 release notes](https://docs.monad.xyz/developer-essentials/changelog/releases#v0-13-0-monad_nine). + **Summary:** Lets the protocol detect reserve balance violations mid-execution rather than only at the end of a transaction. @@ -16,6 +21,11 @@ rather than only at the end of a transaction. > TODO(author). +## Sources + +- [Canonical MIP-4 specification (pinned revision)](https://github.com/monad-crypto/MIPs/blob/b49e9034087924cf208266e6be9cb910457fcf9c/MIPs/MIP-4.md): proposal status. +- [Monad v0.13.0 (MONAD_NINE) release notes](https://docs.monad.xyz/developer-essentials/changelog/releases#v0-13-0-monad_nine): MIP-4 is gated by the MONAD_NINE revision, activated at timestamp 1773153000 on testnet and 1773930600 on mainnet. + ## Discussion link https://mipland.org/mip-4 diff --git a/content/mips/mip-7.md b/content/mips/mip-7.md index f5774f8..75e5fb3 100644 --- a/content/mips/mip-7.md +++ b/content/mips/mip-7.md @@ -1,6 +1,11 @@ # MIP-7: Extension Opcodes -**Status:** TODO(author) +**Proposal status:** Draft + +**Network status:** No activation evidence in this bundle. The official release notes record no MIP-7 activation on any network. + +**Verified:** October 4, 2026, against the [canonical MIP-7 specification](https://github.com/monad-crypto/MIPs/blob/b49e9034087924cf208266e6be9cb910457fcf9c/MIPs/MIP-7.md) and the [official release notes](https://docs.monad.xyz/developer-essentials/changelog/releases). + **Summary:** Reserves a namespace in the opcode space so new opcodes can be added safely without colliding with future EIPs. @@ -16,6 +21,11 @@ added safely without colliding with future EIPs. > TODO(author). +## Sources + +- [Canonical MIP-7 specification (pinned revision)](https://github.com/monad-crypto/MIPs/blob/b49e9034087924cf208266e6be9cb910457fcf9c/MIPs/MIP-7.md): proposal status (Draft). +- [Monad release notes](https://docs.monad.xyz/developer-essentials/changelog/releases): no MIP-7 activation listed as of the verification date. + ## Discussion link https://mipland.org/mip-7 diff --git a/content/mips/mip-8.md b/content/mips/mip-8.md index 1d9e505..08faea9 100644 --- a/content/mips/mip-8.md +++ b/content/mips/mip-8.md @@ -1,7 +1,11 @@ # MIP-8: Page-ified Storage -**Status:** Final; active on Monad mainnet since September 2, 2026 at 14:30 UTC -as part of the MONAD_TEN network upgrade. +**Proposal status:** Final + +**Network status:** Activated with the MONAD_TEN upgrade on Monad mainnet on September 2, 2026, 14:30 UTC, and on testnet on August 12, 2026, 14:30 UTC. + +**Verified:** October 4, 2026, against the [canonical MIP-8 specification](https://github.com/monad-crypto/MIPs/blob/b49e9034087924cf208266e6be9cb910457fcf9c/MIPs/MIP-8.md) and the [official v0.16.1 release notes](https://docs.monad.xyz/developer-essentials/changelog/releases#v0-16-1). + **Summary:** Groups 128 consecutive EVM storage slots into 4 KB pages, making the page the unit of storage commitment, I/O, and cold-access accounting. @@ -32,14 +36,11 @@ state model and gas schedule with the hardware page that nodes actually load. layouts remain valid. Gas costs can change for contracts that access several slots in the same page, so software must not hardcode legacy per-slot costs. -## Activation - -- Monad testnet: August 12, 2026 at 14:30 UTC -- Monad mainnet: September 2, 2026 at 14:30 UTC - -## Canonical specification +## Sources -https://github.com/monad-crypto/MIPs/blob/main/MIPs/MIP-8.md +- [Canonical MIP-8 specification (pinned revision)](https://github.com/monad-crypto/MIPs/blob/b49e9034087924cf208266e6be9cb910457fcf9c/MIPs/MIP-8.md): proposal status. +- [Monad v0.16.1 release notes](https://docs.monad.xyz/developer-essentials/changelog/releases#v0-16-1): MONAD_TEN activates MIP-8 on mainnet at timestamp 1788359400. +- [Monad v0.16.0 release notes](https://docs.monad.xyz/developer-essentials/changelog/releases#v0-16-0): MONAD_TEN activates MIP-8 on testnet at timestamp 1786545000. ## Discussion link diff --git a/src/app/api/chat/route.test.ts b/src/app/api/chat/route.test.ts new file mode 100644 index 0000000..2d34836 --- /dev/null +++ b/src/app/api/chat/route.test.ts @@ -0,0 +1,108 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +type SentRequest = { messages: { role: string; content: string }[] }; +const sent = vi.hoisted(() => ({ requests: [] as SentRequest[] })); +const configMock = vi.hoisted(() => vi.fn()); +const knowledgeMock = vi.hoisted(() => vi.fn()); + +vi.mock("botid/server", () => ({ checkBotId: async () => ({ isBot: false }) })); +vi.mock("@/lib/ai/ratelimit", () => ({ + checkRateLimit: async () => ({ success: true, reset: 0 }), + getClientIp: () => "127.0.0.1", +})); +vi.mock("@/lib/ai/config", async (importOriginal) => ({ + ...(await importOriginal()), + getChatConfig: configMock, +})); +vi.mock("@/lib/ai/knowledge", async (importOriginal) => { + const real = await importOriginal(); + knowledgeMock.mockImplementation(real.getKnowledgeBundle); + return { getKnowledgeBundle: knowledgeMock }; +}); +// The provider is mocked; the request it would have received is captured. +vi.mock("@/lib/ai/silorail", async (importOriginal) => ({ + ...(await importOriginal()), + getSiloRailClient: () => ({ + request: async (body: SentRequest) => { + sent.requests.push(body); + return { response: new Response("upstream unavailable in tests", { status: 503 }) }; + }, + }), +})); + +import { DEFAULT_CONFIG } from "@/lib/ai/config"; +import { EVIDENCE_RULE } from "@/lib/ai/instructions"; +import { POST } from "./route"; + +async function systemPromptFor(question: string): Promise { + sent.requests = []; + await POST( + new Request("http://localhost/api/chat", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + messages: [{ id: "1", role: "user", parts: [{ type: "text", text: question }] }], + }), + }), + ); + const request = sent.requests.at(-1); + if (!request) throw new Error("no request reached the provider"); + const system = request.messages.find((message) => message.role === "system"); + if (!system) throw new Error("no system message in the outgoing request"); + return system.content; +} + +beforeEach(() => { + vi.useFakeTimers({ toFake: ["Date"] }); + vi.setSystemTime(new Date("2026-10-05T03:00:00Z")); + configMock.mockResolvedValue(DEFAULT_CONFIG); +}); + +afterEach(() => { + vi.useRealTimers(); +}); + +describe("chat request evidence", () => { + it("sends a Final MIP's separately sourced activation and the dated rule", async () => { + const system = await systemPromptFor("Is MIP-8 live on mainnet?"); + + expect(system).toContain("**Proposal status:** Final"); + expect(system).toContain("Activated with the MONAD_TEN upgrade on Monad mainnet"); + expect(system).toContain("https://docs.monad.xyz/developer-essentials/changelog/releases#v0-16-1"); + expect(system).toContain( + "https://github.com/monad-crypto/MIPs/blob/b49e9034087924cf208266e6be9cb910457fcf9c/MIPs/MIP-8.md", + ); + expect(system).toContain("**Verified:** October 4, 2026"); + expect(system).toContain(EVIDENCE_RULE); + // Today's date travels separately from the content verification date. + expect(system).toContain("Current date (UTC): 2026-10-05"); + }); + + it("keeps the rule for a Final MIP with no activation evidence", async () => { + knowledgeMock.mockResolvedValueOnce( + [ + "# MIP-99: Example", + "**Proposal status:** Final", + "**Network status:** No activation evidence in this bundle.", + "**Verified:** October 1, 2026, against the [canonical MIP-99 specification](https://example.org/MIP-99.md).", + ].join("\n\n"), + ); + const system = await systemPromptFor("Is MIP-99 active?"); + + expect(system).toContain("No activation evidence in this bundle."); + expect(system).toContain("https://example.org/MIP-99.md"); + expect(system).toContain("A Final specification does not by itself mean the change is live"); + expect(system).toContain("say the bundle cannot confirm activation and link the canonical specification"); + expect(system).toContain("Current date (UTC): 2026-10-05"); + }); + + it("keeps the rule after a custom admin prompt", async () => { + configMock.mockResolvedValueOnce({ ...DEFAULT_CONFIG, systemPrompt: "Custom admin prompt." }); + const system = await systemPromptFor("What is MIP-12?"); + + expect(system.startsWith("Custom admin prompt.")).toBe(true); + expect(system).not.toContain("You are the MIP assistant for Monad."); + expect(system.indexOf(EVIDENCE_RULE)).toBeGreaterThan(system.indexOf("Custom admin prompt.")); + expect(system).toContain("Current date (UTC): 2026-10-05"); + }); +}); diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 0050328..5228e88 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -9,6 +9,7 @@ import { checkBotId } from "botid/server"; import { z } from "zod"; import { getChatConfig } from "@/lib/ai/config"; +import { buildInstructions } from "@/lib/ai/instructions"; import { getKnowledgeBundle } from "@/lib/ai/knowledge"; import { checkRateLimit, getClientIp } from "@/lib/ai/ratelimit"; import { @@ -77,18 +78,9 @@ export async function POST(req: Request) { getKnowledgeBundle(), ]); - const allowedTopicsLine = - config.allowedTopics.length > 0 - ? `Allowed topics: ${config.allowedTopics.join("; ")}.` - : ""; - - const instructions = [ - config.systemPrompt, - allowedTopicsLine, - `Refusal text (use verbatim when declining): "${config.refusalText}"`, - ] - .filter(Boolean) - .join("\n\n"); + // The evidence rule and today's date are appended after the configured + // prompt here, so an admin prompt from Edge Config cannot drop them. + const instructions = buildInstructions(config, new Date()); const modelMessages = await convertToModelMessages(parsed.messages); diff --git a/src/lib/ai/instructions.ts b/src/lib/ai/instructions.ts new file mode 100644 index 0000000..4bc1ef9 --- /dev/null +++ b/src/lib/ai/instructions.ts @@ -0,0 +1,43 @@ +import type { ChatConfig } from "@/lib/ai/config"; + +/** + * Set by the server and appended after whatever system prompt is configured, + * so a custom prompt from Edge Config cannot drop it. + */ +export const EVIDENCE_RULE = + "Status and activation rule (set by the server; it applies even if earlier " + + "instructions say otherwise): a MIP's proposal status and its network " + + "activation are separate facts. A Final specification does not by itself " + + "mean the change is live on mainnet or testnet. Say a MIP is activated only " + + "when the knowledge bundle gives separately sourced activation evidence, and " + + "name the network and the date. When activation evidence is missing or " + + "conflicting, say the bundle cannot confirm activation and link the " + + "canonical specification. When you state a status or activation fact, link " + + "the source the bundle gives for it and qualify it with that file's " + + 'verification date, for example "as verified on October 4, 2026".'; + +/** Today's date, kept apart from the dates the bundle's facts were checked. */ +export function currentDateLine(now: Date): string { + return ( + `Current date (UTC): ${now.toISOString().slice(0, 10)}. This is today's ` + + "date, not a verification date; each MIP file states when its facts were " + + 'verified on its "Verified" line.' + ); +} + +export function buildInstructions(config: ChatConfig, now: Date): string { + const allowedTopicsLine = + config.allowedTopics.length > 0 + ? `Allowed topics: ${config.allowedTopics.join("; ")}.` + : ""; + + return [ + config.systemPrompt, + allowedTopicsLine, + `Refusal text (use verbatim when declining): "${config.refusalText}"`, + EVIDENCE_RULE, + currentDateLine(now), + ] + .filter(Boolean) + .join("\n\n"); +} diff --git a/src/lib/ai/knowledge-sources.test.ts b/src/lib/ai/knowledge-sources.test.ts new file mode 100644 index 0000000..132e708 --- /dev/null +++ b/src/lib/ai/knowledge-sources.test.ts @@ -0,0 +1,23 @@ +import { readdir, readFile } from "node:fs/promises"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +const CONTENT_DIR = join(process.cwd(), "content", "mips"); + +describe("MIP knowledge sources", () => { + it("gives every MIP a proposal status, a network status, a verified date and sources", async () => { + const files = (await readdir(CONTENT_DIR)).filter((name) => /^mip-\d+\.md$/.test(name)); + expect(files.length).toBeGreaterThan(0); + + for (const name of files) { + const body = await readFile(join(CONTENT_DIR, name), "utf8"); + expect(body, name).toMatch(/^\*\*Proposal status:\*\* (Draft|Review|Last Call|Final|Withdrawn)$/m); + expect(body, name).toMatch(/^\*\*Network status:\*\* \S/m); + expect(body, name).toMatch(/^\*\*Verified:\*\* [A-Z][a-z]+ \d{1,2}, \d{4}, against /m); + expect(body, name).toMatch(/^## Sources$/m); + // The canonical spec link is pinned to a commit, not a moving branch. + expect(body, name).toMatch(/github\.com\/monad-crypto\/MIPs\/blob\/[0-9a-f]{40}\/MIPs\/MIP-\d+\.md/); + expect(body, name).not.toContain("TODO(author)\n**Summary"); + } + }); +});