Skip to content

Commit 97e4949

Browse files
committed
Add Activity.preview; improve source and key immutability
1 parent cca290d commit 97e4949

21 files changed

Lines changed: 1423 additions & 180 deletions

File tree

.changeset/itchy-lies-beam.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@plotday/tool-outlook-calendar": minor
3+
"@plotday/tool-google-calendar": minor
4+
"@plotday/tool-linear": minor
5+
"@plotday/tool-asana": minor
6+
"@plotday/tool-gmail": minor
7+
"@plotday/tool-slack": minor
8+
"@plotday/tool-jira": minor
9+
"@plotday/twister": minor
10+
---
11+
12+
Changed: BREAKING: Improve immutability of Activity.source and Note.key by using IDs rather than URLs

.changeset/public-llamas-pull.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@plotday/tool-outlook-calendar": patch
3+
"@plotday/tool-google-calendar": patch
4+
"@plotday/tool-linear": patch
5+
"@plotday/tool-asana": patch
6+
"@plotday/tool-gmail": patch
7+
"@plotday/tool-slack": patch
8+
"@plotday/tool-jira": patch
9+
"@plotday/twister": patch
10+
---
11+
12+
Added: Provide an activity preview

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ All type definitions are in `twister/src/` with full JSDoc:
3333
## Common Pitfalls
3434

3535
1. **❌ Using instance variables for state** - Use `this.set()`/`this.get()` instead (state doesn't persist between executions)
36-
2. **❌ Long-running operations without batching** - Break into chunks with `runTask()` (execution time limits)
36+
2. **❌ Long-running operations without batching** - Break into chunks with `runTask()` (request limits: ~1000 per execution)
3737
3. **❌ Forgetting to clean up** - Delete callbacks and stored state when done
3838
4. **❌ Not handling missing auth** - Always check for stored tokens before operations
3939
5. **❌ Passing functions to `this.callback()`** - See `tools/AGENTS.md` for critical callback serialization pattern

tools/AGENTS.md

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

163260
Building 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:
179278
3. **❌ Not validating callback token exists** - Always check before `callbacks.run()`
180279
4. **❌ Forgetting to store the callback token** - Store it immediately after creating
181280
5. **❌ 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

Comments
 (0)