actual-mcp-server 0.22.25 → 0.22.28
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 +2 -2
- package/dist/src/lib/actual-adapter.js +18 -5
- package/dist/src/lib/constants.js +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -454,7 +454,7 @@ For Claude Desktop (stdio), restart Claude after upgrading.
|
|
|
454
454
|
|
|
455
455
|
Four tools take many items in one call, for clients that allow only a few tool calls per turn. They share ONE contract: each item is applied independently and **not atomically**. There is no rollback, so items applied before a failure stay applied; a bad item is reported by its index and the rest continue; an infrastructure failure (lost connection, timeout) fails the whole call, and then you must read the data back before retrying anything. A bulk path never retries a create on its own.
|
|
456
456
|
|
|
457
|
-
- `actual_transactions_update_batch` (counted under Transactions): update up to 50 transactions, `{id, fields}` each. Result: `{ succeeded: [{id}], failed: [{id, error}], total, successCount, failureCount }`.
|
|
457
|
+
- `actual_transactions_update_batch` (counted under Transactions): update up to 50 transactions, `{id, fields}` each. Result: `{ succeeded: [{id}], failed: [{id, error}], total, successCount, failureCount }`. Items marked "not attempted" were never sent. Re-read with `actual_transactions_get` before retrying any item.
|
|
458
458
|
- `actual_transactions_import` (counted under Transactions): bulk create with import semantics (deduplication and reconciliation against existing transactions, rules optionally applied). Use it to add many transactions at once.
|
|
459
459
|
- `actual_budget_updates_batch`: set budget amounts and/or carryover flags for 1 to 100 month/category pairs, and the way to rebalance categories: read the month with `actual_budgets_getMonth`, then send one batch of absolute amounts. Each operation needs at least one of `amount` (integer cents) or `carryover`. Result: `{ succeeded: [{index, month, categoryId}], failed: [{index, month, categoryId, error}], total, successCount, failureCount }`. An unknown category, an out-of-range month or a carryover on an income category fails only that item. After a timeout, read the months back with `actual_budgets_getMonth` before retrying.
|
|
460
460
|
- `actual_rules_create_batch` (counted under Rules): create 1 to 50 rules, each exactly the input of `actual_rules_create`. Result: `{ succeeded: [{index, id}], failed: [{index, error}], total, successCount, failureCount }`. Items marked "not attempted" were never sent. Re-read with `actual_rules_get` before retrying any item.
|
|
@@ -835,7 +835,7 @@ The software is provided **as-is**, without warranty of any kind. The author acc
|
|
|
835
835
|
|
|
836
836
|
---
|
|
837
837
|
|
|
838
|
-
**Version:** 0.22.
|
|
838
|
+
**Version:** 0.22.28 | **Tool Count:** 83 (verified LibreChat-compatible)
|
|
839
839
|
|
|
840
840
|
## Sponsor
|
|
841
841
|
|
|
@@ -1946,9 +1946,11 @@ export async function transferBudgetAmount(month, fromCategoryId, toCategoryId,
|
|
|
1946
1946
|
}
|
|
1947
1947
|
catch (error) {
|
|
1948
1948
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1949
|
-
//
|
|
1950
|
-
// deterministic (domain) rejection may claim the target is unchanged.
|
|
1951
|
-
|
|
1949
|
+
// A transient error (infrastructure or a rate limit) says nothing about whether the write
|
|
1950
|
+
// landed, so only a deterministic (domain) rejection may claim the target is unchanged.
|
|
1951
|
+
// #544: this is isRetryableError, not _shouldDropPoolOnError: a rate limit keeps the pool
|
|
1952
|
+
// but its outcome is just as unknown. The pool-drop decision itself is untouched.
|
|
1953
|
+
const target = isRetryableError(error)
|
|
1952
1954
|
? `its outcome is unknown (it was ${prevTo})`
|
|
1953
1955
|
: `it is unchanged at ${prevTo}`;
|
|
1954
1956
|
// The original text stays INSIDE the message: isRetryableError classifies by message only
|
|
@@ -2030,6 +2032,9 @@ export async function updateTransaction(id, fields) {
|
|
|
2030
2032
|
observability.incrementToolCall('actual.transactions.update').catch(() => { });
|
|
2031
2033
|
// Use write queue to batch concurrent updates in a single budget session
|
|
2032
2034
|
return queueWriteOperation(async () => {
|
|
2035
|
+
// #494: the drain's op timeout starts when this callback starts, so the split poll's cap
|
|
2036
|
+
// below is measured from here, not from the end of the write (same origin as #538's startedAt).
|
|
2037
|
+
const opStartedAt = Date.now();
|
|
2033
2038
|
// Pre-flight existence check (#212): the raw API silently no-ops on a missing id,
|
|
2034
2039
|
// so an update that changed nothing would otherwise be reported as success. A
|
|
2035
2040
|
// targeted ActualQL query by id keeps this cheap (indexed lookup). #305 extends
|
|
@@ -2100,8 +2105,16 @@ export async function updateTransaction(id, fields) {
|
|
|
2100
2105
|
// with every requested child. Polling here, inside the drain, also guarantees the change is
|
|
2101
2106
|
// local before the drain's api.sync(). A retry after a timeout is safe: the row is then a
|
|
2102
2107
|
// split, so a second call EDITS its children rather than creating anything new.
|
|
2108
|
+
// #494: the poll must end before the op timeout does, or the caller sees "timed out" (which
|
|
2109
|
+
// invites a retry) while this callback keeps reading in the background. Cap it at HALF the op
|
|
2110
|
+
// timeout, counted from op start; 0 disables the op timeout, so the poll keeps its own bound.
|
|
2111
|
+
// Do not cap at opStartedAt + boundMs: that term would also shorten the poll at the default.
|
|
2103
2112
|
const expected = subs.length;
|
|
2104
|
-
const
|
|
2113
|
+
const T = config.ACTUAL_OP_TIMEOUT_MS;
|
|
2114
|
+
const deadline = T > 0
|
|
2115
|
+
? Math.min(Date.now() + SPLIT_VERIFY_TIMEOUT_MS, opStartedAt + Math.floor(T / 2))
|
|
2116
|
+
: Date.now() + SPLIT_VERIFY_TIMEOUT_MS;
|
|
2117
|
+
const boundMs = T > 0 ? Math.min(SPLIT_VERIFY_TIMEOUT_MS, Math.floor(T / 2)) : SPLIT_VERIFY_TIMEOUT_MS;
|
|
2105
2118
|
for (;;) {
|
|
2106
2119
|
const res = (await rawRunQuery(q('transactions').options({ splits: 'all' })
|
|
2107
2120
|
.filter({ $or: [{ id }, { parent_id: id }] })
|
|
@@ -2113,7 +2126,7 @@ export async function updateTransaction(id, fields) {
|
|
|
2113
2126
|
return { split: { parent: id, children } };
|
|
2114
2127
|
}
|
|
2115
2128
|
if (Date.now() >= deadline) {
|
|
2116
|
-
throw new Error(`Transaction "${id}" was sent to be split into ${expected} parts, but the split was not visible after ${
|
|
2129
|
+
throw new Error(`Transaction "${id}" was sent to be split into ${expected} parts, but the split was not visible after ${boundMs}ms (found ${children.length} children), so the outcome is unknown. ` +
|
|
2117
2130
|
'Read it back with actual_transactions_get before retrying: if it is already a split, a retry edits its children rather than duplicating them.');
|
|
2118
2131
|
}
|
|
2119
2132
|
await new Promise((resolve) => setTimeout(resolve, SPLIT_VERIFY_INTERVAL_MS));
|
|
@@ -55,6 +55,13 @@ export const WRITE_SESSION_DELAY_MS = 100;
|
|
|
55
55
|
* therefore reads the split back, polling every SPLIT_VERIFY_INTERVAL_MS, and refuses to report
|
|
56
56
|
* success if it has not appeared within this bound. A local SQLite write, so milliseconds in
|
|
57
57
|
* practice; the bound only decides how long a genuinely lost write takes to be reported.
|
|
58
|
+
*
|
|
59
|
+
* #494: the poll runs inside the drain under ACTUAL_OP_TIMEOUT_MS, which races the callback rather
|
|
60
|
+
* than cancelling it. So the effective bound is min(this, ACTUAL_OP_TIMEOUT_MS / 2), counted from
|
|
61
|
+
* the start of the operation; an op timeout of 0 (disabled) leaves this bound alone. The cap is
|
|
62
|
+
* cooperative: one read already in flight at the deadline can still outlive the op timeout if that
|
|
63
|
+
* single read takes longer than the remaining half. Separately, the pre-flight read's retry backoff
|
|
64
|
+
* (200ms then 400ms) can by itself exceed an op timeout under 600ms; that is outside the poll.
|
|
58
65
|
*/
|
|
59
66
|
export const SPLIT_VERIFY_TIMEOUT_MS = 2000;
|
|
60
67
|
export const SPLIT_VERIFY_INTERVAL_MS = 25;
|