atagen

malice

public
Star0
Watch0

Commit c51e1fe62291

tarball

atagen <boss@atagen.co>2026-10-25 09:09 UTC

13 files changed+23-672
history: remove obsolete repair controls
Modified file: crates/malice-core/src/feed.rs+4 additions−15 deletions
@@ -2,9 +2,9 @@ use std::collections::HashMap;
22
33 use futures_util::{Stream, StreamExt, stream};
44 use macltok::{
5- ConnectionState, EncryptionState, HistoryRepairReport, ImagePacks, MentionsPage,
6- MentionsUpdate, MirrorHandle, RoomSummary, SessionStatus, State, StateChange, TimelineChange,
7- TimelineId, TimelineItem, UploadOutcome, VerificationState, imbl::Vector,
5+ ConnectionState, EncryptionState, ImagePacks, MentionsPage, MentionsUpdate, MirrorHandle,
6+ RoomSummary, SessionStatus, State, StateChange, TimelineChange, TimelineId, TimelineItem,
7+ UploadOutcome, VerificationState, imbl::Vector,
88 };
99 use serde::Serialize;
1010
@@ -20,14 +20,10 @@ pub enum WireEvent {
2020 verification: Option<VerificationState>,
2121 image_packs: ImagePacks,
2222 mentions: Option<MentionsPage>,
23- repairs: HashMap<String, HistoryRepairReport>,
2423 },
2524 Mentions {
2625 update: MentionsUpdate,
2726 },
28- HistoryRepair {
29- report: HistoryRepairReport,
30- },
3127 ImagePacks {
3228 packs: ImagePacks,
3329 },
@@ -119,11 +115,6 @@ pub fn snapshot_event(state: &State) -> WireEvent {
119115 verification: state.verification.clone(),
120116 image_packs: state.image_packs.clone(),
121117 mentions: state.mentions.clone(),
122- repairs: state
123- .repairs
124- .iter()
125- .map(|(room_id, report)| (room_id.as_str().to_owned(), report.clone()))
126- .collect(),
127118 }
128119 }
129120
@@ -193,9 +184,6 @@ pub fn wire_events(change: &StateChange) -> Vec<WireEvent> {
193184 StateChange::Mentions(update) => vec![WireEvent::Mentions {
194185 update: update.clone(),
195186 }],
196- StateChange::HistoryRepair(report) => vec![WireEvent::HistoryRepair {
197- report: report.clone(),
198- }],
199187 StateChange::Failed(failure) => vec![error(&failure.message)],
200188 StateChange::SessionSaveFailed(_) => {
201189 vec![error(
@@ -223,6 +211,7 @@ pub fn wire_events(change: &StateChange) -> Vec<WireEvent> {
223211 | StateChange::VerificationTarget(_)
224212 | StateChange::NotificationSettings
225213 | StateChange::History { .. }
214+ | StateChange::HistoryRepair(_)
226215 | StateChange::Typing(_) => vec![],
227216 }
228217 }
Modified file: crates/malice-core/src/request.rs+5 additions−54 deletions
@@ -1,9 +1,8 @@
11 use macltok::ruma::{OwnedTransactionId, events::GlobalAccountDataEventType, serde::Raw};
22 use macltok::{
3- AttachmentUpload, ClientHandle, CreateRoomOptions, HistoryRepairBudget, HistoryRepairPolicy,
4- ImagePackSource, MediaId, MediaSize, MemberFilter, MentionKey, MentionsBoundary,
5- MessageContent, MessageEditContent, MirrorHandle, PackImageInfo, PackUsage, Patch, RichText,
6- TimelineId, VerificationAction, VerificationTarget,
3+ AttachmentUpload, ClientHandle, CreateRoomOptions, ImagePackSource, MediaId, MediaSize,
4+ MemberFilter, MentionKey, MentionsBoundary, MessageContent, MessageEditContent, MirrorHandle,
5+ PackImageInfo, PackUsage, Patch, RichText, TimelineId, VerificationAction, VerificationTarget,
76 };
87 use serde::Deserialize;
98 use serde_json::json;
@@ -85,21 +84,6 @@ pub enum Request {
8584 #[serde(default)]
8685 room_id: String,
8786 },
88- HistoryRepair {
89- room_id: String,
90- #[serde(default)]
91- policy: Option<HistoryRepairPolicy>,
92- #[serde(default)]
93- budget: HistoryRepairBudget,
94- },
95- ContinueHistoryRepair {
96- room_id: String,
97- #[serde(default)]
98- budget: HistoryRepairBudget,
99- },
100- CancelHistoryRepair {
101- room_id: String,
102- },
10387 StartRecentHistory {
10488 room_id: String,
10589 },
@@ -107,10 +91,6 @@ pub enum Request {
10791 room_id: String,
10892 gap_id: String,
10993 },
110- HistoryRepairPolicy,
111- SetHistoryRepairPolicy {
112- policy: HistoryRepairPolicy,
113- },
11494 EventContext {
11595 #[serde(default)]
11696 room_id: String,
@@ -574,33 +554,11 @@ pub async fn dispatch(
574554 handle.paginate(TimelineId::Room(room_id), 30).await?;
575555 Ok(Reply::Done)
576556 }
577- Request::HistoryRepair {
578- room_id,
579- policy,
580- budget,
581- } => {
582- let report = handle
583- .history_repair(parse_room_id(&room_id)?, policy, budget)
584- .await?;
585- Ok(Reply::Json(json!(report)))
586- }
587- Request::ContinueHistoryRepair { room_id, budget } => {
588- let report = handle
589- .continue_history_repair(parse_room_id(&room_id)?, budget)
590- .await?;
591- Ok(Reply::Json(json!(report)))
592- }
593- Request::CancelHistoryRepair { room_id } => {
594- handle
595- .cancel_history_repair(parse_room_id(&room_id)?)
596- .await?;
597- Ok(Reply::Done)
598- }
599557 Request::StartRecentHistory { room_id } => {
600- let report = handle
558+ handle
601559 .start_recent_history(parse_room_id(&room_id)?)
602560 .await?;
603- Ok(Reply::Json(json!(report)))
561+ Ok(Reply::Done)
604562 }
605563 Request::FillHistoryGap { room_id, gap_id } => {
606564 if gap_id.is_empty() {
@@ -611,13 +569,6 @@ pub async fn dispatch(
611569 .await?;
612570 Ok(Reply::Done)
613571 }
614- Request::HistoryRepairPolicy => {
615- Ok(Reply::Json(json!(handle.history_repair_policy().await?)))
616- }
617- Request::SetHistoryRepairPolicy { policy } => {
618- handle.set_history_repair_policy(policy).await?;
619- Ok(Reply::Done)
620- }
621572 Request::EventContext {
622573 room_id,
623574 event_id,
Modified file: crates/malice-server/src/routes.rs−5 deletions
@@ -565,13 +565,8 @@ async fn post_command(
565565 | Request::SetMentionsRead { .. }
566566 | Request::MarkMentionsReadThrough { .. }
567567 | Request::RecoverMentions
568- | Request::HistoryRepair { .. }
569- | Request::ContinueHistoryRepair { .. }
570- | Request::CancelHistoryRepair { .. }
571568 | Request::StartRecentHistory { .. }
572569 | Request::FillHistoryGap { .. }
573- | Request::HistoryRepairPolicy
574- | Request::SetHistoryRepairPolicy { .. }
575570 | Request::Kick { .. }
576571 | Request::Ban { .. }
577572 | Request::Unban { .. }
Modified file: web/src/App.svelte+4 additions−147 deletions
@@ -4,8 +4,6 @@
44 import LoginPanel from './lib/LoginPanel.svelte';
55 import MessageRow from './lib/MessageRow.svelte';
66 import MentionsInbox from './lib/MentionsInbox.svelte';
7- import HistoryRepairPanel from './lib/HistoryRepairPanel.svelte';
8- import type { HistoryRepairPolicy, HistoryRepairReport } from './lib/repair-model';
97 import MxcAvatar from './lib/MxcAvatar.svelte';
108 import PinButton from './lib/PinButton.svelte';
119 import ContextMenu, { type MenuItem } from './lib/ContextMenu.svelte';
@@ -32,10 +30,6 @@
3230 getRooms,
3331 getSession,
3432 getSpaceChildren,
35- historyRepair,
36- continueHistoryRepair,
37- cancelHistoryRepair,
38- startRecentHistory,
3933 fillHistoryGap,
4034 joinRoom,
4135 leaveRoom,
@@ -72,8 +66,6 @@
7266 pushBanner,
7367 markTimelineOpened,
7468 imagePacks,
75- historyRepairs,
76- publishHistoryRepair,
7769 resetClientState,
7870 rooms,
7971 selectedRoomId,
@@ -187,116 +179,6 @@
187179 let spaceChildrenError: string | null = null;
188180 let avatarByUser: Record<string, string | null> = {};
189181 let restoringRoute = false;
190- let repairPanels: Record<string, boolean> = {};
191- let repairBusy: Record<string, boolean> = {};
192- let repairCancelling: Record<string, boolean> = {};
193- let repairErrors: Record<string, string | null> = {};
194- let repairJumps: Record<string, { previous: HistoryRepairReport | null }> = {};
195- let repairAccount: string | null | undefined;
196- let repairEpoch = 0;
197- $: if ($sessionUser !== repairAccount) {
198- repairAccount = $sessionUser;
199- repairEpoch++;
200- repairPanels = {};
201- repairBusy = {};
202- repairCancelling = {};
203- repairErrors = {};
204- repairJumps = {};
205- }
206- $: currentRepair = $selectedRoomId ? $historyRepairs[$selectedRoomId] ?? null : null;
207- $: currentRepairActive = !!($selectedRoomId && repairBusy[$selectedRoomId]) || repairActive(currentRepair);
208- $: maybeJumpAfterRepair($selectedRoomId, currentRepair, repairJumps);
209- $: settleRepairCancellation($historyRepairs, repairBusy);
210-
211- function repairActive(report: HistoryRepairReport | null | undefined): boolean {
212- return report?.phase === 'FetchingRecent' || report?.phase === 'Bridging';
213- }
214-
215- function settleRepairCancellation(reports: Record<string, HistoryRepairReport>, busy: Record<string, boolean>): void {
216- for (const [roomId, report] of Object.entries(reports)) {
217- if (repairCancelling[roomId] && !busy[roomId] && !repairActive(report)) {
218- repairCancelling = { ...repairCancelling, [roomId]: false };
219- }
220- }
221- }
222-
223- function maybeJumpAfterRepair(roomId: string | null, report: HistoryRepairReport | null, jumps: typeof repairJumps): void {
224- if (!roomId || roomId !== $selectedRoomId || !report?.recent_available || !jumps[roomId] || report === jumps[roomId].previous) return;
225- const next = { ...repairJumps };
226- delete next[roomId];
227- repairJumps = next;
228- stickToBottom = true;
229- const epoch = repairEpoch;
230- void tick().then(() => {
231- if (epoch === repairEpoch && $selectedRoomId === roomId) scrollToBottom();
232- });
233- }
234-
235- async function showHistoryRepair(roomId: string): Promise<void> {
236- if ($selectedRoomId !== roomId) await selectRoom(roomId);
237- if ($selectedRoomId === roomId) repairPanels = { ...repairPanels, [roomId]: true };
238- }
239-
240- async function runHistoryRepair(roomId: string, action: 'start' | 'continue' | 'recent', policy: HistoryRepairPolicy | null = null): Promise<void> {
241- if (!$sessionUser || repairBusy[roomId] || repairActive($historyRepairs[roomId]) || repairCancelling[roomId]) return;
242- const account = $sessionUser;
243- const epoch = repairEpoch;
244- repairPanels = { ...repairPanels, [roomId]: true };
245- repairBusy = { ...repairBusy, [roomId]: true };
246- repairErrors = { ...repairErrors, [roomId]: null };
247- if (action !== 'continue') repairJumps = { ...repairJumps, [roomId]: { previous: $historyRepairs[roomId] ?? null } };
248- try {
249- if (!openReady[roomId]) {
250- await openRoom(roomId);
251- if ($sessionUser !== account || epoch !== repairEpoch) return;
252- openReady = { ...openReady, [roomId]: true };
253- const failures = { ...openFailed };
254- delete failures[roomId];
255- openFailed = failures;
256- touchWarmRoom(roomId);
257- }
258- if (repairCancelling[roomId]) return;
259- const report = action === 'continue' ? await continueHistoryRepair(roomId)
260- : action === 'recent' ? await startRecentHistory(roomId) : await historyRepair(roomId, policy);
261- if ($sessionUser !== account || epoch !== repairEpoch) return;
262- if (report.room_id !== roomId) throw new Error('History repair returned a different room');
263- publishHistoryRepair(report, account);
264- maybeJumpAfterRepair(roomId, report, repairJumps);
265- } catch (cause) {
266- if ($sessionUser === account && epoch === repairEpoch) {
267- repairErrors = { ...repairErrors, [roomId]: cause instanceof Error ? cause.message : 'Could not repair history' };
268- }
269- } finally {
270- if ($sessionUser === account && epoch === repairEpoch) {
271- repairBusy = { ...repairBusy, [roomId]: false };
272- repairCancelling = { ...repairCancelling, [roomId]: false };
273- if (repairJumps[roomId]) {
274- const next = { ...repairJumps };
275- delete next[roomId];
276- repairJumps = next;
277- }
278- }
279- }
280- }
281-
282- async function cancelRoomRepair(roomId: string): Promise<void> {
283- if (!$sessionUser || repairCancelling[roomId]) return;
284- const account = $sessionUser;
285- const epoch = repairEpoch;
286- repairCancelling = { ...repairCancelling, [roomId]: true };
287- try {
288- await cancelHistoryRepair(roomId);
289- if ($sessionUser === account && epoch === repairEpoch && !repairBusy[roomId] && !repairActive($historyRepairs[roomId])) {
290- repairCancelling = { ...repairCancelling, [roomId]: false };
291- }
292- } catch (cause) {
293- if ($sessionUser === account && epoch === repairEpoch) {
294- repairCancelling = { ...repairCancelling, [roomId]: false };
295- repairErrors = { ...repairErrors, [roomId]: cause instanceof Error ? cause.message : 'Could not cancel history repair' };
296- }
297- }
298- }
299-
300182 function motionDuration(ms: number): number {
301183 return typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : ms;
302184 }
@@ -781,11 +663,6 @@
781663
782664 function noteUserScroll(): void {
783665 timelineViewport?.releaseScroll();
784- if ($selectedRoomId && repairJumps[$selectedRoomId]) {
785- const next = { ...repairJumps };
786- delete next[$selectedRoomId];
787- repairJumps = next;
788- }
789666 }
790667
791668 function syncComposer(node: HTMLTextAreaElement) {
@@ -1177,11 +1054,10 @@
11771054 let pinsExpanded = false;
11781055 let pinPreview: TimelineEvent | null = null;
11791056 let pinJumpBusy = false;
1180- $: syncPinned(selectedRoom?.room_id ?? null, (selectedRoom?.pinned_events.length ?? 0) > 0, $selectedRoomId ? $pinnedTimelineVersions[$selectedRoomId] ?? 0 : 0, currentRepairActive);
1181- function syncPinned(roomId: string | null, hasPins: boolean, version: number, repairing: boolean): void {
1057+ $: syncPinned(selectedRoom?.room_id ?? null, (selectedRoom?.pinned_events.length ?? 0) > 0, $selectedRoomId ? $pinnedTimelineVersions[$selectedRoomId] ?? 0 : 0);
1058+ function syncPinned(roomId: string | null, hasPins: boolean, version: number): void {
11821059 const want = hasPins ? roomId : null;
11831060 if (want === pinnedOpenFor && version === pinnedCloseVersion) return;
1184- if (want && repairing) return;
11851061 if (pinnedOpenFor) runCommand({ op: 'close_pinned', room_id: pinnedOpenFor }).catch(() => undefined);
11861062 pinnedOpenFor = want;
11871063 pinnedCloseVersion = version;
@@ -1482,7 +1358,7 @@
14821358
14831359 async function paginate(): Promise<void> {
14841360 const roomId = $selectedRoomId;
1485- if (!roomId || paginateBusy || currentRepairActive) return;
1361+ if (!roomId || paginateBusy) return;
14861362 paginateBusy = true;
14871363 try {
14881364 await paginateRoom(roomId);
@@ -1746,9 +1622,6 @@
17461622 items.push({ label: room.favourite ? 'Unpin' : room.space ? 'Pin to top' : 'Pin', action: () => void togglePin(room) });
17471623 items.push({ label: 'Copy link', action: () => copyText(`https://matrix.to/#/${room.canonical_alias ?? room.room_id}`) });
17481624 items.push({ label: 'Copy room ID', action: () => copyText(room.room_id) });
1749- if (!room.space && room.membership === 'Joined') {
1750- items.push({ label: 'Repair message history', action: () => void showHistoryRepair(room.room_id) });
1751- }
17521625 if (room.membership === 'Joined') items.push({ label: 'Leave', danger: true, divider: true, action: () => void leave(room.room_id) });
17531626 return items;
17541627 }
@@ -2129,9 +2002,6 @@
21292002 {#if roomMenuOpen}
21302003 <div class="room-menu" role="menu">
21312004 {#if selectedRoom.membership === 'Joined'}
2132- <button type="button" role="menuitem" on:click={() => roomMenuAction(() => void showHistoryRepair(selectedRoom.room_id))}>
2133- Repair message history
2134- </button>
21352005 <button
21362006 type="button"
21372007 role="menuitem"
@@ -2484,19 +2354,6 @@
24842354 </p>
24852355 </div>
24862356 {:else}
2487- {#if repairPanels[selectedRoom.room_id] ?? !!currentRepair}
2488- <HistoryRepairPanel
2489- report={currentRepair}
2490- busy={!!repairBusy[selectedRoom.room_id]}
2491- cancelling={!!repairCancelling[selectedRoom.room_id]}
2492- error={repairErrors[selectedRoom.room_id] ?? null}
2493- onStart={(policy) => void runHistoryRepair(selectedRoom.room_id, 'start', policy)}
2494- onCancel={() => void cancelRoomRepair(selectedRoom.room_id)}
2495- onContinue={() => void runHistoryRepair(selectedRoom.room_id, 'continue')}
2496- onRecent={() => void runHistoryRepair(selectedRoom.room_id, 'recent')}
2497- onClose={() => (repairPanels = { ...repairPanels, [selectedRoom.room_id]: false })}
2498- />
2499- {/if}
25002357 {#if pins.length}
25012358 <div class="pinned-strip" class:open={pinsExpanded}>
25022359 <div class="pinned-head">
@@ -2600,7 +2457,7 @@
26002457 {#if hasStart}
26012458 <p class="start-note">Beginning of history</p>
26022459 {:else}
2603- <button type="button" class="ghost-btn" disabled={paginateBusy || currentRepairActive} on:click={() => void paginate()}>
2460+ <button type="button" class="ghost-btn" disabled={paginateBusy} on:click={() => void paginate()}>
26042461 {paginateBusy ? 'Loading…' : 'Load older messages'}
26052462 </button>
26062463 {/if}
Deleted file: web/src/lib/HistoryRepairPanel.svelte−148 deletions
@@ -1,148 +0,0 @@
1-<script lang="ts">
2- import EmojiText from './EmojiText.svelte';
3- import type { HistoryRepairPolicy, HistoryRepairReport } from './repair-model';
4-
5- export let report: HistoryRepairReport | null = null;
6- export let busy = false;
7- export let cancelling = false;
8- export let error: string | null = null;
9- export let onStart: (policy: HistoryRepairPolicy | null) => void;
10- export let onCancel: () => void;
11- export let onContinue: () => void;
12- export let onRecent: () => void;
13- export let onClose: () => void;
14-
15- let expanded = true;
16- let choice: 'saved' | 'StartRecent' | 'Bridge' | 'BridgeShortGaps' = 'saved';
17- let threshold: number | undefined = 15000;
18-
19- $: active = busy || cancelling || report?.phase === 'FetchingRecent' || report?.phase === 'Bridging';
20- $: canRepair = choice !== 'BridgeShortGaps' || (threshold !== undefined && Number.isSafeInteger(threshold) && threshold >= 0);
21- $: serverFailure = serverError(report?.failure?.server);
22- $: paused = !active && report?.phase === 'Paused';
23- $: canContinue = paused && report?.strategy === 'Bridge';
24-
25- function serverError(value: unknown): string {
26- if (!value || typeof value !== 'object' || Array.isArray(value)) return '';
27- const server = value as Record<string, unknown>;
28- const status = server.status;
29- const code = server.errcode;
30- const message = server.message;
31- if ((typeof status !== 'number' || !Number.isFinite(status)) && typeof code !== 'string' && typeof message !== 'string') return '';
32- const parts = [typeof status === 'number' ? `HTTP ${status}` : '', typeof code === 'string' ? code : '', typeof message === 'string' ? message : ''];
33- return parts.filter(Boolean).join(' · ');
34- }
35-
36- function policy(): HistoryRepairPolicy | null {
37- if (choice === 'saved') return null;
38- if (choice === 'BridgeShortGaps') return { BridgeShortGaps: { threshold_ms: threshold! } };
39- return choice;
40- }
41-
42- function stopLabel(value: NonNullable<HistoryRepairReport['stop']>): string {
43- const labels: Record<string, string> = {
44- Budget: 'Paused at the repair budget',
45- Cancelled: 'Repair cancelled',
46- NoAnchor: 'No retained history anchor was found',
47- HistoryUnavailable: 'History is unavailable',
48- CursorStalled: 'Repair made no progress'
49- };
50- return labels[value];
51- }
52-
53- function continuityLabel(value: HistoryRepairReport['continuity']): string {
54- if (value === 'Unresolved') return 'Continuity unresolved';
55- if (value === 'Verified') return 'Continuity verified';
56- if (value === 'AccessibleStart') return 'Beginning of accessible history reached';
57- return value;
58- }
59-
60- function start(): void {
61- if (canRepair) onStart(policy());
62- }
63-</script>
64-
65-<section class="repair" aria-label="History repair">
66- <div class="banner">
67- <button type="button" class="summary" aria-expanded={expanded} on:click={() => expanded = !expanded}>
68- <span>History repair</span>
69- {#if active}<span class="muted">{cancelling ? 'Cancelling…' : report?.phase === 'Bridging' ? 'Bridging history…' : 'Loading recent messages…'}</span>{/if}
70- {#if report?.recent_available === true}<span class="recent">Recent messages available</span>
71- {:else if report?.recent_available === false}<span class="muted">Recent messages unavailable</span>
72- {:else}<span class="muted">{active ? 'Repair in progress' : 'History recovery'}</span>{/if}
73- {#if report?.recent_available === true && report.continuity === 'Unresolved'}<span class="muted">Continuity unresolved</span>{/if}
74- <span aria-hidden="true">{expanded ? '▴' : '▾'}</span>
75- </button>
76- <button type="button" class="close" aria-label="Close history repair" on:click={onClose}>✕</button>
77- </div>
78- {#if expanded}
79- <div class="details" aria-live="polite">
80- {#if report}
81- <p class="state">{#if active}{report.phase === 'FetchingRecent' ? 'Loading recent messages' : 'Bridging retained history'}{:else if report.recent_available}Recent messages are available{:else}Recent messages could not be restored{/if}</p>
82- <p>{continuityLabel(report.continuity)}{report.continuity === 'Unresolved' ? '. The missing interval has not been linked to retained history.' : '.'}</p>
83- <p>{report.strategy === 'Recent' ? 'Recent history' : report.strategy === 'Overlap' ? 'Retained overlap' : 'Bridge progress'} · {report.pages} pages and {report.events} events</p>
84- {#if report.coverage_duration_ms !== null}<p>Gap duration {report.coverage_duration_ms} ms</p>{/if}
85- {#if report.stop}<p>{stopLabel(report.stop)}</p>{/if}
86- {#if report.phase === 'Paused' && report.strategy === 'Bridge'}<p class="muted">Keep this room open to continue bridging. Closing it or restarting the app clears unfinished progress.</p>{/if}
87- {#if report.failure}
88- <p class="failure"><EmojiText text={report.failure.message} /></p>
89- {#if serverFailure}<p class="server-error"><EmojiText text={serverFailure} /></p>{/if}
90- {/if}
91- {#if !report.threshold_evaluated && typeof report.requested_policy !== 'string'}
92- <p>The requested threshold of {report.requested_policy.BridgeShortGaps.threshold_ms} ms was not evaluated. Recent messages were used.</p>
93- {/if}
94- {/if}
95- {#if error}<p class="failure" role="alert"><EmojiText text={error} /></p>{/if}
96- {#if !active}
97- <label class="field">Repair policy
98- <select bind:value={choice}>
99- <option value="saved">Use saved policy</option>
100- <option value="StartRecent">Start recent</option>
101- <option value="Bridge">Bridge retained history</option>
102- <option value="BridgeShortGaps">Bridge short gaps</option>
103- </select>
104- </label>
105- {#if choice === 'BridgeShortGaps'}
106- <label class="field">Threshold in milliseconds
107- <input type="number" min="0" step="1" bind:value={threshold} />
108- </label>
109- <p class="muted">The threshold cannot yet be evaluated, so this currently starts at recent messages. A threshold of zero disables automatic bridging. Bridge retained history remains available.</p>
110- {/if}
111- {/if}
112- <div class="actions">
113- {#if active}
114- <button type="button" class="ghost-btn" disabled={cancelling} on:click={onCancel}>{cancelling ? 'Cancelling…' : 'Cancel repair'}</button>
115- {#if cancelling}<span role="status">Waiting for the active repair to stop</span>{/if}
116- {:else}
117- <button type="button" class="primary-btn" disabled={!canRepair} on:click={start}>Repair history</button>
118- {#if canContinue}
119- <button type="button" class="ghost-btn" on:click={onContinue}>Continue bridging</button>
120- {/if}
121- {#if paused}
122- <button type="button" class="ghost-btn" on:click={onRecent}>Use recent window</button>
123- {/if}
124- {/if}
125- <button type="button" class="ghost-btn" on:click={onClose}>Close</button>
126- </div>
127- </div>
128- {/if}
129-</section>
130-
131-<style>
132- .repair { max-height: min(42vh, 320px); overflow: auto; border: 1px solid var(--border); border-radius: var(--r-md); background: var(--panel); color: var(--text); }
133- .banner { display: flex; align-items: center; gap: 6px; padding: 6px; }
134- .summary { display: flex; flex: 1; min-width: 0; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px; padding: 5px 7px; border: 0; background: transparent; color: inherit; text-align: left; cursor: pointer; }
135- .summary span:first-child { font-weight: 600; }
136- .recent { color: var(--ok-text); font-size: 12px; }
137- .muted { color: var(--muted); font-size: 12px; }
138- .close { border: 0; background: transparent; color: var(--muted); cursor: pointer; }
139- .details { overflow: auto; padding: 0 12px 12px; font-size: 13px; }
140- .details p { margin: 7px 0; }
141- .state { font-weight: 600; }
142- .field { display: flex; flex-direction: column; gap: 5px; margin-top: 10px; }
143- select, input { width: 100%; min-height: var(--ctl-h); padding: 5px 7px; border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--bg-soft); color: var(--text); font: inherit; }
144- .actions { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 10px; }
145- .actions [role='status'] { color: var(--muted); }
146- .failure { color: var(--danger-text); }
147- @media (max-width: 520px) { .summary { flex-wrap: wrap; justify-content: flex-start; } }
148-</style>
Deleted file: web/src/lib/HistoryRepairSettings.svelte−103 deletions
@@ -1,103 +0,0 @@
1-<script lang="ts">
2- import { get } from 'svelte/store';
3- import { onMount } from 'svelte';
4- import { historyRepairPolicy, setHistoryRepairPolicy } from './api';
5- import { sessionUser } from './store';
6- import type { HistoryRepairPolicy } from './repair-model';
7-
8- let selected: 'StartRecent' | 'Bridge' | 'BridgeShortGaps' = 'StartRecent';
9- let threshold: number | undefined = 15000;
10- let loading = true;
11- let loaded = false;
12- let busy = false;
13- let error = '';
14- let notice = '';
15- let alive = true;
16-
17- async function loadPolicy(): Promise<void> {
18- const account = get(sessionUser);
19- if (!account) {
20- loading = false;
21- loaded = false;
22- error = 'Sign in to load the saved history repair policy.';
23- return;
24- }
25- loading = true;
26- loaded = false;
27- error = '';
28- try {
29- const policy = await historyRepairPolicy();
30- if (!alive || get(sessionUser) !== account) return;
31- if (policy === 'StartRecent' || policy === 'Bridge') selected = policy;
32- else {
33- selected = 'BridgeShortGaps';
34- threshold = policy.BridgeShortGaps.threshold_ms;
35- }
36- loaded = true;
37- } catch (cause) {
38- if (alive && get(sessionUser) === account) error = cause instanceof Error ? cause.message : 'Could not load the saved history repair policy.';
39- } finally {
40- if (alive && get(sessionUser) === account) loading = false;
41- }
42- }
43-
44- onMount(() => {
45- void loadPolicy();
46- return () => { alive = false; };
47- });
48-
49- async function save(): Promise<void> {
50- error = '';
51- notice = '';
52- const ms = threshold;
53- if (!loaded || busy || loading) return;
54- if (selected === 'BridgeShortGaps' && (ms === undefined || !Number.isSafeInteger(ms) || ms < 0)) {
55- error = 'Enter a whole number of milliseconds that is zero or greater.';
56- return;
57- }
58- const account = get(sessionUser);
59- if (!account) return;
60- const policy: HistoryRepairPolicy = selected === 'BridgeShortGaps'
61- ? { BridgeShortGaps: { threshold_ms: ms! } }
62- : selected;
63- busy = true;
64- try {
65- await setHistoryRepairPolicy(policy);
66- if (alive && get(sessionUser) === account) notice = 'History repair policy saved.';
67- } catch (cause) {
68- if (alive && get(sessionUser) === account) error = cause instanceof Error ? cause.message : 'Could not save the history repair policy.';
69- } finally {
70- if (alive && get(sessionUser) === account) busy = false;
71- }
72- }
73-</script>
74-
75-<section aria-label="History repair settings">
76- <h3 class="sec-h">History repair</h3>
77- <label class="field">Saved policy
78- <select bind:value={selected} disabled={loading || busy}>
79- <option value="StartRecent">Start recent</option>
80- <option value="Bridge">Bridge retained history</option>
81- <option value="BridgeShortGaps">Bridge short gaps</option>
82- </select>
83- </label>
84- {#if selected === 'BridgeShortGaps'}
85- <label class="field">Gap threshold in milliseconds
86- <input type="number" min="0" step="1" bind:value={threshold} disabled={loading || busy} />
87- </label>
88- <p class="note">The threshold cannot yet be evaluated, so this policy currently starts at recent messages. A threshold of zero disables automatic bridging. Choose Bridge retained history to bridge explicitly.</p>
89- {/if}
90- <button type="button" class="primary-btn" disabled={!loaded || loading || busy || (selected === 'BridgeShortGaps' && (threshold === undefined || !Number.isSafeInteger(threshold) || threshold < 0))} on:click={save}>{busy ? 'Saving…' : 'Save'}</button>
91- {#if !loaded && !loading}<button type="button" class="ghost-btn" disabled={busy} on:click={() => void loadPolicy()}>Retry loading saved policy</button>{/if}
92- {#if error}<p class="error" role="alert">{error}</p>{/if}
93- {#if notice}<p class="notice" role="status">{notice}</p>{/if}
94-</section>
95-
96-<style>
97- section { margin-top: 18px; }
98- .sec-h { margin: 0 0 8px; color: var(--muted); font-size: 12px; letter-spacing: .08em; text-transform: uppercase; }
99- .field { display: flex; flex-direction: column; gap: 6px; margin: 10px 0; font-size: 13px; }
100- select, input { width: 100%; min-height: var(--ctl-h); padding: 6px 8px; border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--bg-soft); color: var(--text); font: inherit; }
101- .note, .notice { color: var(--muted); font-size: 13px; }
102- .error { color: var(--danger-text); background: var(--danger-bg); padding: 8px; border-radius: var(--r-sm); }
103-</style>
Modified file: web/src/lib/SettingsPanel.svelte−2 deletions
@@ -13,7 +13,6 @@
1313 import { DEFAULT_QUICK_REACTIONS } from './quick-reactions';
1414 import ReactionPicker from './ReactionPicker.svelte';
1515 import ThemeSettings from './ThemeSettings.svelte';
16- import HistoryRepairSettings from './HistoryRepairSettings.svelte';
1716 import type { ImagePack, QuickReaction } from './types';
1817
1918 export let quick: QuickReaction[];
@@ -36,7 +35,6 @@
3635 </div>
3736
3837 <ThemeSettings />
39- <HistoryRepairSettings />
4038
4139 <section aria-label="Message spacing">
4240 <h3 class="sec-h">Space between messages</h3>
Modified file: web/src/lib/api.ts+2 additions−12 deletions
@@ -3,16 +3,13 @@
33 import type { EventContext, EventContextPage, MemberSummary, RoomSummary } from './types';
44 import type { Emote } from './emotes';
55 import type { MentionKey, MentionsBoundary, MentionsPage, SavedMentionContext } from './mentions-model';
6-import type { HistoryRepairBudget, HistoryRepairPolicy, HistoryRepairReport } from './repair-model';
7-export { parseHistoryRepairPolicy, parseHistoryRepairReport } from './repair-model';
8-export type { HistoryRepairBudget, HistoryRepairFailure, HistoryRepairPolicy, HistoryRepairReport } from './repair-model';
96 export { mentionKey, parseMentionsPage, parseSavedMentionContext, parseMentionsUpdate } from './mentions-model';
107 export type { MentionKey, MentionReason, MentionCaptureState, MentionEntry, MentionCoverage, MentionGap, MentionsBoundary, MentionsPage, SavedMentionContext, MentionsUpdate } from './mentions-model';
118 import { formatBody } from './markdown';
129 import { mentionedUserIds, type Mention } from './mentions';
1310 import { HttpTransport } from './transport-http';
1411 import type { CommandRequest, SendKind, SessionInfo, Transport, VerifyAction } from './transport';
15-export type { HistoryRepairRequest } from './transport';
12+export type { HistoryRequest } from './transport';
1613 import { createDeviceTransport } from 'malice:device-transport';
1714
1815 export { ApiError } from './transport';
@@ -68,15 +65,8 @@ export const retryMentionContext = (room_id: string, event_id: string): Promise<
6865 export const setMentionsRead = (keys: MentionKey[], read: boolean) => transport.setMentionsRead(keys, read);
6966 export const markMentionsReadThrough = (boundary: MentionsBoundary) => transport.markMentionsReadThrough(boundary);
7067 export const recoverMentions = () => transport.recoverMentions();
71-export const historyRepair = (room_id: string, policy: HistoryRepairPolicy | null = null, budget?: HistoryRepairBudget): Promise<HistoryRepairReport> =>
72- transport.historyRepair(room_id, policy, budget);
73-export const continueHistoryRepair = (room_id: string, budget?: HistoryRepairBudget): Promise<HistoryRepairReport> =>
74- transport.continueHistoryRepair(room_id, budget);
75-export const cancelHistoryRepair = (room_id: string): Promise<void> => transport.cancelHistoryRepair(room_id);
76-export const startRecentHistory = (room_id: string): Promise<HistoryRepairReport> => transport.startRecentHistory(room_id);
68+export const startRecentHistory = (room_id: string): Promise<void> => transport.startRecentHistory(room_id);
7769 export const fillHistoryGap = (room_id: string, gap_id: string): Promise<void> => transport.fillHistoryGap(room_id, gap_id);
78-export const historyRepairPolicy = (): Promise<HistoryRepairPolicy> => transport.historyRepairPolicy();
79-export const setHistoryRepairPolicy = (policy: HistoryRepairPolicy): Promise<void> => transport.setHistoryRepairPolicy(policy);
8070 export const setTyping = (room_id: string, typing: boolean) => transport.setTyping(room_id, typing);
8171 export const getAccountData = (eventType: string) => transport.getAccountData(eventType);
8272 export const getUrlPreview = (room_id: string, url: string) => transport.getUrlPreview(room_id, url);
Deleted file: web/src/lib/repair-model.ts−94 deletions
@@ -1,94 +0,0 @@
1-export type HistoryRepairPolicy =
2- | 'StartRecent'
3- | 'Bridge'
4- | { BridgeShortGaps: { threshold_ms: number } };
5-
6-export interface HistoryRepairBudget {
7- max_pages: number;
8- max_events: number;
9- max_duration_ms: number;
10-}
11-
12-export interface HistoryRepairFailure {
13- kind: string;
14- message: string;
15- server?: { status: number; errcode: string; message: string } | null;
16- [key: string]: unknown;
17-}
18-
19-export interface HistoryRepairReport {
20- room_id: string;
21- requested_policy: HistoryRepairPolicy;
22- strategy: 'Recent' | 'Overlap' | 'Bridge';
23- coverage_duration_ms: number | null;
24- threshold_evaluated: boolean;
25- pages: number;
26- events: number;
27- phase: 'FetchingRecent' | 'Bridging' | 'Ready' | 'Paused';
28- continuity: 'Unresolved' | 'Verified' | 'AccessibleStart';
29- recent_available: boolean;
30- stop: 'Cancelled' | 'Budget' | 'HistoryUnavailable' | 'NoAnchor' | 'CursorStalled' | null;
31- failure: HistoryRepairFailure | null;
32-}
33-
34-export const DEFAULT_HISTORY_REPAIR_BUDGET: HistoryRepairBudget = {
35- max_pages: 10,
36- max_events: 1000,
37- max_duration_ms: 15000
38-};
39-
40-function record(value: unknown): Record<string, unknown> | null {
41- return typeof value === 'object' && value !== null && !Array.isArray(value)
42- ? (value as Record<string, unknown>)
43- : null;
44-}
45-
46-function safeNonnegativeInteger(value: unknown): value is number {
47- return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
48-}
49-
50-export function parseHistoryRepairPolicy(value: unknown): HistoryRepairPolicy | null {
51- if (value === 'StartRecent' || value === 'Bridge') return value;
52- const outer = record(value);
53- const details = outer && record(outer['BridgeShortGaps']);
54- if (!outer || Object.keys(outer).length !== 1 || !details || Object.keys(details).length !== 1 || !safeNonnegativeInteger(details['threshold_ms'])) return null;
55- return { BridgeShortGaps: { threshold_ms: details['threshold_ms'] } };
56-}
57-
58-function isOneOf<T extends string>(value: unknown, values: readonly T[]): value is T {
59- return typeof value === 'string' && (values as readonly string[]).includes(value);
60-}
61-
62-export function parseHistoryRepairReport(value: unknown): HistoryRepairReport | null {
63- const data = record(value);
64- if (!data) return null;
65- const policy = parseHistoryRepairPolicy(data['requested_policy']);
66- const failure = data['failure'];
67- const failureData = failure === null ? null : record(failure);
68- if (
69- typeof data['room_id'] !== 'string' || !policy ||
70- !isOneOf(data['strategy'], ['Recent', 'Overlap', 'Bridge']) ||
71- !(data['coverage_duration_ms'] === null || safeNonnegativeInteger(data['coverage_duration_ms'])) ||
72- typeof data['threshold_evaluated'] !== 'boolean' ||
73- !safeNonnegativeInteger(data['pages']) || !safeNonnegativeInteger(data['events']) ||
74- !isOneOf(data['phase'], ['FetchingRecent', 'Bridging', 'Ready', 'Paused']) ||
75- !isOneOf(data['continuity'], ['Unresolved', 'Verified', 'AccessibleStart']) ||
76- typeof data['recent_available'] !== 'boolean' ||
77- !(data['stop'] === null || isOneOf(data['stop'], ['Cancelled', 'Budget', 'HistoryUnavailable', 'NoAnchor', 'CursorStalled'])) ||
78- !(failure === null || (failureData && typeof failureData['kind'] === 'string' && typeof failureData['message'] === 'string'))
79- ) return null;
80- return {
81- room_id: data['room_id'],
82- requested_policy: policy,
83- strategy: data['strategy'],
84- coverage_duration_ms: data['coverage_duration_ms'],
85- threshold_evaluated: data['threshold_evaluated'],
86- pages: data['pages'],
87- events: data['events'],
88- phase: data['phase'],
89- continuity: data['continuity'],
90- recent_available: data['recent_available'],
91- stop: data['stop'],
92- failure: failureData as HistoryRepairFailure | null
93- };
94-}
Modified file: web/src/lib/store.ts−23 deletions
@@ -1,8 +1,6 @@
11 import { get, writable } from 'svelte/store';
22 import { parseMentionsPage, parseMentionsUpdate } from './mentions-model';
33 import type { MentionsPage } from './mentions-model';
4-import { parseHistoryRepairReport } from './repair-model';
5-import type { HistoryRepairReport } from './repair-model';
64 import { getSession, subscribeEvents } from './api';
75 import type { SoftLogout } from './api';
86 import { isRecord } from './guards';
@@ -45,12 +43,6 @@ export const rooms = writable<RoomSummary[]>([]);
4543 export const timelines = writable<Record<string, TimelineItem[]>>({});
4644 export const pinnedTimelines = writable<Record<string, TimelineItem[]>>({});
4745 export const pinnedTimelineVersions = writable<Record<string, number>>({});
48-export const historyRepairs = writable<Record<string, HistoryRepairReport>>({});
49-
50-export function publishHistoryRepair(report: HistoryRepairReport, account: string): void {
51- if (account !== get(sessionUser)) return;
52- historyRepairs.update((all) => ({ ...all, [report.room_id]: report }));
53-}
5446 export interface EventContextState {
5547 room_id: string;
5648 request_id: string;
@@ -88,7 +80,6 @@ export function resetClientState(): void {
8880 timelines.set({});
8981 pinnedTimelines.set({});
9082 pinnedTimelineVersions.set({});
91- historyRepairs.set({});
9283 eventContexts.set({});
9384 connection.set('connecting');
9485 selectedRoomId.set(null);
@@ -163,14 +154,6 @@ function handlePayload(text: string): void {
163154 const kind = data['type'];
164155 if (kind === 'snapshot') {
165156 if (typeof data['connection'] === 'string') setConnection(data['connection']);
166- const repairs: Record<string, HistoryRepairReport> = {};
167- if (isRecord(data['repairs'])) {
168- for (const [roomId, value] of Object.entries(data['repairs'])) {
169- const report = parseHistoryRepairReport(value);
170- if (report && report.room_id === roomId) repairs[roomId] = report;
171- }
172- }
173- historyRepairs.set(repairs);
174157 const mentions = parseMentionsPage(data['mentions']);
175158 if (mentions) publishMentionsPage(mentions);
176159 setRooms(data['rooms']);
@@ -202,12 +185,6 @@ function handlePayload(text: string): void {
202185 if (update) publishMentionsPage(update.snapshot);
203186 return;
204187 }
205- if (kind === 'history_repair') {
206- const report = parseHistoryRepairReport(data['report']);
207- const account = get(sessionUser);
208- if (report && account) publishHistoryRepair(report, account);
209- return;
210- }
211188 if (kind === 'image_packs') {
212189 imagePacks.set(parseImagePacks(data['packs']));
213190 return;
Modified file: web/src/lib/transport-http.ts+2 additions−27 deletions
@@ -2,8 +2,6 @@ import type { EventContext, EventContextPage, MemberSummary, RoomSummary } from
22 import { parseEventContext, parseEventContextPage } from './types';
33 import { parseMentionsPage, parseSavedMentionContext } from './mentions-model';
44 import type { MentionKey, MentionsBoundary, MentionsPage, SavedMentionContext } from './mentions-model';
5-import { DEFAULT_HISTORY_REPAIR_BUDGET, parseHistoryRepairPolicy, parseHistoryRepairReport } from './repair-model';
6-import type { HistoryRepairBudget, HistoryRepairPolicy, HistoryRepairReport } from './repair-model';
75 import { ApiError, type CommandRequest, type SendKind, type SessionInfo, type Transport, type UploadExtras, type VerifyAction } from './transport';
86
97 async function parseFailure(res: Response, fallback: string): Promise<never> {
@@ -142,35 +140,12 @@ export class HttpTransport implements Transport {
142140 await this.command({ op: 'mark_mentions_read_through', boundary });
143141 }
144142 async recoverMentions(): Promise<void> { await this.command({ op: 'recover_mentions' }); }
145- async historyRepair(room_id: string, policy: HistoryRepairPolicy | null = null, budget: HistoryRepairBudget = DEFAULT_HISTORY_REPAIR_BUDGET): Promise<HistoryRepairReport> {
146- const parsed = parseHistoryRepairReport(await this.command({ op: 'history_repair', room_id, policy, budget }));
147- if (!parsed) throw new ApiError(502, 'Invalid history repair response');
148- return parsed;
149- }
150- async continueHistoryRepair(room_id: string, budget: HistoryRepairBudget = DEFAULT_HISTORY_REPAIR_BUDGET): Promise<HistoryRepairReport> {
151- const parsed = parseHistoryRepairReport(await this.command({ op: 'continue_history_repair', room_id, budget }));
152- if (!parsed) throw new ApiError(502, 'Invalid history repair response');
153- return parsed;
154- }
155- async cancelHistoryRepair(room_id: string): Promise<void> {
156- await this.command({ op: 'cancel_history_repair', room_id });
157- }
158- async startRecentHistory(room_id: string): Promise<HistoryRepairReport> {
159- const parsed = parseHistoryRepairReport(await this.command({ op: 'start_recent_history', room_id }));
160- if (!parsed) throw new ApiError(502, 'Invalid history repair response');
161- return parsed;
143+ async startRecentHistory(room_id: string): Promise<void> {
144+ await this.command({ op: 'start_recent_history', room_id });
162145 }
163146 async fillHistoryGap(room_id: string, gap_id: string): Promise<void> {
164147 await this.command({ op: 'fill_history_gap', room_id, gap_id });
165148 }
166- async historyRepairPolicy(): Promise<HistoryRepairPolicy> {
167- const parsed = parseHistoryRepairPolicy(await this.command({ op: 'history_repair_policy' }));
168- if (!parsed) throw new ApiError(502, 'Invalid history repair policy response');
169- return parsed;
170- }
171- async setHistoryRepairPolicy(policy: HistoryRepairPolicy): Promise<void> {
172- await this.command({ op: 'set_history_repair_policy', policy });
173- }
174149 async eventContextBefore(room_id: string, event_id: string, from: string, limit = 20): Promise<EventContextPage> {
175150 return this.eventContextPage('before', room_id, event_id, from, limit);
176151 }
Modified file: web/src/lib/transport-worker.ts+2 additions−27 deletions
@@ -1,8 +1,6 @@
11 import { parseEventContext, parseEventContextPage } from './types';
22 import { parseMentionsPage, parseSavedMentionContext } from './mentions-model';
33 import type { MentionKey, MentionsBoundary, MentionsPage, SavedMentionContext } from './mentions-model';
4-import { DEFAULT_HISTORY_REPAIR_BUDGET, parseHistoryRepairPolicy, parseHistoryRepairReport } from './repair-model';
5-import type { HistoryRepairBudget, HistoryRepairPolicy, HistoryRepairReport } from './repair-model';
64 import type { EventContext, EventContextPage, MemberSummary, RoomSummary } from './types';
75 import { ApiError, type CommandRequest, type SendKind, type SessionInfo, type SoftLogout, type Transport, type UploadExtras, type VerifyAction } from './transport';
86 import { statusForKind } from './wire-status';
@@ -249,35 +247,12 @@ export class WorkerTransport implements Transport {
249247 async setMentionsRead(keys: MentionKey[], read: boolean): Promise<void> { await this.client.call('set_mentions_read', { keys, read }); }
250248 async markMentionsReadThrough(boundary: MentionsBoundary): Promise<void> { await this.client.call('mark_mentions_read_through', { boundary }); }
251249 async recoverMentions(): Promise<void> { await this.client.call('recover_mentions'); }
252- async historyRepair(room_id: string, policy: HistoryRepairPolicy | null = null, budget: HistoryRepairBudget = DEFAULT_HISTORY_REPAIR_BUDGET): Promise<HistoryRepairReport> {
253- const parsed = parseHistoryRepairReport(await this.client.call('history_repair', { room_id, policy, budget }));
254- if (!parsed) throw new ApiError(502, 'Invalid history repair response');
255- return parsed;
256- }
257- async continueHistoryRepair(room_id: string, budget: HistoryRepairBudget = DEFAULT_HISTORY_REPAIR_BUDGET): Promise<HistoryRepairReport> {
258- const parsed = parseHistoryRepairReport(await this.client.call('continue_history_repair', { room_id, budget }));
259- if (!parsed) throw new ApiError(502, 'Invalid history repair response');
260- return parsed;
261- }
262- async cancelHistoryRepair(room_id: string): Promise<void> {
263- await this.client.call('cancel_history_repair', { room_id });
264- }
265- async startRecentHistory(room_id: string): Promise<HistoryRepairReport> {
266- const parsed = parseHistoryRepairReport(await this.client.call('start_recent_history', { room_id }));
267- if (!parsed) throw new ApiError(502, 'Invalid history repair response');
268- return parsed;
250+ async startRecentHistory(room_id: string): Promise<void> {
251+ await this.client.call('start_recent_history', { room_id });
269252 }
270253 async fillHistoryGap(room_id: string, gap_id: string): Promise<void> {
271254 await this.client.call('fill_history_gap', { room_id, gap_id });
272255 }
273- async historyRepairPolicy(): Promise<HistoryRepairPolicy> {
274- const parsed = parseHistoryRepairPolicy(await this.client.call('history_repair_policy'));
275- if (!parsed) throw new ApiError(502, 'Invalid history repair policy response');
276- return parsed;
277- }
278- async setHistoryRepairPolicy(policy: HistoryRepairPolicy): Promise<void> {
279- await this.client.call('set_history_repair_policy', { policy });
280- }
281256 eventContext(room_id: string, event_id: string, limit = 20): Promise<EventContext> {
282257 return this.contextCall('event_context', { room_id, event_id, limit });
283258 }
Modified file: web/src/lib/transport.ts+4 additions−15 deletions
@@ -1,6 +1,5 @@
11 import type { EventContext, EventContextPage, MemberSummary, PackUsage, RoomSummary } from './types';
22 import type { MentionKey, MentionsBoundary, MentionsPage, SavedMentionContext } from './mentions-model';
3-import type { HistoryRepairBudget, HistoryRepairPolicy, HistoryRepairReport } from './repair-model';
43
54 export class ApiError extends Error {
65 status: number;
@@ -65,17 +64,12 @@ export type CommandRequest =
6564 | { op: 'clone_pack_image'; from_pack: string; shortcode: string; to: PackTarget; usage?: PackUsage[] }
6665 | { op: 'clone_sticker'; room_id: string; event_id: string; to: PackTarget; shortcode: string; usage?: PackUsage[] }
6766 | { op: 'clone_pack'; from_pack: string; to: PackTarget; display_name: string | null; usage?: PackUsage[] }
68- | HistoryRepairRequest
67+ | HistoryRequest
6968 | MentionsRequest;
7069
71-export type HistoryRepairRequest =
72- | { op: 'history_repair'; room_id: string; policy: HistoryRepairPolicy | null; budget: HistoryRepairBudget }
73- | { op: 'continue_history_repair'; room_id: string; budget: HistoryRepairBudget }
74- | { op: 'cancel_history_repair'; room_id: string }
70+export type HistoryRequest =
7571 | { op: 'start_recent_history'; room_id: string }
76- | { op: 'fill_history_gap'; room_id: string; gap_id: string }
77- | { op: 'history_repair_policy' }
78- | { op: 'set_history_repair_policy'; policy: HistoryRepairPolicy };
72+ | { op: 'fill_history_gap'; room_id: string; gap_id: string };
7973
8074 export type MentionsRequest =
8175 | { op: 'mentions_page'; cursor: string | null; limit: number }
@@ -127,13 +121,8 @@ export interface Transport {
127121 setMentionsRead(keys: MentionKey[], read: boolean): Promise<void>;
128122 markMentionsReadThrough(boundary: MentionsBoundary): Promise<void>;
129123 recoverMentions(): Promise<void>;
130- historyRepair(room_id: string, policy?: HistoryRepairPolicy | null, budget?: HistoryRepairBudget): Promise<HistoryRepairReport>;
131- continueHistoryRepair(room_id: string, budget?: HistoryRepairBudget): Promise<HistoryRepairReport>;
132- cancelHistoryRepair(room_id: string): Promise<void>;
133- startRecentHistory(room_id: string): Promise<HistoryRepairReport>;
124+ startRecentHistory(room_id: string): Promise<void>;
134125 fillHistoryGap(room_id: string, gap_id: string): Promise<void>;
135- historyRepairPolicy(): Promise<HistoryRepairPolicy>;
136- setHistoryRepairPolicy(policy: HistoryRepairPolicy): Promise<void>;
137126 setTyping(room_id: string, typing: boolean): Promise<void>;
138127 getAccountData(eventType: string): Promise<unknown | null>;
139128 getUrlPreview(room_id: string, url: string): Promise<unknown | null>;