borgmcp 2.14.0 → 2.14.2
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/dist/assimilate-cmd.js +1 -1
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/inbox-monitor.d.ts +13 -1
- package/dist/inbox-monitor.d.ts.map +1 -1
- package/dist/inbox-monitor.js +25 -2
- package/dist/inbox-monitor.js.map +1 -1
- package/dist/log-stream.d.ts.map +1 -1
- package/dist/log-stream.js +32 -10
- package/dist/log-stream.js.map +1 -1
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +66 -7
- package/dist/remote-client.js.map +1 -1
- package/docs/EXTRACTION_PROVENANCE.md +3 -4
- package/docs/RELEASING.md +11 -1
- package/package.json +1 -1
- package/src/assimilate-cmd.ts +1 -1
- package/src/inbox-monitor.ts +30 -1
- package/src/log-stream.ts +34 -9
- package/src/remote-client.ts +75 -5
|
@@ -22,7 +22,7 @@ The extraction copied the monorepo's `client/src/` production boundary and top-l
|
|
|
22
22
|
recovery, `2.0.2`, `2.0.3`, `2.0.4`, `2.0.5`, and `2.0.6` successors were
|
|
23
23
|
published and registry-verified. The immutable `2.0.7` workflow failed before
|
|
24
24
|
package creation or npm publication. The `2.0.8`, `2.0.9`, `2.0.10`, `2.0.11`,
|
|
25
|
-
`2.1.0`, and `2.1.1` successors were published, and `2.2.0` was subsequently published, and `2.3.0` was subsequently published, and `2.4.0` was subsequently published, and `2.4.1` was subsequently published, and `2.5.0` was subsequently published, and `2.6.0` was subsequently published, and `2.6.1` was subsequently published, and `2.7.0` was subsequently published, and `2.7.1` was subsequently published, and `2.7.2` was subsequently published, and `2.7.3` was subsequently published, and `2.8.0` was subsequently published, and `2.9.0` was subsequently published, and `2.10.0` was subsequently published, and the immutable `2.10.1` release attempt failed before artifact creation or publication. The `2.10.2`, `2.11.0`, `2.12.1`, and `2.13.0` successors were published; the immutable `2.12.0` attempt failed before artifact creation or publication and was superseded by `2.12.1`. The
|
|
25
|
+
`2.1.0`, and `2.1.1` successors were published, and `2.2.0` was subsequently published, and `2.3.0` was subsequently published, and `2.4.0` was subsequently published, and `2.4.1` was subsequently published, and `2.5.0` was subsequently published, and `2.6.0` was subsequently published, and `2.6.1` was subsequently published, and `2.7.0` was subsequently published, and `2.7.1` was subsequently published, and `2.7.2` was subsequently published, and `2.7.3` was subsequently published, and `2.8.0` was subsequently published, and `2.9.0` was subsequently published, and `2.10.0` was subsequently published, and the immutable `2.10.1` release attempt failed before artifact creation or publication. The `2.10.2`, `2.11.0`, `2.12.1`, and `2.13.0` successors were published; the immutable `2.12.0` attempt failed before artifact creation or publication and was superseded by `2.12.1`. The current release identity is `2.14.2`. Extraction and
|
|
26
26
|
versioning do not authorize publication.
|
|
27
27
|
|
|
28
28
|
## Review Holds
|
|
@@ -47,7 +47,6 @@ client `borgmcp@2.10.0` line. Its annotated tag object
|
|
|
47
47
|
`sha512-lOxIPg3WcjSBte46iTM7SAFVN6Y0b2oizOKAJ9Q1EijBdOlmsh/cQlpUIaa8F2UJPaTDFsXy3M64gpGs6XgKzA==`
|
|
48
48
|
in the `borgmcp-server-0.9.0-release` registry decision. That server and its
|
|
49
49
|
client line pin historical `borgmcp-shared` version `0.8.1` and remain
|
|
50
|
-
immutable. Client `borgmcp@2.
|
|
51
|
-
attempt failed before publication and remains preserved.
|
|
52
|
-
candidate remains gated by reviewed `v2.14.0` source, a fresh annotated tag, and
|
|
50
|
+
immutable. Client `borgmcp@2.14.1` is published. The immutable `v2.0.7`
|
|
51
|
+
attempt failed before publication and remains preserved. The current release identity and publication gate remain governed by the reviewed `v2.14.2` source, a fresh annotated tag, and
|
|
53
52
|
the exact-artifact and protected-publication gates.
|
package/docs/RELEASING.md
CHANGED
|
@@ -205,8 +205,18 @@ Never move, replace, reuse, or rerun that tag or workflow. The annotated `v2.13.
|
|
|
205
205
|
`fc5e4d9299210b70f979c7d39130a9c12ea265ea`. Workflow run `30894287863`, attempt 1,
|
|
206
206
|
successfully published that exact source as `borgmcp@2.13.0`; the same-run artifact report records integrity
|
|
207
207
|
`sha512-YuF3NwY5iBRVKhgdRiK+3GX+6O8O7S/V9g+XYN/1A0so9RLIYoQWpS91qJewqGQ59nyIARjRmk2wKhK8ODxU0Q==`.
|
|
208
|
+
Never move, replace, reuse, or rerun that tag or workflow. The annotated `v2.14.0` tag object
|
|
209
|
+
`eb9d5cb14b08b1a2eb6d48b2492acbc6cecb7bef` peels to protected-main commit
|
|
210
|
+
`4dd92bcbac26f460a4f137f766992e8f93843576`. Workflow run `30929322201`, attempt 1,
|
|
211
|
+
successfully published that exact source as `borgmcp@2.14.0`; the same-run artifact report records integrity
|
|
212
|
+
`sha512-haKgpQbcbaRya1XqYQaOy7yZiHMxeP8QO3fr7ahHaYDNYrDvlGmXNlW9VMtqnRJ9mtVDC5UTkeuQmCRJMzF/Tg==`.
|
|
213
|
+
Never move, replace, reuse, or rerun that tag or workflow. The annotated `v2.14.1` tag object
|
|
214
|
+
`c1c8b526dcc31259f6b821f0a6fdd0017a42435c` peels to protected-main commit
|
|
215
|
+
`3de4cbf2e5adf6883334fe4e9b7aa1f43868daae`. Workflow run `30943397747`, attempt 1,
|
|
216
|
+
successfully published that exact source as `borgmcp@2.14.1`; the same-run artifact report records integrity
|
|
217
|
+
`sha512-VqkOp3jo/KnzgusCk98TFj/U8U7Q/gt8agvIbjiSWrV0lIHnsD+A+7w3C+GMQeyA08KXxRuZDF3No9sDomqMTQ==`.
|
|
208
218
|
Never move, replace, reuse, or rerun that tag or workflow. The next candidate
|
|
209
|
-
uses the unused `v2.14.
|
|
219
|
+
uses the unused `v2.14.2` identity from a fresh reviewed protected-main commit
|
|
210
220
|
and requires the complete release gate again.
|
|
211
221
|
|
|
212
222
|
## Release Prerequisites
|
package/package.json
CHANGED
package/src/assimilate-cmd.ts
CHANGED
|
@@ -1060,7 +1060,7 @@ export async function runAssimilate(
|
|
|
1060
1060
|
}
|
|
1061
1061
|
if (error instanceof RepositoryAssociationOperationError) {
|
|
1062
1062
|
const recovery = error.failure === 'repository-already-associated'
|
|
1063
|
-
? 'This repository is already associated with another cube. Run the same command again to use the existing managed association, or ask the server operator to correct the repository binding.'
|
|
1063
|
+
? 'This repository is already associated with another cube. The selected cube grant is valid, but it cannot replace that existing repository binding. Run the same command again to use the existing managed association, or ask the server operator to correct the repository binding.'
|
|
1064
1064
|
: error.failure === 'cube-already-associated'
|
|
1065
1065
|
? 'The selected cube is already associated with another repository. Choose a different cube, or run the command from the repository already linked to that cube.'
|
|
1066
1066
|
: error.failure === 'access-denied'
|
package/src/inbox-monitor.ts
CHANGED
|
@@ -42,6 +42,12 @@ const ENTRY_LINE_RE =
|
|
|
42
42
|
/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\S*)\s+(\S+)\s+\(([^)]+)\):\s*(.*)$/;
|
|
43
43
|
|
|
44
44
|
export const RECENT_EMITTED_LINE_CAP = 1024;
|
|
45
|
+
/**
|
|
46
|
+
* Allow a small amount of server/host clock skew at monitor arm. Lines older
|
|
47
|
+
* than this boundary are historical materialized inbox content, not live
|
|
48
|
+
* activity for this monitor instance.
|
|
49
|
+
*/
|
|
50
|
+
export const INBOX_ARM_TIMESTAMP_SKEW_MS = 5_000;
|
|
45
51
|
|
|
46
52
|
export class RecentLineDeduper {
|
|
47
53
|
private readonly seen = new Set<string>();
|
|
@@ -86,13 +92,35 @@ export function formatEventLine(inboxLine: string): string | null {
|
|
|
86
92
|
return `${label} (${role}): ${summary}`;
|
|
87
93
|
}
|
|
88
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Return whether an inbox entry is fresh enough to have been produced after
|
|
97
|
+
* this monitor armed. A malformed timestamp fails closed when this guard is
|
|
98
|
+
* active; callers without an arm boundary retain the legacy parser behavior.
|
|
99
|
+
*/
|
|
100
|
+
export function isInboxLineAtOrAfterArm(
|
|
101
|
+
inboxLine: string,
|
|
102
|
+
armTimeMs: number,
|
|
103
|
+
skewMs = INBOX_ARM_TIMESTAMP_SKEW_MS,
|
|
104
|
+
): boolean {
|
|
105
|
+
const match = ENTRY_LINE_RE.exec(inboxLine);
|
|
106
|
+
if (!match || !Number.isFinite(armTimeMs) || !Number.isFinite(skewMs) || skewMs < 0) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
const timestampMs = Date.parse(match[1]);
|
|
110
|
+
return Number.isFinite(timestampMs) && timestampMs >= armTimeMs - skewMs;
|
|
111
|
+
}
|
|
112
|
+
|
|
89
113
|
export function formatFreshEventLine(
|
|
90
114
|
inboxLine: string,
|
|
91
115
|
deduper: RecentLineDeduper,
|
|
92
116
|
includeWakeMessage = false,
|
|
117
|
+
armTimeMs?: number,
|
|
93
118
|
): string | null {
|
|
94
119
|
const pretty = formatEventLine(inboxLine);
|
|
95
120
|
if (pretty === null) return null;
|
|
121
|
+
if (armTimeMs !== undefined && !isInboxLineAtOrAfterArm(inboxLine, armTimeMs)) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
96
124
|
return deduper.remember(inboxLine)
|
|
97
125
|
? includeWakeMessage ? formatCubeActivityWakeMessage(pretty) : pretty
|
|
98
126
|
: null;
|
|
@@ -887,6 +915,7 @@ function main(): void {
|
|
|
887
915
|
// wire behavior is identical — only the per-line projection changes.
|
|
888
916
|
// `-n 0` skips backfilling history so fresh sessions don't replay
|
|
889
917
|
// old entries on every restart.
|
|
918
|
+
const monitorArmTimeMs = Date.now();
|
|
890
919
|
const deduper = new RecentLineDeduper();
|
|
891
920
|
seedDeduperFromInboxTail(inboxPath, deduper);
|
|
892
921
|
|
|
@@ -918,7 +947,7 @@ function main(): void {
|
|
|
918
947
|
|
|
919
948
|
const rl = createInterface({ input: tail.stdout, crlfDelay: Infinity });
|
|
920
949
|
rl.on('line', (line) => {
|
|
921
|
-
const pretty = formatFreshEventLine(line, deduper, true);
|
|
950
|
+
const pretty = formatFreshEventLine(line, deduper, true, monitorArmTimeMs);
|
|
922
951
|
if (pretty !== null) {
|
|
923
952
|
console.log(pretty);
|
|
924
953
|
// Delivered → the tail is current; re-anchor the offset to the live
|
package/src/log-stream.ts
CHANGED
|
@@ -739,7 +739,10 @@ export async function streamOnce(
|
|
|
739
739
|
// Local mirror of the resume cursor, updated AFTER each successful
|
|
740
740
|
// disk write (or dedup-recognized replay). Heartbeat-hwm comparison
|
|
741
741
|
// reads this value.
|
|
742
|
-
|
|
742
|
+
// Seed it from the persisted cursor as well as the in-memory Last-Event-ID:
|
|
743
|
+
// a reconnect after process restart must not treat the cursor's durable
|
|
744
|
+
// horizon as unknown while catchup is being replayed.
|
|
745
|
+
let lastPersistedEventId: string | null = cursor?.id ?? lastEventId;
|
|
743
746
|
let lastBroadcastHwm: BroadcastHwm | null = null;
|
|
744
747
|
let pendingHwmDivergence:
|
|
745
748
|
| { hwm: BroadcastHwm; timer: NodeJS.Timeout }
|
|
@@ -769,7 +772,9 @@ export async function streamOnce(
|
|
|
769
772
|
// a created_at would itself widen the next reconnect's window (the very
|
|
770
773
|
// storm this fixes). Proven older-or-equal events are still recorded in
|
|
771
774
|
// recentIds for dedup but do not move the cursor or re-fire onEventId.
|
|
772
|
-
let lastPersistedHwm: BroadcastHwm | null =
|
|
775
|
+
let lastPersistedHwm: BroadcastHwm | null = cursor
|
|
776
|
+
? { id: cursor.id, created_at: cursor.created_at }
|
|
777
|
+
: null;
|
|
773
778
|
const markEventPersisted = (id: string, createdAt: string) => {
|
|
774
779
|
const next: BroadcastHwm = { id, created_at: createdAt };
|
|
775
780
|
if (
|
|
@@ -842,16 +847,36 @@ export async function streamOnce(
|
|
|
842
847
|
const line = formatInboxLine(withSseEventId(ev.data, ev.id));
|
|
843
848
|
const deliveryId = ev.wake_nonce ?? ev.id;
|
|
844
849
|
const isReping = ev.wake_nonce !== undefined;
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
|
|
850
|
+
const eventHwm = ev.data?.created_at
|
|
851
|
+
? { id: ev.id, created_at: ev.data.created_at }
|
|
852
|
+
: null;
|
|
853
|
+
const atOrBeforeResumeCursor = Boolean(
|
|
854
|
+
isCatchingUp &&
|
|
855
|
+
lastPersistedHwm &&
|
|
856
|
+
eventHwm &&
|
|
857
|
+
compareBroadcastHwm(eventHwm, lastPersistedHwm) <= 0
|
|
858
|
+
);
|
|
859
|
+
// The persisted stream cursor is the authoritative catchup horizon. Only
|
|
860
|
+
// fall back to file-content inspection when there is no comparable cursor
|
|
861
|
+
// (legacy/no-cursor sessions), or for a nonce reping whose durable line is
|
|
862
|
+
// intentionally reused while its wake identity is delivered again.
|
|
863
|
+
const consultInboxFile = ev.wake_nonce !== undefined ||
|
|
864
|
+
(isCatchingUp && lastPersistedHwm === null);
|
|
865
|
+
const alreadyPersisted = atOrBeforeResumeCursor || (
|
|
866
|
+
consultInboxFile &&
|
|
867
|
+
// gh#441: pass the rendered line so the fallback dedup can also
|
|
868
|
+
// recognize LEGACY (no-entry_id-prefix) on-disk lines, not just the
|
|
869
|
+
// [entry_id:] marker.
|
|
848
870
|
await hasInboxEntryId(active.cubeId, active.droneId, ev.id, line)
|
|
849
871
|
);
|
|
850
872
|
if (alreadyPersisted) {
|
|
851
|
-
//
|
|
852
|
-
//
|
|
853
|
-
//
|
|
854
|
-
|
|
873
|
+
// An ordinary entry at/before the persisted cursor is already delivered
|
|
874
|
+
// and must not re-enter the OpenCode queue. Re-pings are the explicit
|
|
875
|
+
// exception: each wake nonce is a new delivery identity even when the
|
|
876
|
+
// underlying inbox line is already durable.
|
|
877
|
+
if (isReping) {
|
|
878
|
+
await injectOpenCode(formatOpenCodeWakePrompt(line, ev.wake_nonce), deliveryId, true);
|
|
879
|
+
}
|
|
855
880
|
markEventPersisted(ev.id, ev.data?.created_at ?? '');
|
|
856
881
|
return 'persisted-skip';
|
|
857
882
|
}
|
package/src/remote-client.ts
CHANGED
|
@@ -80,6 +80,11 @@ export interface RemoteConnection {
|
|
|
80
80
|
// retryAfter can't wedge the call.
|
|
81
81
|
const RATE_LIMIT_MAX_RETRIES = 3;
|
|
82
82
|
const RATE_LIMIT_MAX_WAIT_MS = 60_000; // cap a single Retry-After honor
|
|
83
|
+
const UNREAD_CURSOR_MAX_TRANSPORT_RETRIES = 1;
|
|
84
|
+
// Replay is opt-in: most local requests are mutations or have ambiguous
|
|
85
|
+
// delivery, while unread-log reads carry an explicit cursor and are safe to
|
|
86
|
+
// repeat with the exact same request body.
|
|
87
|
+
type AuthedFetchRetryMode = 'unread-cursor';
|
|
83
88
|
export const LOCAL_SERVER_RESPONSE_LIMIT_BYTES = 32 * 1024 * 1024;
|
|
84
89
|
// A typed auth-error envelope is tiny; anything larger is hostile and the
|
|
85
90
|
// bounded read throws → the 401 fails closed to non-destructive CREDENTIAL_REJECTED.
|
|
@@ -179,6 +184,25 @@ export async function retryOn429(
|
|
|
179
184
|
return response;
|
|
180
185
|
}
|
|
181
186
|
|
|
187
|
+
function isConnectionReset(error: unknown): boolean {
|
|
188
|
+
let candidate: unknown = error;
|
|
189
|
+
for (let depth = 0; depth < 2; depth += 1) {
|
|
190
|
+
if (candidate === null || typeof candidate !== 'object') return false;
|
|
191
|
+
const typed = candidate as { code?: unknown; cause?: unknown };
|
|
192
|
+
if (typed.code === 'ECONNRESET') return true;
|
|
193
|
+
candidate = typed.cause;
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function unreadLogTransportFailure(cause: unknown): BorgServerUnreachableError {
|
|
199
|
+
return new BorgServerUnreachableError(
|
|
200
|
+
'Borg could not complete the unread log read after one automatic retry. ' +
|
|
201
|
+
'The request may have reached the server; repeat `borg_read-log unread_only=true` until caught up.',
|
|
202
|
+
{ cause },
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
182
206
|
async function localAuthorityContext(
|
|
183
207
|
sessionToken: string,
|
|
184
208
|
apiUrl: string,
|
|
@@ -271,6 +295,7 @@ async function localServerRequest<T>(
|
|
|
271
295
|
path: string,
|
|
272
296
|
method: 'GET' | 'POST' | 'PUT' | 'PATCH',
|
|
273
297
|
payload?: Record<string, unknown>,
|
|
298
|
+
options: { retryMode?: AuthedFetchRetryMode } = {},
|
|
274
299
|
): Promise<T | null> {
|
|
275
300
|
return decodeLocalProtocolResponse<T>((signal) => authedFetch(path, {
|
|
276
301
|
method,
|
|
@@ -286,6 +311,7 @@ async function localServerRequest<T>(
|
|
|
286
311
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
287
312
|
body: JSON.stringify(createProtocolEnvelope(randomUUID(), payload)),
|
|
288
313
|
}),
|
|
314
|
+
retryMode: options.retryMode,
|
|
289
315
|
}), true);
|
|
290
316
|
}
|
|
291
317
|
|
|
@@ -492,7 +518,11 @@ function localCursorBinding(active: ActiveCube) {
|
|
|
492
518
|
|
|
493
519
|
async function localReadLogPage(
|
|
494
520
|
active: ActiveCube,
|
|
495
|
-
opts: {
|
|
521
|
+
opts: {
|
|
522
|
+
cursor?: LocalServerCursor | null;
|
|
523
|
+
limit?: number;
|
|
524
|
+
retryMode?: AuthedFetchRetryMode;
|
|
525
|
+
} = {},
|
|
496
526
|
): Promise<any> {
|
|
497
527
|
const payload = await localServerRequest<any>(
|
|
498
528
|
active,
|
|
@@ -502,6 +532,7 @@ async function localReadLogPage(
|
|
|
502
532
|
cursor: opts.cursor ?? null,
|
|
503
533
|
...(opts.limit === undefined ? {} : { limit: opts.limit }),
|
|
504
534
|
},
|
|
535
|
+
{ retryMode: opts.retryMode },
|
|
505
536
|
);
|
|
506
537
|
if (!payload) throw new Error('Local Borg server returned an empty log response');
|
|
507
538
|
return payload;
|
|
@@ -627,6 +658,7 @@ async function authedFetch(
|
|
|
627
658
|
authToken?: string;
|
|
628
659
|
serverTrustIdentity?: string;
|
|
629
660
|
localSessionCredentialRef?: string;
|
|
661
|
+
retryMode?: AuthedFetchRetryMode;
|
|
630
662
|
} = {}
|
|
631
663
|
): Promise<Response> {
|
|
632
664
|
const {
|
|
@@ -635,6 +667,7 @@ async function authedFetch(
|
|
|
635
667
|
authToken,
|
|
636
668
|
serverTrustIdentity: suppliedTrustIdentity,
|
|
637
669
|
localSessionCredentialRef,
|
|
670
|
+
retryMode,
|
|
638
671
|
headers,
|
|
639
672
|
...rest
|
|
640
673
|
} = init;
|
|
@@ -697,7 +730,35 @@ async function authedFetch(
|
|
|
697
730
|
return res;
|
|
698
731
|
};
|
|
699
732
|
|
|
700
|
-
|
|
733
|
+
let transportRetriesRemaining = retryMode === 'unread-cursor'
|
|
734
|
+
? UNREAD_CURSOR_MAX_TRANSPORT_RETRIES
|
|
735
|
+
: 0;
|
|
736
|
+
const requestWithRetry = async (): Promise<Response> => {
|
|
737
|
+
try {
|
|
738
|
+
return await buildRequest(token);
|
|
739
|
+
} catch (error) {
|
|
740
|
+
if (retryMode !== 'unread-cursor' || !isConnectionReset(error)) throw error;
|
|
741
|
+
if (transportRetriesRemaining === 0) throw unreadLogTransportFailure(error);
|
|
742
|
+
transportRetriesRemaining -= 1;
|
|
743
|
+
debugLog('↻ retrying unread log read after ECONNRESET');
|
|
744
|
+
try {
|
|
745
|
+
return await buildRequest(token);
|
|
746
|
+
} catch (retryError) {
|
|
747
|
+
if (isConnectionReset(retryError)) throw unreadLogTransportFailure(retryError);
|
|
748
|
+
throw retryError;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
let response = await requestWithRetry();
|
|
754
|
+
let rateLimitRetryExhausted = false;
|
|
755
|
+
if (retryMode === 'unread-cursor') {
|
|
756
|
+
response = await retryOn429(response, requestWithRetry, {
|
|
757
|
+
sleep,
|
|
758
|
+
log: debugLog,
|
|
759
|
+
});
|
|
760
|
+
rateLimitRetryExhausted = response.status === 429;
|
|
761
|
+
}
|
|
701
762
|
|
|
702
763
|
if (response.status === 401) {
|
|
703
764
|
// Reached only after pinned-TLS trust is verified (localAuthorityContext
|
|
@@ -805,11 +866,14 @@ async function authedFetch(
|
|
|
805
866
|
if (localSessionCredentialRef !== undefined) markSeatRejected(localSessionCredentialRef);
|
|
806
867
|
throw new CubeDeletedError();
|
|
807
868
|
}
|
|
869
|
+
const retryGuidance = rateLimitRetryExhausted
|
|
870
|
+
? ' Repeat `borg_read-log unread_only=true` until caught up.'
|
|
871
|
+
: '';
|
|
808
872
|
throw new BorgServerHttpError(
|
|
809
873
|
response.status,
|
|
810
874
|
serverMessage
|
|
811
|
-
? `Borg server request failed (HTTP ${response.status}): ${serverMessage}`
|
|
812
|
-
: `Borg server request failed (HTTP ${response.status})`,
|
|
875
|
+
? `Borg server request failed (HTTP ${response.status}): ${serverMessage}${retryGuidance}`
|
|
876
|
+
: `Borg server request failed (HTTP ${response.status})${retryGuidance}`,
|
|
813
877
|
code,
|
|
814
878
|
);
|
|
815
879
|
}
|
|
@@ -954,7 +1018,13 @@ export async function readLog(
|
|
|
954
1018
|
let cursor: LocalServerCursor | null = null;
|
|
955
1019
|
if (opts.unreadOnly) cursor = await getLocalServerCursor(localCursorBinding(local));
|
|
956
1020
|
if (opts.since !== undefined) cursor = await resolveLocalLogCursor(local, opts.since);
|
|
957
|
-
const page = await localReadLogPage(local, {
|
|
1021
|
+
const page = await localReadLogPage(local, {
|
|
1022
|
+
cursor,
|
|
1023
|
+
limit: opts.limit,
|
|
1024
|
+
// Keep the cursor payload stable across a lost response; do not re-read or
|
|
1025
|
+
// advance local state until one response has been decoded successfully.
|
|
1026
|
+
...(opts.unreadOnly && opts.since === undefined ? { retryMode: 'unread-cursor' as const } : {}),
|
|
1027
|
+
});
|
|
958
1028
|
if (opts.unreadOnly && page.cursor) {
|
|
959
1029
|
await advanceLocalServerCursor(localCursorBinding(local), page.cursor);
|
|
960
1030
|
}
|