A plugin that clips arbitrary selected turn ranges out of a conversation and merges them into a new session.
/klip 1..3,7 # cut turns 1..3 and 7 into one new session
/klip -5.., not -3 # the last 5 turns, minus turn 3klip re-indexes the selected events into a new session and attaches it to the current workspace.
A KInterval is a comma-separated list of clauses, each selecting turns by 1-based index. Negative numbers count from the end (-1 is the last turn). All intervals are closed.
| Form | Meaning |
|---|---|
x |
turn x only |
a..b |
turns a through b |
a.. |
turn a to the last |
..b |
first turn through b |
.. |
all turns |
not I |
exclude the interval I |
Examples:
/klip 3 # just turn 3
/klip 2..5 # turns 2, 3, 4, 5
/klip 4.. # turn 4 to the last
/klip ..3 # turns 1, 2, 3
/klip .. # all turns
/klip .., not 2 # all turns except turn 2
/klip -3.. # the last 3 turnsWhitespace is ignored, so 1..2 and 1 .. 2 are equivalent.
- Automatic re-indexing. Selected turns are renumbered to a contiguous
1..NandSessionEvent.seqis reset to start from 0. - Dangling reference cleanup. When an event references a cut-away event, it is dropped too, cascading until nothing references a deleted event.
- Customizable rules. Re-indexing is driven by the rule tables in
src/rules.ts; supporting a third-party event type only needs a rule, not engine changes. - Automatic naming. New sessions are named
KLIP <source title>.
Re-indexing is driven by two tables in src/rules.ts: turnRules remaps turn values, and seqRules remaps seq and translates references between events. Each event type maps to a cell — a rules array plus an optional presence flag:
override: true— the type's own rules fully replace the*wildcard (default: they extend it).
The two tables have separate rule types. turnRules renumbers turn references (value / array / interval); seqRules owns the seq references plus the structural skip rules. seqRules rules:
value— a single numeric reference (e.g.seq). The event is dropped if the target is not in the result.array— a numeric array reference (e.g.sourceEventSeqs). Dead members are filtered out; the event is dropped only when all members are dead.interval— a closed-interval reference (e.g.surfaceOp.start/surfaceOp.end). It is intersected with the surviving seq set; the event is dropped only when the intersection is empty.skip-n— drops this event and the nextnevents (a fixed-length run), used to drop a prune pair.skip-till— drops events until one of typetillappears (inclusive); uses a bracket-matching stack, so skip blocks nest.
Two optional presence flags may appear on the rules:
keep: true— onarrayonly: an all-dead reference no longer drops the event; instead the event is kept with the array emptied to[].value/intervalhave nokeep(a hard reference still sinks the event when its target is gone).surface: true— onintervalonly (seq table): re-project the range onto the surface-only seq map instead of all survivors. This is whatsurfaceOp.start/surfaceOp.enduse.
A cell may have no reference rule at all ({ rules: [] } or empty), which keeps the event unconditionally — e.g. command/done, whose sourceEventSeq is a display-only soft reference: a value rule on it would drop the event and leave a surviving command/run rendering as still executing (calling forever).
The surface is not a cell or rule flag: it is its own table, seqSurface, listing the event types that join the model-visible surface (message-producing nodes the surface fold keeps). A type in that list is added to the surface-only seq map; an interval with surface: true uses that map, while every other reference (value / array / interval) re-projects onto all survivors. Ordinary references like sourceEventSeqs — which point at plain records such as tool/call — leave surface off precisely so they do not get constrained to the surface.
To support a third-party event type, add a cell (and list it in seqSurface if it produces a message):
// src/rules.ts
export const seqSurface = ['user/message', 'assistant/message', 'tool/result']
export const seqRules: SeqReIndexRules = {
'*': { rules: [{ kind: 'value', path: 'seq' }] },
// ...existing user/message, tool/result, ... entries...
'my/plugin/event': {
rules: [{ kind: 'value', path: 'data.parentSeq' }], // new
},
}Rebuild (npm run build) and restart the profile to apply.
KInterval (src/k-interval.ts) parses the range text into include/exclude intervals; reIndexEvents (src/re-index.ts) takes the selected events, renumbers them (seq contiguous, turn dense), and remaps all references, producing a valid session seed.
Notes:
- Only completed turns are cut. Header events (those without a
turnfield, before the first turn) are always kept. - The new session is created through the agent factory, flushed to disk, then attached to the source workspace.
dsh-klip/
├── package.json # package contract: exports, peer deps
├── scripts/build.mjs # esbuild build
├── src/
│ ├── index.ts # plugin entry: /klip command, session creation, workspace attach
│ ├── k-interval.ts # KInterval language (pure)
│ ├── rules.ts # editable rule tables (default turnRules / seqRules)
│ └── re-index.ts # pure re-indexing of events into a session seed
└── test/ # KInterval and re-indexing tests
npm run typecheck # tsc --noEmit
npm run build # emit lib/index.js, lib/types/
npm test # run the testsMIT