@magpie-community/opencode-cursor-auth 0.2.40 → 0.2.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -102,7 +102,11 @@ uses the same session/cache key and first user message as `conversation_id`, so
102
102
  Task subagents sharing their parent's session can run independently. A new
103
103
  user turn whose earlier native calls all have their results gets a retained Run
104
104
  of its own, with those calls as imported history; while any of them is still
105
- pending, the turn goes stateless. Retention
105
+ pending, the turn goes stateless. New input after results already given (a
106
+ reminder Claude Code sends beside a tool result reaches the plugin as a user
107
+ message after it) also gets a retained Run of its own while a parent Run of
108
+ the conversation still awaits results. That Run takes no reservation and
109
+ leaves the parent alone; only its own calls' results resume it. Retention
106
110
  waits at most 120 seconds for a result batch and 15 minutes for a Run's total
107
111
  lifetime. A continuation arriving with less than 10 minutes of that lifetime
108
112
  left rebuilds from supplied history on a fresh Run before submitting results
@@ -137,8 +141,8 @@ given also ends the step (#550). The step's calls are the rest.
137
141
 
138
142
  These are retention limits. A missing, expired, restarted, busy, incompatible,
139
143
  or full owner uses the original history reconstruction and stateless decoder
140
- on a new Run. Pending parent calls and new input can take that path without
141
- cancelling the parent's owner. Already supplied client tool results become
144
+ on a new Run. Pending parent calls can take that path without cancelling the
145
+ parent's owner. Already supplied client tool results become
142
146
  history; the plugin never executes those external tools. A failed native result
143
147
  write is reported to that request because its outcome may be uncertain. A later
144
148
  request can rebuild from its supplied history without submitting the result to
@@ -155,12 +159,22 @@ stateless debt as `usage.magpie_deferred`, separately from its own prompt total.
155
159
  If a started native Run is abandoned after a pending handoff, its dashboard
156
160
  count is owed once to the same account and conversation. Successful final
157
161
  snapshots, including explicit zero, and cached replays do not enroll that debt.
162
+ A Run still awaiting results when a later native Run of its conversation
163
+ starts owes that count at once, for the later Run to collect, rather than at
164
+ abandonment; what it had collected and not given out goes along. Cursor adds
165
+ each step to a Run's one event as the step ends, so that event is looked for
166
+ only `owedAt` (5 seconds) after the handover; a final waits for it. If its own
167
+ results resume it after all, a count not collected yet is withdrawn, and one
168
+ already collected comes off its final (from the earlier calls' share first),
169
+ so the event is billed once.
158
170
 
159
171
  Dashboard attribution uses event timestamps, rather than when events arrive.
160
172
  Older debt is bounded strictly before the native Run's start, and keeps that
161
173
  bound across later stateless Runs so an authoritative native event is not
162
- billed again. An earlier event stamped on or after that boundary cannot be
163
- identified reliably and stays unknown. Missing or ambiguous debt expires after
174
+ billed again. A Run abandoned after a later native Run of its conversation
175
+ started owes with the same bound, before that later start. An earlier event
176
+ stamped on or after that boundary cannot be identified reliably and stays
177
+ unknown. Missing or ambiguous debt expires after
164
178
  10 minutes; at most 64 distinct bounded debt groups are kept per account and
165
179
  conversation, dropping the oldest on overflow. These counts are never guessed
166
180
  as prior usage. Stateless Runs otherwise keep the dashboard accounting above.
package/index.mjs CHANGED
@@ -2014,11 +2014,13 @@ function addUsage(a, b) {
2014
2014
  // event timestamps before its start: its authoritative final must never be
2015
2015
  // collected as an earlier Run. Keep that bound when a later stateless Run
2016
2016
  // adds unbounded debt. Ordinary stateless debt retains its original shape.
2017
+ // A debt owed with a tag is one Run's event, kept in a group of its own: the
2018
+ // tag says whether a step collected it (and what) or the Run withdrew it.
2017
2019
  const DEBT_GROUP_LIMIT = 64
2018
- function owe(key, { since = Date.now(), n = 0, ready = Date.now(), carry = null, before }) {
2020
+ function owe(key, { since = Date.now(), n = 0, ready = Date.now(), carry = null, before, tag }) {
2019
2021
  for (const [k, d] of owed) {
2020
2022
  if (d.groups) {
2021
- d.groups = d.groups.filter((g) => Date.now() - g.since <= STEP_WAIT.owedFor)
2023
+ d.groups = d.groups.filter((g) => Date.now() - g.since <= STEP_WAIT.owedFor && g.tag?.state !== "withdrawn")
2022
2024
  d.n = d.groups.reduce((sum, g) => sum + g.n, 0)
2023
2025
  if (Date.now() - d.since > STEP_WAIT.owedFor) d.carry = null
2024
2026
  if (!d.n && !d.carry) owed.delete(k)
@@ -2027,15 +2029,16 @@ function owe(key, { since = Date.now(), n = 0, ready = Date.now(), carry = null,
2027
2029
  }
2028
2030
  if (n <= 0 && !carry) return
2029
2031
  const d = owed.get(key)
2032
+ if (!d && tag) return void owed.set(key, { since, n, ready, carry: carry && { ...carry }, groups: [{ since, n, ready, before, tag }] })
2030
2033
  if (!d) return void owed.set(key, { since, n, ready, carry: carry && { ...carry }, ...(before !== undefined ? { before } : {}) })
2031
- if (n > 0 && (d.groups || d.before !== undefined || before !== undefined)) {
2034
+ if (n > 0 && (d.groups || d.before !== undefined || before !== undefined || tag)) {
2032
2035
  const groups = d.groups ?? (d.n > 0 ? [{ since: d.since, n: d.n, ready: d.ready, before: d.before }] : [])
2033
- const same = groups.find((g) => g.before === before)
2036
+ const same = !tag && groups.find((g) => !g.tag && g.before === before)
2034
2037
  if (same) {
2035
2038
  same.since = Math.min(same.since, since)
2036
2039
  same.n += n
2037
2040
  same.ready = Math.min(same.ready, ready)
2038
- } else groups.push({ since, n, ready, before })
2041
+ } else groups.push({ since, n, ready, before, ...(tag ? { tag } : {}) })
2039
2042
  // As with expired dashboard debt, overflow stays unknown, never estimated
2040
2043
  // or merged across bounds into an authoritative native Run's event.
2041
2044
  groups.sort((a, b) => a.since - b.since)
@@ -2051,6 +2054,19 @@ function owe(key, { since = Date.now(), n = 0, ready = Date.now(), carry = null,
2051
2054
  d.carry = addUsage(d.carry, carry)
2052
2055
  }
2053
2056
 
2057
+ // withdraw takes back the debt owed with tag while no step has collected it:
2058
+ // its Run was resumed after all, and its own final counts its event.
2059
+ function withdraw(key, tag) {
2060
+ if (tag.state !== "owed") return
2061
+ tag.state = "withdrawn"
2062
+ const d = owed.get(key)
2063
+ if (!d?.groups) return
2064
+ d.groups = d.groups.filter((g) => g.tag !== tag)
2065
+ d.n = d.groups.reduce((sum, g) => sum + g.n, 0)
2066
+ if (!d.n && !d.carry) owed.delete(key)
2067
+ else if (d.groups.length) d.since = Math.min(...d.groups.map((g) => g.since))
2068
+ }
2069
+
2054
2070
  // usageEvents are the dashboard's events of conversation conv from since
2055
2071
  // on, the `want` oldest not counted yet, summed: usage (null when none),
2056
2072
  // each with its time (events), how many were found (got), and refused
@@ -2094,12 +2110,17 @@ async function usageEvents(tok, conv, since, want, { poll = false, first = STEP_
2094
2110
  // found already excludes timestamps claimed by earlier fetches. Two
2095
2111
  // distinct events in this response may share a millisecond; count both.
2096
2112
  if (at >= before()) continue
2097
- const group = groups?.findIndex((g, i) => counts[i] < g.n && at >= g.since - 1000 && at < (g.before ?? Infinity))
2113
+ const group = groups?.findIndex((g, i) => counts[i] < g.n && g.tag?.state !== "withdrawn" && at >= g.since - 1000 && at < (g.before ?? Infinity))
2098
2114
  if (groups && group === -1) continue
2099
2115
  if (groups) counts[group]++
2100
2116
  claimed.set(key + "\0" + ev.timestamp, Date.now())
2101
2117
  const t = ev.tokenUsage
2102
2118
  const u = { input: num(t.inputTokens), output: num(t.outputTokens), cacheRead: num(t.cacheReadTokens), cacheWrite: num(t.cacheWriteTokens), reasoning: 0 }
2119
+ const tag = groups?.[group].tag
2120
+ if (tag) {
2121
+ tag.state = "collected"
2122
+ tag.usage = addUsage(tag.usage, u)
2123
+ }
2103
2124
  usage = addUsage(usage ?? ZERO(), u)
2104
2125
  events.push({ at: num(ev.timestamp), usage: u })
2105
2126
  got++
@@ -2128,8 +2149,15 @@ function collect(tok, conv, { poll = false, first, signal, over = () => false, a
2128
2149
  const d = owed.get(key)
2129
2150
  if (!d) return Promise.resolve({ usage: null, carry: null, events: [], short: false })
2130
2151
  owed.delete(key)
2152
+ // A handed-over Run's event still grows by the step that Run just ended
2153
+ // (Cursor adds each step to a Run's one event as the step ends). It is
2154
+ // looked for only from its own ready on, whatever else is owed with it
2155
+ // sooner: until then it stays owed.
2156
+ const now = Date.now()
2157
+ const held = d.groups?.filter((g) => g.tag?.state === "owed" && g.ready > now) ?? []
2158
+ for (const g of held) owe(key, { since: g.since, n: g.n, ready: g.ready, before: g.before, tag: g.tag })
2131
2159
  const p = (async () => {
2132
- const groups = (d.groups ?? [d]).filter((g) => (g.before === undefined && !d.groups) || Date.now() - g.since <= STEP_WAIT.owedFor)
2160
+ const groups = (d.groups ?? [d]).filter((g) => !held.includes(g) && g.tag?.state !== "withdrawn" && ((g.before === undefined && !d.groups) || Date.now() - g.since <= STEP_WAIT.owedFor))
2133
2161
  const want = groups.reduce((sum, g) => sum + g.n, 0)
2134
2162
  let r = { usage: null, events: [], got: 0 }
2135
2163
  if (want > 0 && canRead(tok)) r = await usageEvents(tok, conv, Math.min(...groups.map((g) => g.since)), want, {
@@ -2138,9 +2166,10 @@ function collect(tok, conv, { poll = false, first, signal, over = () => false, a
2138
2166
  let short = false
2139
2167
  for (const [i, g] of groups.entries()) {
2140
2168
  const remaining = g.n - (d.groups ? r.counts?.[i] ?? 0 : r.got)
2141
- if (remaining > 0 && canRead(tok)) {
2169
+ // a tag withdrawn while this was asked is its Run's own again
2170
+ if (remaining > 0 && canRead(tok) && g.tag?.state !== "withdrawn") {
2142
2171
  short = true
2143
- owe(key, { since: g.since, n: remaining, ready: g.ready, before: g.before })
2172
+ owe(key, { since: g.since, n: remaining, ready: g.ready, before: g.before, tag: g.tag })
2144
2173
  }
2145
2174
  }
2146
2175
  const carry = d.carry && { ...d.carry }
@@ -2200,10 +2229,21 @@ function ledger(tok, conv, since, signal, kept, { before } = {}) {
2200
2229
  }
2201
2230
  return r
2202
2231
  })
2232
+ // heldUntil is when the latest handed-over event still held back
2233
+ // (collect) becomes ready; 0 when none is
2234
+ const heldUntil = () => {
2235
+ const now = Date.now()
2236
+ return Math.max(0, ...(owed.get(debtKey)?.groups ?? []).filter((g) => g.tag?.state === "owed" && g.ready > now).map((g) => g.ready))
2237
+ }
2203
2238
  let collecting = null
2204
2239
  const debt = kept ? owed.get(debtKey) : undefined
2205
2240
  const timer = debt ? setTimeout(() => (collecting = gather()), Math.max(0, debt.ready - Date.now())) : null
2206
2241
  timer?.unref?.()
2242
+ const sleep = (ms) => new Promise((r) => {
2243
+ if (signal?.aborted) return r()
2244
+ const t = setTimeout(r, ms)
2245
+ signal?.addEventListener("abort", () => (clearTimeout(t), r()), { once: true })
2246
+ })
2207
2247
  const takeMine = () => {
2208
2248
  const u = mine
2209
2249
  mine = null
@@ -2239,10 +2279,27 @@ function ledger(tok, conv, since, signal, kept, { before } = {}) {
2239
2279
  if (final) {
2240
2280
  await collecting
2241
2281
  await Promise.all([...(flights.get(debtKey) ?? [])])
2282
+ // a handed-over event held back is waited for, up to its ready
2283
+ const until = heldUntil()
2284
+ if (until) await sleep(until - Date.now())
2285
+ await Promise.all([...(flights.get(debtKey) ?? [])])
2242
2286
  if (owed.has(debtKey)) await gather({ poll: owed.get(debtKey).n > 0, first: 0, signal })
2243
2287
  }
2244
2288
  return take()
2245
2289
  },
2290
+ // handover: a later Run of the conversation began while this one waits
2291
+ // on the caller's results. That Run collects from here on: what this one
2292
+ // collected and has not given out goes to it, as does what an ask still
2293
+ // going brings. A step that ends this Run after all still collects.
2294
+ handover() {
2295
+ clearTimeout(timer)
2296
+ const give = () => {
2297
+ const u = take()
2298
+ if (u) owe(debtKey, { carry: u })
2299
+ }
2300
+ give()
2301
+ collecting?.then(give, () => {})
2302
+ },
2246
2303
  done() {
2247
2304
  over = true
2248
2305
  clearTimeout(timer)
@@ -2700,8 +2757,32 @@ async function runNative({ tok, base, id, tools, conv, convID, signal, maxMode,
2700
2757
  const book = ledger(tok, cid, since, accounting.signal, !!convID, { before: since })
2701
2758
  const debtKey = usageKey(tok, cid)
2702
2759
  let started = false, settled = false, deferred = null
2760
+ // handed is this Run's step owed at a later Run's start while this one
2761
+ // waited on results (handover): owed, collected (and what), or withdrawn
2762
+ let handed = null
2763
+ const owes = () => started && native.segments > 0 && !native.finalUsage && convID && canRead(tok)
2703
2764
  native.accounting = {
2704
2765
  started() { started = true },
2766
+ // A later Run of the conversation began (NativeSessionPool.create) while
2767
+ // this one waits on the caller's results. Its step's event is owed now,
2768
+ // bounded before that start, for the later Run to collect, rather than
2769
+ // when this one is abandoned at resultWait. Cursor stamps a Run's one
2770
+ // event at its start and adds each step to it as the step ends.
2771
+ handover() {
2772
+ if (settled || handed && handed.state !== "withdrawn" || !owes()) return
2773
+ handed = { state: "owed" }
2774
+ owe(debtKey, { since, n: 1, ready: Date.now() + STEP_WAIT.owedAt, before: native.laterRunAt, tag: handed })
2775
+ book.handover()
2776
+ },
2777
+ // resumed by its own results after all: a step not collected yet is this
2778
+ // Run's to count again, in its final; one collected is counted (below)
2779
+ resumed() {
2780
+ if (handed?.state === "owed") withdraw(debtKey, handed)
2781
+ },
2782
+ // what a later Run collected of this Run's event: its final leaves it out
2783
+ counted() {
2784
+ return handed?.state === "collected" ? handed.usage : null
2785
+ },
2705
2786
  async settle() {
2706
2787
  const more = await book.owed(true)
2707
2788
  if (settled) {
@@ -2725,10 +2806,14 @@ async function runNative({ tok, base, id, tools, conv, convID, signal, maxMode,
2725
2806
  accounting.abort()
2726
2807
  if (deferred) owe(debtKey, { carry: deferred })
2727
2808
  deferred = null
2809
+ // a step handed over is owed already, or was collected
2810
+ if (handed && handed.state !== "withdrawn") return
2728
2811
  // Every handoff of this owner belongs to one Run. Only an abandoned
2729
2812
  // started/pending Run is owed; a final snapshot (including zero) is not.
2730
- if (started && native.segments > 0 && !native.finalUsage && convID && canRead(tok)) {
2731
- owe(debtKey, { since, n: 1, ready: Date.now() + STEP_WAIT.owedAt })
2813
+ if (owes()) {
2814
+ // laterRunAt: a later Run of this conversation began while this one
2815
+ // was live (NativeSessionPool.create); its events are not this debt
2816
+ owe(debtKey, { since, n: 1, ready: Date.now() + STEP_WAIT.owedAt, before: native.laterRunAt })
2732
2817
  }
2733
2818
  },
2734
2819
  }
@@ -805,7 +805,15 @@ export class NativeSessionPool {
805
805
  const freshTurn = last?.role === "user" && !toolAttachment(last)
806
806
  if (!freshTurn || historical.some((c) => !answered.has(c.id))) return fallback("historical native calls")
807
807
  }
808
- if (selector && this.selectors.has(account + ":" + selector)) return fallback("owner busy")
808
+ if (selector && this.selectors.has(account + ":" + selector)) {
809
+ if (!historical.length) return fallback("owner busy")
810
+ // New input after results the caller already gave its native calls
811
+ // (Claude Code sends a reminder beside a tool result, which a chat
812
+ // request carries as a user message after the tool message). The
813
+ // reservation's holder, a parent awaiting results, is left as it is:
814
+ // this Run takes no reservation, and only its own calls resume it.
815
+ return { account, selector, controls, selected: false, chat }
816
+ }
809
817
  return { account, selector, controls, selected: !!selector, chat }
810
818
  }
811
819
  create(prepared, resolution, wire) {
@@ -823,6 +831,17 @@ export class NativeSessionPool {
823
831
  if (!(e instanceof NativeSessionError)) throw e
824
832
  return null
825
833
  }
834
+ // A Run of this conversation still live may be abandoned after this one
835
+ // starts: what it then owes is bounded before this start, so this Run's
836
+ // own dashboard events are never collected as its debt.
837
+ // One that waits on the caller's results owes its step now, for this
838
+ // Run to collect, rather than when it is abandoned at resultWait.
839
+ const at = Date.now()
840
+ for (const e of this.entries) {
841
+ if (e.account !== owner.account || !e.selector || e.selector !== owner.selector) continue
842
+ e.laterRunAt ??= at
843
+ if (e.state === "awaiting-results") e.accounting?.handover()
844
+ }
826
845
  this.entries.add(owner)
827
846
  if (prepared.selected) this.selectors.set(owner.account + ":" + owner.selector, owner)
828
847
  return owner
@@ -1004,6 +1023,9 @@ class NativeSession {
1004
1023
  batch.next = next
1005
1024
  this.segment = next
1006
1025
  this.state = "submitting"
1026
+ // a step owed at a later Run's start and not collected yet is this Run's
1027
+ // to count again, in its own final
1028
+ this.accounting?.resumed()
1007
1029
  clearTimeout(this.resultTimer)
1008
1030
  clearTimeout(this.batchTimer)
1009
1031
  this.batchTimer = null
@@ -1265,6 +1287,15 @@ class NativeSession {
1265
1287
  earlier = { input: u.input - own.input, cacheRead: u.cacheRead - own.cacheRead, cacheWrite: u.cacheWrite - own.cacheWrite, output: u.output - own.output }
1266
1288
  Object.assign(u, own)
1267
1289
  }
1290
+ // What a later Run collected of this Run's event while this one waited on
1291
+ // results (handover) is in the turn's totals too: it comes off them, from
1292
+ // the earlier steps' share first, so the event counts once.
1293
+ const counted = this.accounting?.counted?.()
1294
+ if (counted) for (const k of ["input", "cacheRead", "cacheWrite", "output"]) {
1295
+ const off = Math.min(earlier?.[k] ?? 0, counted[k] ?? 0)
1296
+ if (earlier) earlier[k] -= off
1297
+ u[k] = Math.max(0, u[k] - ((counted[k] ?? 0) - off))
1298
+ }
1268
1299
  const prompt = u.input + u.cacheRead + u.cacheWrite
1269
1300
  this.finalUsage = {
1270
1301
  prompt_tokens: prompt, completion_tokens: u.output, total_tokens: prompt + u.output,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-cursor-auth",
3
- "version": "0.2.40",
3
+ "version": "0.2.41",
4
4
  "description": "Cursor subscriptions (Pro, Pro+, Ultra, Teams) in OpenCode and magpie, through the API cursor-agent talks to",
5
5
  "type": "module",
6
6
  "main": "./opencode.mjs",