@@ -158,6 +158,103 @@ await this.tools.callbacks.run(token, ...args);
158158- RPC stubs: Function references across worker boundaries
159159- Circular references: Objects referencing themselves
160160
161+ ## Critical: Callback Backward Compatibility
162+
163+ ** IMPORTANT:** All callbacks automatically upgrade to new twist/tool versions when a new version is deployed. You ** MUST** maintain backward compatibility in ALL callback methods to prevent breaking existing callbacks.
164+
165+ ### Backward Compatibility Rules
166+
167+ - ❌ ** DON'T** change function signatures (remove/reorder parameters, change types)
168+ - ❌ ** DON'T** change callback signatures passed to ` createFromParent() `
169+ - ✅ ** DO** add optional parameters at the end
170+ - ✅ ** DO** use version guards for behavioral changes
171+ - ✅ ** DO** handle both old and new data formats
172+
173+ ### What Callbacks Auto-Upgrade?
174+
175+ All callbacks automatically upgrade to the new twist version:
176+ - ** Webhook handlers** (onWebhook, onCalendarWebhook, etc.)
177+ - ** Batch processing callbacks** (syncBatch, processBatch, etc.)
178+ - ** Scheduled callbacks** (renewWatch, cleanupExpired, etc.)
179+ - ** Tool→Twist callbacks** (onEvent, handleActivity, etc.)
180+ - ** Internal tool callbacks** (any method called via ` this.callback() ` )
181+
182+ ### Examples: Good and Bad Practices
183+
184+ ``` typescript
185+ // v1.0 - Original
186+ async syncBatch (batchNumber : number , authToken : string , calendarId : string ) {
187+ // Sync logic
188+ }
189+
190+ // v1.1 - ✅ GOOD: Optional parameter added at the end
191+ async syncBatch (
192+ batchNumber : number ,
193+ authToken : string ,
194+ calendarId : string ,
195+ initialSync ?: boolean // New optional parameter
196+ ) {
197+ const isInitial = initialSync ?? true ; // Safe default
198+ // Sync logic with initialSync support
199+ }
200+
201+ // v2.0 - ❌ BAD: Breaking change
202+ async syncBatch (options : SyncOptions ) { // Completely changed signature!
203+ // Existing batch callbacks will fail when they pass (number, string, string)
204+ }
205+
206+ // v2.0 - ✅ GOOD: Handle both old and new signatures
207+ async syncBatch (
208+ batchNumberOrOptions : number | SyncOptions ,
209+ authToken ?: string ,
210+ calendarId ?: string ,
211+ initialSync ?: boolean
212+ ) {
213+ let options: SyncOptions ;
214+
215+ if (typeof batchNumberOrOptions === " number" ) {
216+ // Old version - reconstruct options from individual parameters
217+ options = {
218+ batchNumber: batchNumberOrOptions ,
219+ authToken: authToken ! ,
220+ calendarId: calendarId ! ,
221+ initialSync ,
222+ };
223+ } else {
224+ // New version - use options object directly
225+ options = batchNumberOrOptions ;
226+ }
227+
228+ // Sync logic using options
229+ }
230+ ```
231+
232+ ### Handling Breaking Changes
233+
234+ If you ** must** make a breaking change, implement migration logic in the ` upgrade() ` hook:
235+
236+ ``` typescript
237+ async upgrade (oldVersion : string , newVersion : string ) {
238+ if (oldVersion === " 1.0" && newVersion === " 2.0" ) {
239+ // Get all in-progress syncs and recreate callbacks with new signature
240+ const syncs = await this .get <SyncState []>(" active_syncs" );
241+
242+ for (const sync of syncs ) {
243+ // Stop old sync
244+ await this .stopSync (sync .authToken , sync .calendarId );
245+
246+ // Restart with new callback signature
247+ await this .startSync ({
248+ authToken: sync .authToken ,
249+ calendarId: sync .calendarId ,
250+ }, this .onEvent );
251+ }
252+ }
253+ }
254+ ```
255+
256+ ** Note:** Users will need to uninstall and reinstall the twist/tool if breaking changes aren't properly migrated.
257+
161258## Tool Development Checklist
162259
163260Building a tool? Follow this checklist:
@@ -169,7 +266,9 @@ Building a tool? Follow this checklist:
169266- [ ] ** Store callback tokens** , don't pass them as arguments
170267- [ ] ** Pass only serializable values** to ` this.callback() `
171268- [ ] Retrieve tokens with ` this.get() ` and execute with ` callbacks.run() `
172- - [ ] Use batch processing for long operations (see ` ../twister/cli/templates/AGENTS.template.md ` )
269+ - [ ] Use batch processing for long operations - break loops into chunks to stay under ~ 1000 requests per execution
270+ - [ ] Size batches appropriately - calculate requests per item to determine safe batch size
271+ - [ ] Use ` this.runTask() ` to create new executions with fresh request limits
173272- [ ] Clean up stored state and callbacks in lifecycle methods
174273
175274## Common Tool Pitfalls
@@ -179,6 +278,7 @@ Building a tool? Follow this checklist:
1792783 . ** ❌ Not validating callback token exists** - Always check before ` callbacks.run() `
1802794 . ** ❌ Forgetting to store the callback token** - Store it immediately after creating
1812805 . ** ❌ Passing undefined instead of null** - Use ` null ` for optional values
281+ 6 . ** ❌ Not breaking loops into batches** - Each execution has ~ 1000 request limit; use ` runTask() ` for fresh limits
182282
183283---
184284
0 commit comments