Files
claudetools/.claude/memory/feedback_syncro_timer_response_shape.md
Howard Enos bc39d75304 sync: auto-sync from HOWARD-HOME at 2026-05-05 16:44:25
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-05-05 16:44:25
2026-05-05 16:44:26 -07:00

2.6 KiB

name: Syncro — timer_entry response is FLAT, not wrapped description: POST /tickets/{id}/timer_entry returns a flat object {"id": N, "ticket_id": ..., "product_id": ..., ...}, NOT wrapped in {"timer": {...}} or {"timer_entry": {...}}. Parse as .id, never .timer.id — using the wrapped pattern silently returns null and creates duplicate timers when the script "retries". type: feedback

Rule: When parsing the response from POST /tickets/{id}/timer_entry, use .id directly — the response is a FLAT object. Do NOT use .timer.id // .timer_entry.id.

Verified response shape (2026-05-05, ticket #32253):

{
  "id": 39031258,
  "ticket_id": 109895882,
  "user_id": 1750,
  "start_time": "2026-05-05T09:00:00.000-07:00",
  "end_time": "2026-05-05T09:30:00.000-07:00",
  "recorded": false,
  "billable": true,
  "notes": "...",
  "product_id": 26118,
  "comment_id": null,
  "ticket_line_item_id": null,
  "active_duration": 1800,
  "billable_time": 1800
  ...
}

Why: The skill doc at .claude/commands/syncro.md shows

TIMER_ID=$(echo "$TIMER_RESP" | jq -r '.timer.id // .timer_entry.id')

That fallback resolves to null because neither key exists on the flat response. A null TIMER_ID then breaks charge_timer_entry ("Not found"). If the script retries the timer_entry POST after the perceived failure, it creates a duplicate — Syncro has no idempotency. Hit this on ticket #32253 (Cascades) on 2026-05-05; created two duplicate 0.5hr timers and had to delete one via delete_timer_entry before charging.

How to apply:

  • Parsing: Always jq -r '.id' on the timer_entry response.
  • After ANY ambiguous timer_entry response (null .id, jq error, network blip): GET the ticket and inspect .ticket.ticket_timers[] BEFORE retrying. Filter for recorded: false entries with the start/end times you just sent.
  • Cleanup if duplicates exist: POST /tickets/{id}/delete_timer_entry with {"timer_entry_id": N} for the older duplicate(s). Returns {"success": true}.
  • Verifying the timer is on the ticket: GET /tickets/{id}.ticket.ticket_timers is the authoritative list. The standalone /ticket_timers?ticket_id=N query parameter does NOT filter by ticket — returns the entire global timer history.

Charge timer response is also flat:

{"id": 39031258, "recorded": true, "ticket_line_item_id": 42313052, ...}

Parse as .ticket_line_item_id to get the auto-generated line. Do not look for a wrapper.

Where this lands in skill code: .claude/commands/syncro.md example block needs .id not .timer.id // .timer_entry.id. Until the skill is patched, override the example pattern when running.