Browse Source

compaction improvements

Dax Raad 5 months ago
parent
commit
4c94753eda

+ 7 - 3
packages/opencode/src/cli/cmd/tui.ts

@@ -16,6 +16,7 @@ import { Ide } from "../../ide"
 import { Flag } from "../../flag/flag"
 import { Session } from "../../session"
 import { Instance } from "../../project/instance"
+import { $ } from "bun"
 
 declare global {
   const OPENCODE_TUI_PATH: string
@@ -111,8 +112,7 @@ export const TuiCommand = cmd({
           hostname: args.hostname,
         })
 
-        let cmd = ["go", "run", "./main.go"]
-        let cwd = Bun.fileURLToPath(new URL("../../../../tui/cmd/opencode", import.meta.url))
+        let cmd = [] as string[]
         const tui = Bun.embeddedFiles.find((item) => (item as File).name.includes("tui")) as File
         if (tui) {
           let binaryName = tui.name
@@ -125,9 +125,13 @@ export const TuiCommand = cmd({
             await Bun.write(file, tui, { mode: 0o755 })
             await fs.chmod(binary, 0o755)
           }
-          cwd = process.cwd()
           cmd = [binary]
         }
+        if (!tui) {
+          const dir = Bun.fileURLToPath(new URL("../../../../tui/cmd/opencode", import.meta.url))
+          await $`go build -o ./dist/tui ./main.go`.cwd(dir)
+          cmd = [path.join(dir, "dist/tui")]
+        }
         Log.Default.info("tui", {
           cmd,
         })

+ 3 - 3
packages/opencode/src/id/id.ts

@@ -30,7 +30,7 @@ export namespace Identifier {
 
   function generateID(prefix: keyof typeof prefixes, descending: boolean, given?: string): string {
     if (!given) {
-      return generateNewID(prefix, descending)
+      return create(prefix, descending)
     }
 
     if (!given.startsWith(prefixes[prefix])) {
@@ -49,8 +49,8 @@ export namespace Identifier {
     return result
   }
 
-  function generateNewID(prefix: keyof typeof prefixes, descending: boolean): string {
-    const currentTimestamp = Date.now()
+  export function create(prefix: keyof typeof prefixes, descending: boolean, timestamp?: number): string {
+    const currentTimestamp = timestamp ?? Date.now()
 
     if (currentTimestamp !== lastTimestamp) {
       lastTimestamp = currentTimestamp

+ 94 - 60
packages/opencode/src/session/index.ts

@@ -86,6 +86,7 @@ export namespace Session {
       time: z.object({
         created: z.number(),
         updated: z.number(),
+        compacting: z.number().optional(),
       }),
       revert: z
         .object({
@@ -137,12 +138,17 @@ export namespace Session {
         error: MessageV2.Assistant.shape.error,
       }),
     ),
+    Compacted: Bus.event(
+      "session.compacted",
+      z.object({
+        sessionID: z.string(),
+      }),
+    ),
   }
 
   const state = Instance.state(
     () => {
       const pending = new Map<string, AbortController>()
-      const autoCompacting = new Map<string, boolean>()
       const queued = new Map<
         string,
         {
@@ -156,7 +162,6 @@ export namespace Session {
 
       return {
         pending,
-        autoCompacting,
         queued,
       }
     },
@@ -714,24 +719,8 @@ export namespace Session {
     })().then((x) => Provider.getModel(x.providerID, x.modelID))
     let msgs = await messages(input.sessionID)
 
-    const previous = msgs.filter((x) => x.info.role === "assistant").at(-1)?.info as MessageV2.Assistant
     const outputLimit = Math.min(model.info.limit.output, OUTPUT_TOKEN_MAX) || OUTPUT_TOKEN_MAX
 
-    // auto summarize if too long
-    if (previous && previous.tokens) {
-      const tokens =
-        previous.tokens.input + previous.tokens.cache.read + previous.tokens.cache.write + previous.tokens.output
-      if (model.info.limit.context && tokens > Math.max((model.info.limit.context - outputLimit) * 0.9, 0)) {
-        state().autoCompacting.set(input.sessionID, true)
-
-        await summarize({
-          sessionID: input.sessionID,
-          providerID: model.providerID,
-          modelID: model.info.id,
-        })
-        return prompt(input)
-      }
-    }
     using abort = lock(input.sessionID)
 
     const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true)
@@ -999,7 +988,38 @@ export namespace Session {
           error: e,
         })
       },
-      async prepareStep({ messages }) {
+      async prepareStep({ messages, steps }) {
+        // Auto compact if too long
+        const tokens = (() => {
+          if (steps.length) {
+            const previous = steps.at(-1)
+            if (previous) return getUsage(model.info, previous.usage, previous.providerMetadata).tokens
+          }
+          const msg = msgs.findLast((x) => x.info.role === "assistant")?.info as MessageV2.Assistant
+          if (msg && msg.tokens) {
+            return msg.tokens
+          }
+        })()
+        if (tokens) {
+          log.info("compact check", tokens)
+          const count = tokens.input + tokens.cache.read + tokens.cache.write + tokens.output
+          if (model.info.limit.context && count > Math.max((model.info.limit.context - outputLimit) * 0.9, 0)) {
+            log.info("compacting in prepareStep")
+            const summarized = await summarize({
+              sessionID: input.sessionID,
+              providerID: model.providerID,
+              modelID: model.info.id,
+            })
+            const msgs = await Session.messages(input.sessionID).then((x) =>
+              x.filter((x) => x.info.id >= summarized.id),
+            )
+            return {
+              messages: MessageV2.toModelMessage(msgs),
+            }
+          }
+        }
+
+        // Add queued messages to the stream
         const queue = (state().queued.get(input.sessionID) ?? []).filter((x) => !x.processed)
         if (queue.length) {
           for (const item of queue) {
@@ -1756,10 +1776,22 @@ export namespace Session {
   }
 
   export async function summarize(input: { sessionID: string; providerID: string; modelID: string }) {
-    using abort = lock(input.sessionID)
+    await update(input.sessionID, (draft) => {
+      draft.time.compacting = Date.now()
+    })
+    await using _ = defer(async () => {
+      await update(input.sessionID, (draft) => {
+        draft.time.compacting = undefined
+      })
+    })
     const msgs = await messages(input.sessionID)
-    const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true)
-    const filtered = msgs.filter((msg) => !lastSummary || msg.info.id >= lastSummary.info.id)
+    const start = Math.max(
+      0,
+      msgs.findLastIndex((msg) => msg.info.role === "assistant" && msg.info.summary === true),
+    )
+    const split = start + Math.floor((msgs.length - start) / 2)
+    log.info("summarizing", { start, split })
+    const toSummarize = msgs.slice(start, split)
     const model = await Provider.getModel(input.providerID, input.modelID)
     const system = [
       ...SystemPrompt.summarize(model.providerID),
@@ -1767,36 +1799,8 @@ export namespace Session {
       ...(await SystemPrompt.custom()),
     ]
 
-    const next: MessageV2.Info = {
-      id: Identifier.ascending("message"),
-      role: "assistant",
-      sessionID: input.sessionID,
-      system,
-      mode: "build",
-      path: {
-        cwd: Instance.directory,
-        root: Instance.worktree,
-      },
-      summary: true,
-      cost: 0,
-      modelID: input.modelID,
-      providerID: model.providerID,
-      tokens: {
-        input: 0,
-        output: 0,
-        reasoning: 0,
-        cache: { read: 0, write: 0 },
-      },
-      time: {
-        created: Date.now(),
-      },
-    }
-    await updateMessage(next)
-
-    const processor = createProcessor(next, model.info)
-    const stream = streamText({
+    const generated = await generateText({
       maxRetries: 10,
-      abortSignal: abort.signal,
       model: model.language,
       messages: [
         ...system.map(
@@ -1805,7 +1809,7 @@ export namespace Session {
             content: x,
           }),
         ),
-        ...MessageV2.toModelMessage(filtered),
+        ...MessageV2.toModelMessage(toSummarize),
         {
           role: "user",
           content: [
@@ -1817,9 +1821,45 @@ export namespace Session {
         },
       ],
     })
+    const usage = getUsage(model.info, generated.usage, generated.providerMetadata)
+    const msg: MessageV2.Info = {
+      id: Identifier.create("message", false, toSummarize.at(-1)!.info.time.created + 1),
+      role: "assistant",
+      sessionID: input.sessionID,
+      system,
+      mode: "build",
+      path: {
+        cwd: Instance.directory,
+        root: Instance.worktree,
+      },
+      summary: true,
+      cost: usage.cost,
+      tokens: usage.tokens,
+      modelID: input.modelID,
+      providerID: model.providerID,
+      time: {
+        created: Date.now(),
+        completed: Date.now(),
+      },
+    }
+    await updateMessage(msg)
+    await updatePart({
+      type: "text",
+      sessionID: input.sessionID,
+      messageID: msg.id,
+      id: Identifier.ascending("part"),
+      text: generated.text,
+      time: {
+        start: Date.now(),
+        end: Date.now(),
+      },
+    })
 
-    const result = await processor.process(stream)
-    return result
+    Bus.publish(Event.Compacted, {
+      sessionID: input.sessionID,
+    })
+
+    return msg
   }
 
   function isLocked(sessionID: string) {
@@ -1837,12 +1877,6 @@ export namespace Session {
         log.info("unlocking", { sessionID })
         state().pending.delete(sessionID)
 
-        const isAutoCompacting = state().autoCompacting.get(sessionID) ?? false
-        if (isAutoCompacting) {
-          state().autoCompacting.delete(sessionID)
-          return
-        }
-
         const session = await get(sessionID)
         if (session.parentID) return
 

+ 1 - 1
packages/sdk/go/.release-please-manifest.json

@@ -1,3 +1,3 @@
 {
-  ".": "0.8.0"
+  ".": "0.9.0"
 }

+ 2 - 2
packages/sdk/go/.stats.yml

@@ -1,4 +1,4 @@
 configured_endpoints: 43
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-97b61518d8666ea7cb310af04248e00bcf8dc9753ba3c7e84471df72b3232004.yml
-openapi_spec_hash: a3500531973ad999c350b87c21aa3ab8
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-46826ba8640557721614b0c9a3f1860681d825ca8d8b12869652fa25aacb0b4c.yml
+openapi_spec_hash: 33b8db6fde3021579b21325ce910197d
 config_hash: 026ef000d34bf2f930e7b41e77d2d3ff

+ 8 - 0
packages/sdk/go/CHANGELOG.md

@@ -1,5 +1,13 @@
 # Changelog
 
+## 0.9.0 (2025-09-10)
+
+Full Changelog: [v0.8.0...v0.9.0](https://github.com/sst/opencode-sdk-go/compare/v0.8.0...v0.9.0)
+
+### Features
+
+- **api:** api update ([2d3a28d](https://github.com/sst/opencode-sdk-go/commit/2d3a28df5657845aa4d73087e1737d1fc8c3ce1c))
+
 ## 0.8.0 (2025-09-01)
 
 Full Changelog: [v0.7.0...v0.8.0](https://github.com/sst/opencode-sdk-go/compare/v0.7.0...v0.8.0)

+ 1 - 1
packages/sdk/go/README.md

@@ -24,7 +24,7 @@ Or to pin the version:
 <!-- x-release-please-start-version -->
 
 ```sh
-go get -u 'github.com/sst/opencode-sdk-go@v0.8.0'
+go get -u 'github.com/sst/opencode-sdk-go@v0.9.0'
 ```
 
 <!-- x-release-please-end -->

+ 47 - 23
packages/sdk/go/app.go

@@ -50,33 +50,37 @@ func (r *AppService) Providers(ctx context.Context, query AppProvidersParams, op
 }
 
 type Model struct {
-	ID          string                 `json:"id,required"`
-	Attachment  bool                   `json:"attachment,required"`
-	Cost        ModelCost              `json:"cost,required"`
-	Limit       ModelLimit             `json:"limit,required"`
-	Name        string                 `json:"name,required"`
-	Options     map[string]interface{} `json:"options,required"`
-	Reasoning   bool                   `json:"reasoning,required"`
-	ReleaseDate string                 `json:"release_date,required"`
-	Temperature bool                   `json:"temperature,required"`
-	ToolCall    bool                   `json:"tool_call,required"`
-	JSON        modelJSON              `json:"-"`
+	ID           string                 `json:"id,required"`
+	Attachment   bool                   `json:"attachment,required"`
+	Cost         ModelCost              `json:"cost,required"`
+	Limit        ModelLimit             `json:"limit,required"`
+	Name         string                 `json:"name,required"`
+	Options      map[string]interface{} `json:"options,required"`
+	Reasoning    bool                   `json:"reasoning,required"`
+	ReleaseDate  string                 `json:"release_date,required"`
+	Temperature  bool                   `json:"temperature,required"`
+	ToolCall     bool                   `json:"tool_call,required"`
+	Experimental bool                   `json:"experimental"`
+	Provider     ModelProvider          `json:"provider"`
+	JSON         modelJSON              `json:"-"`
 }
 
 // modelJSON contains the JSON metadata for the struct [Model]
 type modelJSON struct {
-	ID          apijson.Field
-	Attachment  apijson.Field
-	Cost        apijson.Field
-	Limit       apijson.Field
-	Name        apijson.Field
-	Options     apijson.Field
-	Reasoning   apijson.Field
-	ReleaseDate apijson.Field
-	Temperature apijson.Field
-	ToolCall    apijson.Field
-	raw         string
-	ExtraFields map[string]apijson.Field
+	ID           apijson.Field
+	Attachment   apijson.Field
+	Cost         apijson.Field
+	Limit        apijson.Field
+	Name         apijson.Field
+	Options      apijson.Field
+	Reasoning    apijson.Field
+	ReleaseDate  apijson.Field
+	Temperature  apijson.Field
+	ToolCall     apijson.Field
+	Experimental apijson.Field
+	Provider     apijson.Field
+	raw          string
+	ExtraFields  map[string]apijson.Field
 }
 
 func (r *Model) UnmarshalJSON(data []byte) (err error) {
@@ -135,6 +139,26 @@ func (r modelLimitJSON) RawJSON() string {
 	return r.raw
 }
 
+type ModelProvider struct {
+	Npm  string            `json:"npm,required"`
+	JSON modelProviderJSON `json:"-"`
+}
+
+// modelProviderJSON contains the JSON metadata for the struct [ModelProvider]
+type modelProviderJSON struct {
+	Npm         apijson.Field
+	raw         string
+	ExtraFields map[string]apijson.Field
+}
+
+func (r *ModelProvider) UnmarshalJSON(data []byte) (err error) {
+	return apijson.UnmarshalRoot(data, r)
+}
+
+func (r modelProviderJSON) RawJSON() string {
+	return r.raw
+}
+
 type Provider struct {
 	ID     string           `json:"id,required"`
 	Env    []string         `json:"env,required"`

+ 48 - 23
packages/sdk/go/config.go

@@ -1562,34 +1562,38 @@ func (r configProviderJSON) RawJSON() string {
 }
 
 type ConfigProviderModel struct {
-	ID          string                    `json:"id"`
-	Attachment  bool                      `json:"attachment"`
-	Cost        ConfigProviderModelsCost  `json:"cost"`
-	Limit       ConfigProviderModelsLimit `json:"limit"`
-	Name        string                    `json:"name"`
-	Options     map[string]interface{}    `json:"options"`
-	Reasoning   bool                      `json:"reasoning"`
-	ReleaseDate string                    `json:"release_date"`
-	Temperature bool                      `json:"temperature"`
-	ToolCall    bool                      `json:"tool_call"`
-	JSON        configProviderModelJSON   `json:"-"`
+	ID           string                       `json:"id"`
+	Attachment   bool                         `json:"attachment"`
+	Cost         ConfigProviderModelsCost     `json:"cost"`
+	Experimental bool                         `json:"experimental"`
+	Limit        ConfigProviderModelsLimit    `json:"limit"`
+	Name         string                       `json:"name"`
+	Options      map[string]interface{}       `json:"options"`
+	Provider     ConfigProviderModelsProvider `json:"provider"`
+	Reasoning    bool                         `json:"reasoning"`
+	ReleaseDate  string                       `json:"release_date"`
+	Temperature  bool                         `json:"temperature"`
+	ToolCall     bool                         `json:"tool_call"`
+	JSON         configProviderModelJSON      `json:"-"`
 }
 
 // configProviderModelJSON contains the JSON metadata for the struct
 // [ConfigProviderModel]
 type configProviderModelJSON struct {
-	ID          apijson.Field
-	Attachment  apijson.Field
-	Cost        apijson.Field
-	Limit       apijson.Field
-	Name        apijson.Field
-	Options     apijson.Field
-	Reasoning   apijson.Field
-	ReleaseDate apijson.Field
-	Temperature apijson.Field
-	ToolCall    apijson.Field
-	raw         string
-	ExtraFields map[string]apijson.Field
+	ID           apijson.Field
+	Attachment   apijson.Field
+	Cost         apijson.Field
+	Experimental apijson.Field
+	Limit        apijson.Field
+	Name         apijson.Field
+	Options      apijson.Field
+	Provider     apijson.Field
+	Reasoning    apijson.Field
+	ReleaseDate  apijson.Field
+	Temperature  apijson.Field
+	ToolCall     apijson.Field
+	raw          string
+	ExtraFields  map[string]apijson.Field
 }
 
 func (r *ConfigProviderModel) UnmarshalJSON(data []byte) (err error) {
@@ -1650,6 +1654,27 @@ func (r configProviderModelsLimitJSON) RawJSON() string {
 	return r.raw
 }
 
+type ConfigProviderModelsProvider struct {
+	Npm  string                           `json:"npm,required"`
+	JSON configProviderModelsProviderJSON `json:"-"`
+}
+
+// configProviderModelsProviderJSON contains the JSON metadata for the struct
+// [ConfigProviderModelsProvider]
+type configProviderModelsProviderJSON struct {
+	Npm         apijson.Field
+	raw         string
+	ExtraFields map[string]apijson.Field
+}
+
+func (r *ConfigProviderModelsProvider) UnmarshalJSON(data []byte) (err error) {
+	return apijson.UnmarshalRoot(data, r)
+}
+
+func (r configProviderModelsProviderJSON) RawJSON() string {
+	return r.raw
+}
+
 type ConfigProviderOptions struct {
 	APIKey  string `json:"apiKey"`
 	BaseURL string `json:"baseURL"`

+ 72 - 3
packages/sdk/go/event.go

@@ -63,7 +63,8 @@ type EventListResponse struct {
 	// [EventListResponseEventSessionUpdatedProperties],
 	// [EventListResponseEventSessionDeletedProperties],
 	// [EventListResponseEventSessionIdleProperties],
-	// [EventListResponseEventSessionErrorProperties], [interface{}].
+	// [EventListResponseEventSessionErrorProperties],
+	// [EventListResponseEventSessionCompactedProperties], [interface{}].
 	Properties interface{}           `json:"properties,required"`
 	Type       EventListResponseType `json:"type,required"`
 	JSON       eventListResponseJSON `json:"-"`
@@ -105,6 +106,7 @@ func (r *EventListResponse) UnmarshalJSON(data []byte) (err error) {
 // [EventListResponseEventPermissionReplied], [EventListResponseEventFileEdited],
 // [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted],
 // [EventListResponseEventSessionIdle], [EventListResponseEventSessionError],
+// [EventListResponseEventSessionCompacted],
 // [EventListResponseEventServerConnected].
 func (r EventListResponse) AsUnion() EventListResponseUnion {
 	return r.union
@@ -118,7 +120,8 @@ func (r EventListResponse) AsUnion() EventListResponseUnion {
 // [EventListResponseEventPermissionUpdated],
 // [EventListResponseEventPermissionReplied], [EventListResponseEventFileEdited],
 // [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted],
-// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError] or
+// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError],
+// [EventListResponseEventSessionCompacted] or
 // [EventListResponseEventServerConnected].
 type EventListResponseUnion interface {
 	implementsEventListResponse()
@@ -193,6 +196,11 @@ func init() {
 			Type:               reflect.TypeOf(EventListResponseEventSessionError{}),
 			DiscriminatorValue: "session.error",
 		},
+		apijson.UnionVariant{
+			TypeFilter:         gjson.JSON,
+			Type:               reflect.TypeOf(EventListResponseEventSessionCompacted{}),
+			DiscriminatorValue: "session.compacted",
+		},
 		apijson.UnionVariant{
 			TypeFilter:         gjson.JSON,
 			Type:               reflect.TypeOf(EventListResponseEventServerConnected{}),
@@ -1108,6 +1116,66 @@ func (r EventListResponseEventSessionErrorType) IsKnown() bool {
 	return false
 }
 
+type EventListResponseEventSessionCompacted struct {
+	Properties EventListResponseEventSessionCompactedProperties `json:"properties,required"`
+	Type       EventListResponseEventSessionCompactedType       `json:"type,required"`
+	JSON       eventListResponseEventSessionCompactedJSON       `json:"-"`
+}
+
+// eventListResponseEventSessionCompactedJSON contains the JSON metadata for the
+// struct [EventListResponseEventSessionCompacted]
+type eventListResponseEventSessionCompactedJSON struct {
+	Properties  apijson.Field
+	Type        apijson.Field
+	raw         string
+	ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventSessionCompacted) UnmarshalJSON(data []byte) (err error) {
+	return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventSessionCompactedJSON) RawJSON() string {
+	return r.raw
+}
+
+func (r EventListResponseEventSessionCompacted) implementsEventListResponse() {}
+
+type EventListResponseEventSessionCompactedProperties struct {
+	SessionID string                                               `json:"sessionID,required"`
+	JSON      eventListResponseEventSessionCompactedPropertiesJSON `json:"-"`
+}
+
+// eventListResponseEventSessionCompactedPropertiesJSON contains the JSON metadata
+// for the struct [EventListResponseEventSessionCompactedProperties]
+type eventListResponseEventSessionCompactedPropertiesJSON struct {
+	SessionID   apijson.Field
+	raw         string
+	ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventSessionCompactedProperties) UnmarshalJSON(data []byte) (err error) {
+	return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventSessionCompactedPropertiesJSON) RawJSON() string {
+	return r.raw
+}
+
+type EventListResponseEventSessionCompactedType string
+
+const (
+	EventListResponseEventSessionCompactedTypeSessionCompacted EventListResponseEventSessionCompactedType = "session.compacted"
+)
+
+func (r EventListResponseEventSessionCompactedType) IsKnown() bool {
+	switch r {
+	case EventListResponseEventSessionCompactedTypeSessionCompacted:
+		return true
+	}
+	return false
+}
+
 type EventListResponseEventServerConnected struct {
 	Properties interface{}                               `json:"properties,required"`
 	Type       EventListResponseEventServerConnectedType `json:"type,required"`
@@ -1163,12 +1231,13 @@ const (
 	EventListResponseTypeSessionDeleted       EventListResponseType = "session.deleted"
 	EventListResponseTypeSessionIdle          EventListResponseType = "session.idle"
 	EventListResponseTypeSessionError         EventListResponseType = "session.error"
+	EventListResponseTypeSessionCompacted     EventListResponseType = "session.compacted"
 	EventListResponseTypeServerConnected      EventListResponseType = "server.connected"
 )
 
 func (r EventListResponseType) IsKnown() bool {
 	switch r {
-	case EventListResponseTypeInstallationUpdated, EventListResponseTypeLspClientDiagnostics, EventListResponseTypeMessageUpdated, EventListResponseTypeMessageRemoved, EventListResponseTypeMessagePartUpdated, EventListResponseTypeMessagePartRemoved, EventListResponseTypePermissionUpdated, EventListResponseTypePermissionReplied, EventListResponseTypeFileEdited, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionIdle, EventListResponseTypeSessionError, EventListResponseTypeServerConnected:
+	case EventListResponseTypeInstallationUpdated, EventListResponseTypeLspClientDiagnostics, EventListResponseTypeMessageUpdated, EventListResponseTypeMessageRemoved, EventListResponseTypeMessagePartUpdated, EventListResponseTypeMessagePartRemoved, EventListResponseTypePermissionUpdated, EventListResponseTypePermissionReplied, EventListResponseTypeFileEdited, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionIdle, EventListResponseTypeSessionError, EventListResponseTypeSessionCompacted, EventListResponseTypeServerConnected:
 		return true
 	}
 	return false

+ 69 - 20
packages/sdk/go/file.go

@@ -100,15 +100,17 @@ func (r FileStatus) IsKnown() bool {
 }
 
 type FileNode struct {
-	Ignored bool         `json:"ignored,required"`
-	Name    string       `json:"name,required"`
-	Path    string       `json:"path,required"`
-	Type    FileNodeType `json:"type,required"`
-	JSON    fileNodeJSON `json:"-"`
+	Absolute string       `json:"absolute,required"`
+	Ignored  bool         `json:"ignored,required"`
+	Name     string       `json:"name,required"`
+	Path     string       `json:"path,required"`
+	Type     FileNodeType `json:"type,required"`
+	JSON     fileNodeJSON `json:"-"`
 }
 
 // fileNodeJSON contains the JSON metadata for the struct [FileNode]
 type fileNodeJSON struct {
+	Absolute    apijson.Field
 	Ignored     apijson.Field
 	Name        apijson.Field
 	Path        apijson.Field
@@ -141,16 +143,18 @@ func (r FileNodeType) IsKnown() bool {
 }
 
 type FileReadResponse struct {
-	Content string               `json:"content,required"`
-	Type    FileReadResponseType `json:"type,required"`
-	JSON    fileReadResponseJSON `json:"-"`
+	Content string                `json:"content,required"`
+	Diff    string                `json:"diff"`
+	Patch   FileReadResponsePatch `json:"patch"`
+	JSON    fileReadResponseJSON  `json:"-"`
 }
 
 // fileReadResponseJSON contains the JSON metadata for the struct
 // [FileReadResponse]
 type fileReadResponseJSON struct {
 	Content     apijson.Field
-	Type        apijson.Field
+	Diff        apijson.Field
+	Patch       apijson.Field
 	raw         string
 	ExtraFields map[string]apijson.Field
 }
@@ -163,19 +167,64 @@ func (r fileReadResponseJSON) RawJSON() string {
 	return r.raw
 }
 
-type FileReadResponseType string
+type FileReadResponsePatch struct {
+	Hunks       []FileReadResponsePatchHunk `json:"hunks,required"`
+	NewFileName string                      `json:"newFileName,required"`
+	OldFileName string                      `json:"oldFileName,required"`
+	Index       string                      `json:"index"`
+	NewHeader   string                      `json:"newHeader"`
+	OldHeader   string                      `json:"oldHeader"`
+	JSON        fileReadResponsePatchJSON   `json:"-"`
+}
 
-const (
-	FileReadResponseTypeRaw   FileReadResponseType = "raw"
-	FileReadResponseTypePatch FileReadResponseType = "patch"
-)
+// fileReadResponsePatchJSON contains the JSON metadata for the struct
+// [FileReadResponsePatch]
+type fileReadResponsePatchJSON struct {
+	Hunks       apijson.Field
+	NewFileName apijson.Field
+	OldFileName apijson.Field
+	Index       apijson.Field
+	NewHeader   apijson.Field
+	OldHeader   apijson.Field
+	raw         string
+	ExtraFields map[string]apijson.Field
+}
 
-func (r FileReadResponseType) IsKnown() bool {
-	switch r {
-	case FileReadResponseTypeRaw, FileReadResponseTypePatch:
-		return true
-	}
-	return false
+func (r *FileReadResponsePatch) UnmarshalJSON(data []byte) (err error) {
+	return apijson.UnmarshalRoot(data, r)
+}
+
+func (r fileReadResponsePatchJSON) RawJSON() string {
+	return r.raw
+}
+
+type FileReadResponsePatchHunk struct {
+	Lines    []string                      `json:"lines,required"`
+	NewLines float64                       `json:"newLines,required"`
+	NewStart float64                       `json:"newStart,required"`
+	OldLines float64                       `json:"oldLines,required"`
+	OldStart float64                       `json:"oldStart,required"`
+	JSON     fileReadResponsePatchHunkJSON `json:"-"`
+}
+
+// fileReadResponsePatchHunkJSON contains the JSON metadata for the struct
+// [FileReadResponsePatchHunk]
+type fileReadResponsePatchHunkJSON struct {
+	Lines       apijson.Field
+	NewLines    apijson.Field
+	NewStart    apijson.Field
+	OldLines    apijson.Field
+	OldStart    apijson.Field
+	raw         string
+	ExtraFields map[string]apijson.Field
+}
+
+func (r *FileReadResponsePatchHunk) UnmarshalJSON(data []byte) (err error) {
+	return apijson.UnmarshalRoot(data, r)
+}
+
+func (r fileReadResponsePatchHunkJSON) RawJSON() string {
+	return r.raw
 }
 
 type FileListParams struct {

+ 1 - 1
packages/sdk/go/internal/version.go

@@ -2,4 +2,4 @@
 
 package internal
 
-const PackageVersion = "0.8.0" // x-release-please-version
+const PackageVersion = "0.9.0" // x-release-please-version

+ 5 - 3
packages/sdk/go/session.go

@@ -1332,15 +1332,17 @@ func (r sessionJSON) RawJSON() string {
 }
 
 type SessionTime struct {
-	Created float64         `json:"created,required"`
-	Updated float64         `json:"updated,required"`
-	JSON    sessionTimeJSON `json:"-"`
+	Created    float64         `json:"created,required"`
+	Updated    float64         `json:"updated,required"`
+	Compacting float64         `json:"compacting"`
+	JSON       sessionTimeJSON `json:"-"`
 }
 
 // sessionTimeJSON contains the JSON metadata for the struct [SessionTime]
 type sessionTimeJSON struct {
 	Created     apijson.Field
 	Updated     apijson.Field
+	Compacting  apijson.Field
 	raw         string
 	ExtraFields map[string]apijson.Field
 }

+ 3 - 0
packages/tui/internal/app/app.go

@@ -653,6 +653,9 @@ func getDefaultModel(
 }
 
 func (a *App) IsBusy() bool {
+	if a.Session.Time.Compacting > 0 {
+		return true
+	}
 	if len(a.Messages) == 0 {
 		return false
 	}

+ 3 - 0
packages/tui/internal/components/chat/editor.go

@@ -385,6 +385,9 @@ func (m *editorComponent) Content() string {
 	} else if m.app.IsBusy() {
 		keyText := m.getInterruptKeyText()
 		status := "working"
+		if m.app.Session.Time.Compacting > 0 {
+			status = "compacting"
+		}
 		if m.app.CurrentPermission.ID != "" {
 			status = "waiting for permission"
 		}

+ 6 - 0
packages/tui/internal/components/chat/messages.go

@@ -365,6 +365,9 @@ func (m *messagesComponent) renderView() tea.Cmd {
 		lastAssistantMessage := "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
 		for _, msg := range slices.Backward(m.app.Messages) {
 			if assistant, ok := msg.Info.(opencode.AssistantMessage); ok {
+				if assistant.Time.Completed > 0 {
+					break
+				}
 				lastAssistantMessage = assistant.ID
 				break
 			}
@@ -475,6 +478,9 @@ func (m *messagesComponent) renderView() tea.Cmd {
 				}
 
 			case opencode.AssistantMessage:
+				if casted.Summary {
+					continue
+				}
 				if casted.ID == m.app.Session.Revert.MessageID {
 					reverted = true
 					revertedMessageCount = 1

+ 36 - 2
packages/tui/internal/tui/tui.go

@@ -592,10 +592,40 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			}
 
 			if matchIndex == -1 {
-				a.app.Messages = append(a.app.Messages, app.Message{
+				// Extract the new message ID
+				var newMessageID string
+				switch casted := msg.Properties.Info.AsUnion().(type) {
+				case opencode.UserMessage:
+					newMessageID = casted.ID
+				case opencode.AssistantMessage:
+					newMessageID = casted.ID
+				}
+
+				// Find the correct insertion index by scanning backwards
+				// Most messages are added to the end, so start from the end
+				insertIndex := len(a.app.Messages)
+				for i := len(a.app.Messages) - 1; i >= 0; i-- {
+					var existingID string
+					switch casted := a.app.Messages[i].Info.(type) {
+					case opencode.UserMessage:
+						existingID = casted.ID
+					case opencode.AssistantMessage:
+						existingID = casted.ID
+					}
+					if existingID < newMessageID {
+						insertIndex = i + 1
+						break
+					}
+				}
+
+				// Create the new message
+				newMessage := app.Message{
 					Info:  msg.Properties.Info.AsUnion(),
 					Parts: []opencode.PartUnion{},
-				})
+				}
+
+				// Insert at the correct position
+				a.app.Messages = append(a.app.Messages[:insertIndex], append([]app.Message{newMessage}, a.app.Messages[insertIndex:]...)...)
 			}
 		}
 	case opencode.EventListResponseEventPermissionUpdated:
@@ -627,6 +657,10 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			slog.Error("Server error", "name", err.Name, "message", err.Data.Message)
 			return a, toast.NewErrorToast(err.Data.Message, toast.WithTitle(string(err.Name)))
 		}
+	case opencode.EventListResponseEventSessionCompacted:
+		if msg.Properties.SessionID == a.app.Session.ID {
+			return a, toast.NewSuccessToast("Session compacted successfully")
+		}
 	case tea.WindowSizeMsg:
 		msg.Height -= 2 // Make space for the status bar
 		a.width, a.height = msg.Width, msg.Height