Guest User

Untitled

a guest
Aug 22nd, 2026
258
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 4.96 KB | None | 0 0
  1. import type { Plugin } from "@opencode-ai/plugin"
  2.  
  3. const PROVIDER_ID = "opencode-go"
  4. const MODEL_ID = "deepseek-v4-flash"
  5. const MAX_ATTEMPTS = 3
  6. const RETRY_DELAY_MS = 3000
  7. const STATE_TTL_MS = 10 * 60 * 1000
  8.  
  9. type RetryState = {
  10. attempts: number
  11. scheduled: boolean
  12. lastActivity: number
  13. }
  14.  
  15. // Keyed by `${sessionID}:${userMessageText}`: a re-send creates a new user
  16. // message, so the text is the stable identity across retry generations.
  17. const retryState = new Map<string, RetryState>()
  18.  
  19. const pruneStale = () => {
  20. const now = Date.now()
  21. for (const [key, state] of retryState) {
  22. if (now - state.lastActivity > STATE_TTL_MS && !state.scheduled) {
  23. retryState.delete(key)
  24. }
  25. }
  26. }
  27.  
  28. export const RetryGoFlash: Plugin = (async ({ client }) => {
  29. const fetchMessage = async (sessionID: string, messageID: string) => {
  30. const res = await client.session.message({
  31. path: { id: sessionID, messageID },
  32. })
  33. if (res.error || !res.data) {
  34. throw new Error(res.error ? String(res.error) : "empty message response")
  35. }
  36. return res.data
  37. }
  38.  
  39. const getTextParts = async (sessionID: string, userMsgID: string) => {
  40. const { parts } = await fetchMessage(sessionID, userMsgID)
  41. return parts.filter((p) => p.type === "text")
  42. }
  43.  
  44. const notifyFailure = async (sessionID: string) => {
  45. try {
  46. await client.tui.showToast({
  47. body: {
  48. title: "Retry failed",
  49. message: `3 retries failed after stream drop (session ${sessionID}). Resume manually.`,
  50. variant: "error",
  51. duration: 10000,
  52. },
  53. })
  54. } catch (error) {
  55. console.error(`[retry-go-flash] toast failed for ${sessionID}:`, error)
  56. }
  57. }
  58.  
  59. // Schedule one resend; on drop it re-enters via the event hook (same text
  60. // key, incremented attempts). If the resend itself errors out, retry it.
  61. const scheduleResend = (
  62. sessionID: string,
  63. userText: string,
  64. attempts: number,
  65. key: string,
  66. state: RetryState,
  67. ) => {
  68. if (attempts > MAX_ATTEMPTS) {
  69. retryState.delete(key)
  70. void notifyFailure(sessionID)
  71. return
  72. }
  73. state.scheduled = true
  74. state.lastActivity = Date.now()
  75. setTimeout(async () => {
  76. try {
  77. console.error(`[retry-go-flash] attempt ${attempts}/${MAX_ATTEMPTS} resending in ${sessionID}`)
  78. const res = await client.session.promptAsync({
  79. path: { id: sessionID },
  80. body: {
  81. parts: [{ type: "text", text: userText }],
  82. },
  83. })
  84. if (res.error) {
  85. throw new Error(String(res.error))
  86. }
  87. state.scheduled = false
  88. state.lastActivity = Date.now()
  89. } catch (error) {
  90. console.error(`[retry-go-flash] resend ${attempts} failed for ${sessionID}:`, error)
  91. scheduleResend(sessionID, userText, attempts + 1, key, state)
  92. }
  93. }, RETRY_DELAY_MS)
  94. }
  95.  
  96. return {
  97. async event({ event }) {
  98. if (event.type !== "message.part.updated") return
  99. const part = event.properties.part
  100. if (part.type !== "step-finish" || part.reason !== "unknown") return
  101. const sessionID = part.sessionID
  102. const messageID = part.messageID
  103.  
  104. pruneStale()
  105.  
  106. // Confirm this is the flash model in build mode, not a plan/compaction abort.
  107. let info: Awaited<ReturnType<typeof fetchMessage>>["info"] | undefined
  108. try {
  109. const msg = await fetchMessage(sessionID, messageID)
  110. info = msg.info
  111. } catch (error) {
  112. console.error(`[retry-go-flash] cannot fetch message ${sessionID}/${messageID}:`, error)
  113. return
  114. }
  115. if (!info || info.role !== "assistant") return
  116. if (info.providerID !== PROVIDER_ID || !info.modelID.startsWith(MODEL_ID)) return
  117. if (info.summary) return
  118. if (info.finish !== "unknown") return
  119. // Real drops produce tokens; zero-token "unknown" finishes are
  120. // intentional aborts (permission interrupts, compaction) and must not
  121. // be retried.
  122. if (!(info.tokens?.output > 0)) return
  123.  
  124. const userMsgID = info.parentID
  125. if (!userMsgID) return
  126.  
  127. let textParts: Awaited<ReturnType<typeof getTextParts>>
  128. try {
  129. textParts = await getTextParts(sessionID, userMsgID)
  130. } catch (error) {
  131. console.error(`[retry-go-flash] cannot fetch user parts for ${sessionID}/${userMsgID}:`, error)
  132. return
  133. }
  134. if (textParts.length === 0) return
  135. const userText = (textParts[0] as { text: string }).text
  136.  
  137. const key = `${sessionID}:${userText}`
  138. const existing = retryState.get(key)
  139. if (existing?.scheduled) return
  140.  
  141. const attempts = (existing?.attempts ?? 0) + 1
  142. const state: RetryState = existing ?? { attempts: 0, scheduled: false, lastActivity: Date.now() }
  143. state.attempts = attempts
  144. retryState.set(key, state)
  145. scheduleResend(sessionID, userText, attempts, key, state)
  146. },
  147. }
  148. }) satisfies Plugin
  149.  
Advertisement
Add Comment
Please, Sign In to add comment