diff --git a/packages/opencode/src/cli/cmd/debug/agent.handler.ts b/packages/opencode/src/cli/cmd/debug/agent.handler.ts index b9d9ff49c8e4..2706a868d9bc 100644 --- a/packages/opencode/src/cli/cmd/debug/agent.handler.ts +++ b/packages/opencode/src/cli/cmd/debug/agent.handler.ts @@ -8,6 +8,7 @@ import { Provider } from "@/provider/provider" import { Session } from "@/session/session" import type { MessageV2 } from "../../../session/message-v2" import { MessageID, PartID } from "../../../session/schema" +import { Identifier } from "@/id/id" import { ToolRegistry } from "@/tool/registry" import { Permission } from "../../../permission" import { iife } from "../../../util/iife" @@ -129,7 +130,6 @@ const createToolContext = Effect.fn("Cli.debug.agent.createToolContext")(functio ) { const sessionSvc = yield* Session.Service const session = yield* sessionSvc.create({ title: `Debug tool run (${agent.name})` }) - const messageID = MessageID.ascending() const model = agent.model ? agent.model : yield* Effect.gen(function* () { @@ -149,12 +149,13 @@ const createToolContext = Effect.fn("Cli.debug.agent.createToolContext")(functio }), ) }) - const now = Date.now() + const created = Date.now() + const messageID = MessageID.ascending(Identifier.create("msg", "ascending", created)) const message: SessionV1.Assistant = { id: messageID, sessionID: session.id, role: "assistant", - time: { created: now }, + time: { created }, parentID: messageID, modelID: model.modelID, providerID: model.providerID, diff --git a/packages/opencode/src/cli/cmd/github.handler.ts b/packages/opencode/src/cli/cmd/github.handler.ts index fcf44279ce7f..2266468e289b 100644 --- a/packages/opencode/src/cli/cmd/github.handler.ts +++ b/packages/opencode/src/cli/cmd/github.handler.ts @@ -22,7 +22,7 @@ import { InstanceRef } from "@/effect/instance-ref" import { SessionShare } from "@/share/session" import { Session } from "@/session/session" import type { SessionID } from "../../session/schema" -import { MessageID, PartID } from "../../session/schema" +import { PartID } from "../../session/schema" import { Provider } from "@/provider/provider" import { MessageV2 } from "../../session/message-v2" import { EventV2Bridge } from "@/event-v2-bridge" @@ -902,7 +902,6 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?: const prompt = sessionPrompt const result = yield* prompt.prompt({ sessionID: session.id, - messageID: MessageID.ascending(), variant, model: { providerID, @@ -950,7 +949,6 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?: console.log("Requesting summary from agent...") const summary = yield* prompt.prompt({ sessionID: session.id, - messageID: MessageID.ascending(), variant, model: { providerID, diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 75d6374bfa54..187310ffa3c0 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -3,6 +3,7 @@ import { SessionV1 } from "@opencode-ai/core/v1/session" import { ConfigV1 } from "@opencode-ai/core/v1/config/config" import { Session } from "./session" import { SessionID, MessageID, PartID } from "./schema" +import { Identifier } from "@/id/id" import { Provider } from "@/provider/provider" import { MessageV2 } from "./message-v2" import { Token } from "@/util/token" @@ -390,8 +391,9 @@ const layer = Layer.effect( .filter(Boolean) .join("\n\n") const ctx = yield* InstanceState.context + const created = Date.now() const msg: SessionV1.Assistant = { - id: MessageID.ascending(), + id: MessageID.ascending(Identifier.create("msg", "ascending", created)), role: "assistant", parentID: input.parentID, sessionID: input.sessionID, @@ -412,9 +414,7 @@ const layer = Layer.effect( }, modelID: model.id, providerID: model.providerID, - time: { - created: Date.now(), - }, + time: { created }, } yield* session.updateMessage(msg) const processor = yield* processors.create({ @@ -468,11 +468,12 @@ const layer = Layer.effect( if (result === "continue" && input.auto) { if (replay) { const original = replay.info + const created = Date.now() const replayMsg = yield* session.updateMessage({ - id: MessageID.ascending(), + id: MessageID.ascending(Identifier.create("msg", "ascending", created)), role: "user", sessionID: input.sessionID, - time: { created: Date.now() }, + time: { created }, agent: original.agent, model: original.model, format: original.format, @@ -516,11 +517,12 @@ const layer = Layer.effect( { enabled: true }, )).enabled ) { + const created = Date.now() const continueMsg = yield* session.updateMessage({ - id: MessageID.ascending(), + id: MessageID.ascending(Identifier.create("msg", "ascending", created)), role: "user", sessionID: input.sessionID, - time: { created: Date.now() }, + time: { created }, agent: userMessage.agent, model: userMessage.model, }) @@ -563,13 +565,14 @@ const layer = Layer.effect( auto: boolean overflow?: boolean }) { + const created = Date.now() const msg = yield* session.updateMessage({ - id: MessageID.ascending(), + id: MessageID.ascending(Identifier.create("msg", "ascending", created)), role: "user", model: input.model, sessionID: input.sessionID, agent: input.agent, - time: { created: Date.now() }, + time: { created }, }) yield* session.updatePart({ id: PartID.ascending(), diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 0f85d44f209b..5fd599e09f5a 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -4,6 +4,7 @@ import path from "path" import { SessionV1 } from "@opencode-ai/core/v1/session" import os from "os" import { SessionID, MessageID, PartID } from "./schema" +import { Identifier } from "@/id/id" import { MessageV2 } from "./message-v2" import { SessionRevert } from "./revert" import { Session } from "./session" @@ -265,8 +266,9 @@ const layer = Layer.effect( const promptOps = yield* ops() const { task: taskTool } = yield* registry.named() const taskModel = task.model ? yield* getModel(task.model.providerID, task.model.modelID, sessionID) : model + const created = Date.now() const assistantMessage: SessionV1.Assistant = yield* sessions.updateMessage({ - id: MessageID.ascending(), + id: MessageID.ascending(Identifier.create("msg", "ascending", created)), role: "assistant", parentID: lastUser.id, sessionID, @@ -278,7 +280,7 @@ const layer = Layer.effect( tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, modelID: taskModel.id, providerID: taskModel.providerID, - time: { created: Date.now() }, + time: { created }, }) let part: SessionV1.ToolPart = yield* sessions.updatePart({ id: PartID.ascending(), @@ -429,11 +431,12 @@ const layer = Layer.effect( if (!task.command) return + const summaryCreated = Date.now() const summaryUserMsg: SessionV1.User = { - id: MessageID.ascending(), + id: MessageID.ascending(Identifier.create("msg", "ascending", summaryCreated)), sessionID, role: "user", - time: { created: Date.now() }, + time: { created: summaryCreated }, agent: lastUser.agent, model: lastUser.model, } @@ -467,10 +470,11 @@ const layer = Layer.effect( throw error } const model = input.model ?? agent.model ?? (yield* currentModel(input.sessionID)) + const userCreated = Date.now() const userMsg: SessionV1.User = { - id: input.messageID ?? MessageID.ascending(), + id: input.messageID ?? MessageID.ascending(Identifier.create("msg", "ascending", userCreated)), sessionID: input.sessionID, - time: { created: Date.now() }, + time: { created: userCreated }, role: "user", agent: input.agent, model: { providerID: model.providerID, modelID: model.modelID }, @@ -486,15 +490,16 @@ const layer = Layer.effect( } yield* sessions.updatePart(userPart) + const assistantCreated = Date.now() const msg: SessionV1.Assistant = { - id: MessageID.ascending(), + id: MessageID.ascending(Identifier.create("msg", "ascending", assistantCreated)), sessionID: input.sessionID, parentID: userMsg.id, mode: input.agent, agent: input.agent, cost: 0, path: { cwd: ctx.directory, root: ctx.worktree }, - time: { created: Date.now() }, + time: { created: assistantCreated }, role: "assistant", tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, modelID: model.modelID, @@ -653,11 +658,12 @@ const layer = Layer.effect( : undefined const variant = input.variant ?? (ag.variant && full?.variants?.[ag.variant] ? ag.variant : undefined) + const created = Date.now() const info: SessionV1.User = { - id: input.messageID ?? MessageID.ascending(), + id: input.messageID ?? MessageID.ascending(Identifier.create("msg", "ascending", created)), role: "user", sessionID: input.sessionID, - time: { created: Date.now() }, + time: { created }, tools: input.tools, agent: ag.name, model: { @@ -1183,8 +1189,9 @@ const layer = Layer.effect( Effect.provideService(Session.Service, sessions), ) + const created = Date.now() const msg: SessionV1.Assistant = { - id: MessageID.ascending(), + id: MessageID.ascending(Identifier.create("msg", "ascending", created)), parentID: lastUser.id, role: "assistant", mode: agent.name, @@ -1195,7 +1202,7 @@ const layer = Layer.effect( tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, modelID: model.id, providerID: model.providerID, - time: { created: Date.now() }, + time: { created }, sessionID, } yield* sessions.updateMessage(msg) diff --git a/packages/opencode/src/tool/plan.ts b/packages/opencode/src/tool/plan.ts index 3b5ed978545a..397d213176e8 100644 --- a/packages/opencode/src/tool/plan.ts +++ b/packages/opencode/src/tool/plan.ts @@ -7,6 +7,7 @@ import { Session } from "@/session/session" import { MessageV2 } from "../session/message-v2" import { Provider } from "@/provider/provider" import { InstanceState } from "@/effect/instance-state" +import { Identifier } from "@/id/id" import { MessageID, PartID } from "../session/schema" import EXIT_DESCRIPTION from "./plan-exit.txt" @@ -50,11 +51,12 @@ export const PlanExitTool = Tool.define( const model = lastUser?.info.role === "user" && lastUser.info.model ? lastUser.info.model : yield* provider.defaultModel() + const created = Date.now() const msg: SessionV1.User = { - id: MessageID.ascending(), + id: MessageID.ascending(Identifier.create("msg", "ascending", created)), sessionID: ctx.sessionID, role: "user", - time: { created: Date.now() }, + time: { created }, agent: "build", model, } diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index d8ca640cfba9..83b7e0616122 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -4,7 +4,7 @@ import { ToolJsonSchema } from "./json-schema" import { SessionV1 } from "@opencode-ai/core/v1/session" import { BackgroundJob } from "@/background/job" import { Session } from "@/session/session" -import { SessionID, MessageID } from "../session/schema" +import { SessionID } from "../session/schema" import { MessageV2 } from "../session/message-v2" import { Agent } from "../agent/agent" import { deriveSubagentSessionPermission } from "../agent/subagent-permissions" @@ -200,7 +200,6 @@ export const TaskTool = Tool.define( const runTask = Effect.fn("TaskTool.runTask")(function* () { const parts = yield* ops.resolvePromptParts(params.prompt) const result = yield* ops.prompt({ - messageID: MessageID.ascending(), sessionID: nextSession.id, model: { modelID: model.modelID, diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index da6e0f8d036f..fbc405363d66 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -5,6 +5,7 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { SessionProjector } from "@opencode-ai/core/session/projector" import { eq } from "drizzle-orm" import { EventV2Bridge } from "@/event-v2-bridge" +import { Identifier } from "@/id/id" import { expect } from "bun:test" import { Cause, Deferred, Duration, Effect, Exit, Fiber, Layer } from "effect" import path from "path" @@ -442,6 +443,34 @@ const boot = Effect.fn("test.boot")(function* (input?: { title?: string }) { return { prompt, run, sessions, chat } }) +it.instance("prompt persists the timestamp embedded in its freshly minted message ID", () => + Effect.acquireUseRelease( + Effect.sync(() => { + const original = Date.now + let reads = 0 + Date.now = () => 1_000 + reads++ + return original + }), + () => + Effect.gen(function* () { + const { prompt, chat } = yield* boot() + const message = yield* prompt.prompt({ + sessionID: chat.id, + agent: "build", + model: ref, + noReply: true, + parts: [{ type: "text", text: "timestamp invariant" }], + }) + + expect(Identifier.timestamp(message.info.id)).toBe(message.info.time.created) + }), + (original) => + Effect.sync(() => { + Date.now = original + }), + ), +) + // Loop semantics noLLMServer.instance(