SLOPSHOPPER

ctx-bytes-guard

Compacta sozinho ao fim do turno quando o payload da conversa passa de 16 MB, inclusive em sessões headless (-p / SDK). Com o synapse-rate-limit instalado, os…

newtoastprompt
A shopper browsing a rack in a slop shop
README

claude-code-mods

Português · English

A marketplace of Claude Code mods.

Install

/plugin marketplace add fabioivan/claude-code-mods
/plugin install synapse-rate-limit@fabioivan-mods
/plugin install ctx-bytes-guard@fabioivan-mods
/reload-plugins

Plugins

PluginDescription
synapse-rate-limitA full HUD (based on the hud mod and claude-hud) with alerts, a limit forecast, a budget, a detail pane and 12 themes, plus the synapse line: weather, context/attachments size in MB, tokens and a chart of the last turns.
ctx-bytes-guardAuto-compacts when the conversation payload passes 16 MB (maxMb), checked after each turn (also in headless -p / SDK sessions); optionally also at a context percent (maxPercent, off by default). With synapse-rate-limit installed, the context line shows the size and context against those limits.

Disable the original hud plugin when using synapse-rate-limit, so the bar does not show twice.

License

MIT (see LICENSE).

synapse-rate-limit includes code from claude-hud, under MIT (see synapse-rate-limit/LICENSE.claude-hud).

Source 2 files
hooks/register.ts 88 lines
1// Compacta sozinho quando o contexto passa de MAX_PERCENT ou o payload de MAX_BYTES.
2// Em sessão headless (-p / SDK) compacta com o comando /compact ao fim do turno.
3// Os limites vão para o atom `limits`, que o synapse-rate-limit desenha ao lado dos MB.
4import { atom, update } from 'claude-code'
5import type { EngineInterface, Register } from 'claude-code'
6
7import type { GuardLimits } from '../types'
8
9const DEFAULT_MB = 16
10const DEFAULT_PERCENT = 100
11
12const limits = atom({ plugin: 'ctx-bytes-guard', key: 'limits' } as const, null as GuardLimits | null)
13
14interface Guard {
15  maxBytes: number
16  maxPercent: number
17  // Sessões headless (-p / SDK) recusam `$.session.compact`: lá a compactação só roda como o comando
18  // /compact, e só depois do turno. Vira true na primeira recusa.
19  headless: boolean
20}
21
22const HEADLESS_RE = /headless/i
23const DEFER_RE = /later event/i
24
25// Declarada no topo do módulo: o motor só aceita `$` repassado a função assim.
26// Devolve o motivo quando a compactação não aconteceu, para o hook avisar.
27async function check($: EngineInterface, guard: Guard, when: 'submit' | 'turn'): Promise<string | null> {
28  await update($, limits, () => ({ maxBytes: guard.maxBytes, maxPercent: guard.maxPercent }))
29
30  const msgs = await $.session.messages({ as: 'api' })
31  // ponytail: JSON.stringify materializes the payload; fine at ~16MB, stream-count if it hurts
32  const bytes = Array.isArray(msgs) ? JSON.stringify(msgs).length : 0
33  const { context } = await $.session.usage()
34  const percent = context.percent ?? 0
35
36  if (bytes < guard.maxBytes && percent < guard.maxPercent) return null
37
38  if (!guard.headless) {
39    $.ui.toast(`auto-compact: ${percent}% / ${(bytes / 1048576).toFixed(1)}MB`)
40    try {
41      const result = await $.session.compact()
42      return result && 'skip' in result && result.skip ? `compactação vetada: ${result.skip}` : null
43    } catch (err) {
44      const msg = String(err)
45      // O motor recusa compactar de dentro do prompt.submit (o turno está preso): fica para o turn.complete.
46      if (when === 'submit' && DEFER_RE.test(msg)) return null
47      if (!HEADLESS_RE.test(msg)) throw err
48      guard.headless = true
49    }
50  }
51
52  // Headless: antes da mensagem o turno está preso e /compact esperaria por ele; depois do turno roda.
53  if (when === 'submit') return null
54  await $.command.run({ command: 'compact' })
55  return null
56}
57
58async function run($: EngineInterface, guard: Guard, when: 'submit' | 'turn'): Promise<void> {
59  try {
60    const why = await check($, guard, when)
61    if (why) $.ui.toast(`ctx-bytes-guard: ${why}`)
62  } catch (err) {
63    $.ui.toast(`ctx-bytes-guard: falhou: ${String(err)}`)
64  }
65}
66
67export const register: Register = (on, options) => {
68  const mb = typeof options.maxMb === 'number' && options.maxMb > 0 ? options.maxMb : DEFAULT_MB
69  const guard: Guard = { maxBytes: mb * 1024 * 1024, maxPercent: 0, headless: false }
70  guard.maxPercent =
71    typeof options.maxPercent === 'number' && options.maxPercent > 0 ? options.maxPercent : DEFAULT_PERCENT
72
73  // Antes de cada mensagem: compactar com o turno em andamento é recusado, e o erro de request grande
74  // acontece justamente durante o turno.
75  on('prompt.submit', async ($, e, next) => {
76    await run($, guard, 'submit')
77    return next(e)
78  })
79
80  // Depois do turno: cobre o que cresceu durante ele.
81  on('turn.complete', async ($, e, next) => {
82    const ran = await next(e)
83    if (e.agentId === undefined) {
84      await run($, guard, 'turn')
85    }
86    return ran
87  })
88}
types/index.d.ts 11 lines
1/** Os limites em que o ctx-bytes-guard compacta; o synapse-rate-limit os lê para a linha dos MB. */
2export type GuardLimits = { maxBytes: number; maxPercent: number }
3
4declare module 'claude-code' {
5  interface PluginState {
6    'ctx-bytes-guard': {
7      limits: GuardLimits | null
8    }
9  }
10}
11